Skip to content

Full Script Source

This page renders the reference files directly from /scripts. The displayed code is therefore the same version that is parsed and versioned in the repository; it is not a second, manually maintained copy.

Use the copy button on each block to paste the complete script into the appropriate local PowerShell or shell session. Read the relevant runbook first: execution order, prerequisites, configurable parameters, validation evidence, and validated boundaries remain in the runbook and Script Catalog.

Deploy-HQ-CHR01.sh

#!/usr/bin/env bash

set -Eeuo pipefail

VMID="${VMID:-4010}"
SOURCE_TEMPLATE_VMID="${SOURCE_TEMPLATE_VMID:-4004}"
VM_NAME="${VM_NAME:-HQ-CHR01}"
WAN_BRIDGE="${WAN_BRIDGE:-GEILWAN}"
LAN_BRIDGE="${LAN_BRIDGE:-GEILLAN}"
WAN_IP_CIDR="${WAN_IP_CIDR:-172.31.255.2/30}"
WAN_GATEWAY="${WAN_GATEWAY:-172.31.255.1}"
TIMEZONE_NAME="${TIMEZONE_NAME:-America/Santo_Domingo}"
AUTORUN_IDENTITY_NAME="${AUTORUN_IDENTITY_NAME:-$VM_NAME}"
STARTUP_ORDER="${STARTUP_ORDER:-10}"
STARTUP_UP_DELAY="${STARTUP_UP_DELAY:-30}"
STARTUP_DOWN_DELAY="${STARTUP_DOWN_DELAY:-60}"
KEEP_AUTORUN_TEST_FILE="${KEEP_AUTORUN_TEST_FILE:-0}"
SKIP_BOOT_VALIDATION="${SKIP_BOOT_VALIDATION:-0}"

log() {
    printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

fail() {
    printf 'ERROR: %s\n' "$*" >&2
    exit 1
}

require_command() {
    command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}

bridge_exists() {
    ip link show "$1" >/dev/null 2>&1
}

cleanup_mount() {
    mountpoint -q /mnt/gntech-chr-rw && umount /mnt/gntech-chr-rw || true
}

mount_rw_partition() {
    local disk_path zvol_dev
    disk_path="$(qm config "$VMID" | awk -F': ' '/^virtio0:/ {print $2; exit}' | cut -d',' -f1)"
    [[ -n "$disk_path" ]] || fail "Could not resolve virtio0 path for VM $VMID"

    zvol_dev="$(readlink -f "/dev/zvol/$disk_path")"
    [[ -b "$zvol_dev" ]] || fail "Resolved zvol device is not a block device: $zvol_dev"
    [[ -b "${zvol_dev}p2" ]] || fail "RouterOS RW partition not found: ${zvol_dev}p2"

    mkdir -p /mnt/gntech-chr-rw
    mount -o rw "${zvol_dev}p2" /mnt/gntech-chr-rw
}

write_autorun() {
    local marker_line=""
    if [[ "$KEEP_AUTORUN_TEST_FILE" == "1" ]]; then
        marker_line='/file print file=bootstrap-validation'
    fi

    cat > /mnt/gntech-chr-rw/rw/autorun.scr <<EOF
/system identity set name=${AUTORUN_IDENTITY_NAME}
/system clock set time-zone-name=${TIMEZONE_NAME}
/ip dhcp-client remove [find where interface=ether1]
/ip address add address=${WAN_IP_CIDR} interface=ether1 comment=WAN
/ip route add dst-address=0.0.0.0/0 gateway=${WAN_GATEWAY} comment=Default
${marker_line}
EOF
    sync
}

validate_boot() {
    log "Waiting for QEMU Guest Agent on VM ${VMID}"
    local ready=0
    for _ in $(seq 1 30); do
        if qm guest cmd "$VMID" ping >/dev/null 2>&1; then
            ready=1
            break
        fi
        sleep 2
    done
    [[ "$ready" == "1" ]] || fail "QEMU Guest Agent did not respond within 60 seconds on VM ${VMID}"

    log "Collecting first-boot guest state"
    qm guest cmd "$VMID" get-osinfo
    qm guest cmd "$VMID" get-host-name
    qm guest cmd "$VMID" network-get-interfaces
}

trap cleanup_mount EXIT

require_command qm
require_command ip
require_command mount
require_command umount
require_command readlink

qm status "$SOURCE_TEMPLATE_VMID" >/dev/null 2>&1 || fail "Source template VMID ${SOURCE_TEMPLATE_VMID} not found"
qm config "$SOURCE_TEMPLATE_VMID" | grep -q '^template: 1' || fail "Source VMID ${SOURCE_TEMPLATE_VMID} is not a template"
qm status "$VMID" >/dev/null 2>&1 && fail "Target VMID ${VMID} already exists"

bridge_exists "$WAN_BRIDGE" || fail "WAN bridge ${WAN_BRIDGE} does not exist"
bridge_exists "$LAN_BRIDGE" || fail "LAN bridge ${LAN_BRIDGE} does not exist"

log "Cloning template ${SOURCE_TEMPLATE_VMID} to ${VMID}"
qm clone "$SOURCE_TEMPLATE_VMID" "$VMID" --name "$VM_NAME" \
    --description "Enterprise headquarters MikroTik CHR router and firewall. Clean validation deploy from TPL-CHR-7.21.5 (${SOURCE_TEMPLATE_VMID})."

log "Applying Proxmox-side runtime settings"
qm set "$VMID" \
    --net0 "virtio,bridge=${WAN_BRIDGE},firewall=0" \
    --net1 "virtio,bridge=${LAN_BRIDGE},firewall=0" \
    --onboot 0 \
    --startup "order=${STARTUP_ORDER},up=${STARTUP_UP_DELAY},down=${STARTUP_DOWN_DELAY}" \
    --tags "enterprise;hq;router;firewall;mikrotik"

log "Injecting first-boot RouterOS bootstrap into rw/autorun.scr"
mount_rw_partition
write_autorun
cleanup_mount

log "Starting VM ${VMID}"
qm start "$VMID"

if [[ "$SKIP_BOOT_VALIDATION" != "1" ]]; then
    validate_boot
fi

log "Deployment completed for VM ${VMID}"

Invoke-Qga-WindowsBootstrap.sh

#!/usr/bin/env bash

set -Eeuo pipefail

VMID="${VMID:-}"
GUEST_NAME="${GUEST_NAME:-Windows guest}"
IPV4_ADDRESS="${IPV4_ADDRESS:-}"
PREFIX_LENGTH="${PREFIX_LENGTH:-24}"
GATEWAY="${GATEWAY:-}"
DNS_SERVERS="${DNS_SERVERS:-}"
ENABLE_RDP="${ENABLE_RDP:-1}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-900}"

log() {
    printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

fail() {
    printf 'ERROR: %s\n' "$*" >&2
    exit 1
}

require_command() {
    command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}

require_value() {
    local name="$1"
    local value="$2"
    [[ -n "$value" ]] || fail "Missing required value: $name"
}

wait_for_qga() {
    local ready=0
    for _ in $(seq 1 60); do
        if qm guest cmd "$VMID" ping >/dev/null 2>&1; then
            ready=1
            break
        fi
        sleep 2
    done
    [[ "$ready" == "1" ]] || fail "QEMU Guest Agent did not respond on VM $VMID"
}

run_powershell_via_qga() {
    local script_file="$1"
    local encoded
    encoded="$(
        iconv -f UTF-8 -t UTF-16LE "$script_file" |
        base64 -w 0
    )"

    local response pid
    response="$(
        qm guest exec "$VMID" -- \
            powershell.exe \
            -NoProfile \
            -NonInteractive \
            -ExecutionPolicy Bypass \
            -EncodedCommand "$encoded"
    )"

    pid="$(printf '%s\n' "$response" | sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n 1)"
    [[ -n "$pid" ]] || fail "Could not obtain guest PID from qm guest exec response"

    local waited=0
    while (( waited < TIMEOUT_SECONDS )); do
        local status
        status="$(qm guest exec-status "$VMID" "$pid")"
        if printf '%s' "$status" | grep -q '"exited"[[:space:]]*:[[:space:]]*true'; then
            printf '%s\n' "$status"
            return 0
        fi
        sleep 5
        waited=$(( waited + 5 ))
    done

    fail "Guest command timed out after ${TIMEOUT_SECONDS} seconds"
}

require_command qm
require_command iconv
require_command base64
require_command sed

require_value "VMID" "$VMID"
require_value "IPV4_ADDRESS" "$IPV4_ADDRESS"
require_value "GATEWAY" "$GATEWAY"
require_value "DNS_SERVERS" "$DNS_SERVERS"

qm status "$VMID" >/dev/null 2>&1 || fail "VMID $VMID was not found"

log "Waiting for QEMU Guest Agent on VM $VMID ($GUEST_NAME)"
wait_for_qga

TMP_SCRIPT="$(mktemp)"
trap 'rm -f "$TMP_SCRIPT"' EXIT

DNS_SERVERS_CSV="$DNS_SERVERS" python3 - <<'PY' > "${TMP_SCRIPT}.dns"
import os
servers = [s.strip() for s in os.environ["DNS_SERVERS_CSV"].split(",") if s.strip()]
print(",".join("'" + s.replace("'", "''") + "'" for s in servers))
PY

DNS_ARRAY_LITERAL="$(cat "${TMP_SCRIPT}.dns")"
rm -f "${TMP_SCRIPT}.dns"

cat > "$TMP_SCRIPT" <<EOF
\$ErrorActionPreference = 'Stop'
\$ProgressPreference = 'SilentlyContinue'

function Get-PrimaryAdapter {
    \$adapter = Get-NetAdapter |
        Where-Object Status -eq 'Up' |
        Sort-Object ifIndex |
        Select-Object -First 1

    if (-not \$adapter) {
        throw 'No active network adapter was found.'
    }

    \$adapter
}

function Set-ExactIPv4Configuration {
    param(
        [int]\$InterfaceIndex,
        [string]\$DesiredIPv4,
        [int]\$DesiredPrefixLength,
        [string]\$DesiredGateway
    )

    \$existing = Get-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue

    foreach (\$address in \$existing) {
        if (\$address.IPAddress -eq \$DesiredIPv4 -and \$address.PrefixLength -eq \$DesiredPrefixLength) {
            continue
        }

        if (\$address.IPAddress -like '169.254.*') {
            continue
        }

        Remove-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -IPAddress \$address.IPAddress -Confirm:\$false -ErrorAction SilentlyContinue
    }

    \$desiredAddress = Get-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
        Where-Object { \$_.IPAddress -eq \$DesiredIPv4 -and \$_.PrefixLength -eq \$DesiredPrefixLength } |
        Select-Object -First 1

    if (-not \$desiredAddress) {
        New-NetIPAddress -InterfaceIndex \$InterfaceIndex -IPAddress \$DesiredIPv4 -PrefixLength \$DesiredPrefixLength | Out-Null
    }

    \$defaultRoutes = Get-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue

    foreach (\$route in \$defaultRoutes) {
        if (\$route.NextHop -ne \$DesiredGateway) {
            Remove-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop \$route.NextHop -Confirm:\$false -ErrorAction SilentlyContinue
        }
    }

    \$desiredRoute = Get-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue |
        Where-Object NextHop -eq \$DesiredGateway |
        Select-Object -First 1

    if (-not \$desiredRoute) {
        New-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop \$DesiredGateway -RouteMetric 256 | Out-Null
    }
}

\$adapter = Get-PrimaryAdapter
Set-ExactIPv4Configuration -InterfaceIndex \$adapter.ifIndex -DesiredIPv4 '${IPV4_ADDRESS}' -DesiredPrefixLength ${PREFIX_LENGTH} -DesiredGateway '${GATEWAY}'
Set-DnsClientServerAddress -InterfaceIndex \$adapter.ifIndex -ServerAddresses @(${DNS_ARRAY_LITERAL})

if (${ENABLE_RDP} -eq 1) {
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -Name 'fDenyTSConnections' -Value 0
    Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
}

[pscustomobject]@{
    ComputerName = \$env:COMPUTERNAME
    Adapter = \$adapter.Name
    IPv4 = '${IPV4_ADDRESS}'
    PrefixLength = ${PREFIX_LENGTH}
    Gateway = '${GATEWAY}'
    DnsServers = '${DNS_SERVERS}'
    RdpEnabled = [bool](${ENABLE_RDP})
}
EOF

log "Applying guest network bootstrap through QGA"
run_powershell_via_qga "$TMP_SCRIPT"

log "Bootstrap completed for VM $VMID"

qga-run.sh

#!/usr/bin/env bash

set -Eeuo pipefail

usage() {
    cat <<'EOF'
Usage:
  qga-run.sh <VMID> <PowerShell-script.ps1> [timeout-seconds]

Example:
  qga-run.sh 4000 validate-golden-image.ps1 900
EOF
}

fail() {
    echo "ERROR: $*" >&2
    exit 1
}

[[ $# -ge 2 && $# -le 3 ]] || {
    usage
    exit 2
}

VMID="$1"
SCRIPT_FILE="$2"
TIMEOUT_SECONDS="${3:-900}"
POLL_SECONDS=5

command -v qm >/dev/null 2>&1 || fail "qm was not found."
command -v jq >/dev/null 2>&1 || fail "jq was not found. Install it with: apt install jq"
command -v iconv >/dev/null 2>&1 || fail "iconv was not found."
command -v base64 >/dev/null 2>&1 || fail "base64 was not found."

[[ -f "$SCRIPT_FILE" ]] || fail "PowerShell script not found: $SCRIPT_FILE"
[[ "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || fail "Timeout must be an integer."

qm status "$VMID" >/dev/null 2>&1 || fail "VMID $VMID does not exist."
[[ "$(qm status "$VMID" | awk '{print $2}')" == "running" ]] || fail "VMID $VMID is not running."
qm guest cmd "$VMID" ping >/dev/null 2>&1 || fail "QEMU Guest Agent is not responding for VMID $VMID."

ENCODED_COMMAND="$(iconv -f UTF-8 -t UTF-16LE "$SCRIPT_FILE" | base64 -w 0)"

START_RESPONSE="$(
    qm guest exec "$VMID" -- powershell.exe -NoLogo -NoProfile -NonInteractive \
        -ExecutionPolicy Bypass -EncodedCommand "$ENCODED_COMMAND"
)"

if jq -e '.exited == 1' >/dev/null 2>&1 <<<"$START_RESPONSE"; then
    FINAL_RESPONSE="$START_RESPONSE"
else
    PID="$(jq -r '.pid // empty' <<<"$START_RESPONSE")"
    [[ -n "$PID" ]] || fail "Unable to determine guest execution PID. Response: $START_RESPONSE"
    echo "Guest execution started with PID $PID."
    START_TIME="$(date +%s)"

    while true; do
        FINAL_RESPONSE="$(qm guest exec-status "$VMID" "$PID")"
        jq -e '.exited == 1' >/dev/null 2>&1 <<<"$FINAL_RESPONSE" && break

        CURRENT_TIME="$(date +%s)"
        if (( CURRENT_TIME - START_TIME >= TIMEOUT_SECONDS )); then
            echo "Timeout reached; the guest process may still be running."
            echo "Check it with: qm guest exec-status $VMID $PID"
            exit 124
        fi
        sleep "$POLL_SECONDS"
    done
fi

STDOUT="$(jq -r '."out-data" // empty' <<<"$FINAL_RESPONSE")"
STDERR="$(jq -r '."err-data" // empty' <<<"$FINAL_RESPONSE")"
EXIT_CODE="$(jq -r '.exitcode // 1' <<<"$FINAL_RESPONSE")"

[[ -z "$STDOUT" ]] || printf '%s\n' "$STDOUT"
[[ -z "$STDERR" ]] || printf '%s\n' "$STDERR" >&2
[[ "$EXIT_CODE" -eq 0 ]] || fail "PowerShell script failed with exit code $EXIT_CODE."

echo "QGA execution completed successfully."

Configure-HQ-DC01-GPOBaseline.ps1

param(
    [string]$DomainFqdn,
    [string]$DomainDn,
    [string]$EnterpriseRootOuDn,
    [string]$DomainControllersDn,
    [string]$ServersOuDn,
    [string]$WorkstationsOuDn,
    [string]$DcBaselineGpoName,
    [string]$ServerBaselineGpoName,
    [string]$WorkstationBaselineGpoName,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:DomainFqdn = if ([string]::IsNullOrWhiteSpace($DomainFqdn)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainFqdn.Trim() }
    $script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) { Read-RequiredValue 'Domain DN' 'DC=corp,DC=gntech,DC=me' } else { $DomainDn.Trim() }
    $script:EnterpriseRootOuDn = if ([string]::IsNullOrWhiteSpace($EnterpriseRootOuDn)) { Read-RequiredValue 'Enterprise root OU DN' 'OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $EnterpriseRootOuDn.Trim() }
    $script:DomainControllersDn = if ([string]::IsNullOrWhiteSpace($DomainControllersDn)) { Read-RequiredValue 'Domain Controllers OU DN' 'OU=Domain Controllers,DC=corp,DC=gntech,DC=me' } else { $DomainControllersDn.Trim() }
    $script:ServersOuDn = if ([string]::IsNullOrWhiteSpace($ServersOuDn)) { Read-RequiredValue 'Servers OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $ServersOuDn.Trim() }
    $script:WorkstationsOuDn = if ([string]::IsNullOrWhiteSpace($WorkstationsOuDn)) { Read-RequiredValue 'Workstations OU DN' 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $WorkstationsOuDn.Trim() }
    $script:DcBaselineGpoName = if ([string]::IsNullOrWhiteSpace($DcBaselineGpoName)) { Read-RequiredValue 'DC baseline GPO name' 'GPO-DC-Baseline' } else { $DcBaselineGpoName.Trim() }
    $script:ServerBaselineGpoName = if ([string]::IsNullOrWhiteSpace($ServerBaselineGpoName)) { Read-RequiredValue 'Server baseline GPO name' 'GPO-Servers-Baseline' } else { $ServerBaselineGpoName.Trim() }
    $script:WorkstationBaselineGpoName = if ([string]::IsNullOrWhiteSpace($WorkstationBaselineGpoName)) { Read-RequiredValue 'Workstation baseline GPO name' 'GPO-Workstations-Baseline' } else { $WorkstationBaselineGpoName.Trim() }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-Gpo {
    param(
        [string]$Name,
        [string]$Comment
    )

    $gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
    if (-not $gpo) {
        $gpo = New-GPO -Name $Name -Comment $Comment
    }
    return $gpo
}

function Ensure-GpoLink {
    param(
        [string]$Name,
        [string]$Target
    )

    $existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
    if ($existingLink.Count -eq 0) {
        New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
    }
}

function Set-BaselineRegistryValue {
    param(
        [string]$GpoName,
        [string]$Key,
        [string]$ValueName,
        [string]$Type,
        [object]$Value
    )

    Set-GPRegistryValue -Name $GpoName -Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name GroupPolicy

$summary = [pscustomobject]@{
    DomainFqdn = $script:DomainFqdn
    DomainDn = $script:DomainDn
    EnterpriseRootOuDn = $script:EnterpriseRootOuDn
    DomainControllersDn = $script:DomainControllersDn
    ServersOuDn = $script:ServersOuDn
    WorkstationsOuDn = $script:WorkstationsOuDn
    DcBaselineGpoName = $script:DcBaselineGpoName
    ServerBaselineGpoName = $script:ServerBaselineGpoName
    WorkstationBaselineGpoName = $script:WorkstationBaselineGpoName
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. GPO baseline was not applied.'
    return
}

Set-ADDefaultDomainPasswordPolicy `
    -Identity $script:DomainFqdn `
    -ComplexityEnabled $true `
    -MinPasswordLength 14 `
    -PasswordHistoryCount 24 `
    -MinPasswordAge 1.00:00:00 `
    -MaxPasswordAge 90.00:00:00 `
    -LockoutThreshold 10 `
    -LockoutDuration 0.00:15:00 `
    -LockoutObservationWindow 0.00:15:00

$definitions = @(
    @{ Name = $script:DcBaselineGpoName; Comment = 'Baseline for domain controllers'; Target = $script:DomainControllersDn },
    @{ Name = $script:ServerBaselineGpoName; Comment = 'Baseline for member servers'; Target = $script:ServersOuDn },
    @{ Name = $script:WorkstationBaselineGpoName; Comment = 'Baseline for workstation endpoints'; Target = $script:WorkstationsOuDn }
)

foreach ($definition in $definitions) {
    Ensure-Gpo -Name $definition.Name -Comment $definition.Comment | Out-Null
    Ensure-GpoLink -Name $definition.Name -Target $definition.Target
}

foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName)) {
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 131072
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 262144
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 131072
}

Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 65536
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 131072
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 65536

foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName, $script:WorkstationBaselineGpoName)) {
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -ValueName 'EnableScriptBlockLogging' -Type DWord -Value 1
}

Write-Output 'GPO baseline completed successfully.'

Configure-HQ-DC01-PostPromotion.ps1

param(
    [string]$DomainFqdn,
    [string]$DomainDn,
    [string]$DcFqdn,
    [string]$DcIp,
    [string]$UpnSuffix,
    [string[]]$DnsForwarders,
    [string]$ServerReverseNetworkId,
    [string]$WorkstationReverseNetworkId,
    [string]$DhcpScopeName,
    [string]$DhcpScopeId,
    [string]$DhcpStartRange,
    [string]$DhcpEndRange,
    [string]$DhcpSubnetMask,
    [string]$DhcpRouter,
    [string]$EnterpriseRootOuName,
    [string]$OuTemplateCsvPath,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-StringList {
    param(
        [string]$Prompt,
        [string[]]$DefaultValues
    )

    while ($true) {
        $defaultValue = $DefaultValues -join ','
        $value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
        $items = @(
            $value.Split(',') |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )

        if ($items.Count -gt 0) {
            return $items
        }
    }
}

function Resolve-Inputs {
    $script:DomainFqdn = if ([string]::IsNullOrWhiteSpace($DomainFqdn)) {
        Read-RequiredValue -Prompt 'Domain FQDN' -DefaultValue 'corp.gntech.me'
    } else { $DomainFqdn.Trim() }

    $script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) {
        Read-RequiredValue -Prompt 'Domain distinguished name' -DefaultValue 'DC=corp,DC=gntech,DC=me'
    } else { $DomainDn.Trim() }

    $script:DcFqdn = if ([string]::IsNullOrWhiteSpace($DcFqdn)) {
        Read-RequiredValue -Prompt 'Domain controller FQDN' -DefaultValue 'HQ-DC01.corp.gntech.me'
    } else { $DcFqdn.Trim() }

    $script:DcIp = if ([string]::IsNullOrWhiteSpace($DcIp)) {
        Read-RequiredValue -Prompt 'Domain controller IPv4' -DefaultValue '172.20.20.11'
    } else { $DcIp.Trim() }

    $script:UpnSuffix = if ([string]::IsNullOrWhiteSpace($UpnSuffix)) {
        Read-RequiredValue -Prompt 'User UPN suffix' -DefaultValue 'gntech.me'
    } else { $UpnSuffix.Trim() }

    $script:DnsForwarders = if ($DnsForwarders.Count -eq 0) {
        Read-StringList -Prompt 'DNS forwarders (comma-separated)' -DefaultValues @('1.1.1.1','9.9.9.9')
    } else {
        @($DnsForwarders | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }

    $script:ServerReverseNetworkId = if ([string]::IsNullOrWhiteSpace($ServerReverseNetworkId)) {
        Read-RequiredValue -Prompt 'Server reverse zone network' -DefaultValue '172.20.20.0/24'
    } else { $ServerReverseNetworkId.Trim() }

    $script:WorkstationReverseNetworkId = if ([string]::IsNullOrWhiteSpace($WorkstationReverseNetworkId)) {
        Read-RequiredValue -Prompt 'Workstation reverse zone network' -DefaultValue '172.20.30.0/24'
    } else { $WorkstationReverseNetworkId.Trim() }

    $script:DhcpScopeName = if ([string]::IsNullOrWhiteSpace($DhcpScopeName)) {
        Read-RequiredValue -Prompt 'DHCP scope name' -DefaultValue 'VLAN30 Workstations'
    } else { $DhcpScopeName.Trim() }

    $script:DhcpScopeId = if ([string]::IsNullOrWhiteSpace($DhcpScopeId)) {
        Read-RequiredValue -Prompt 'DHCP scope ID' -DefaultValue '172.20.30.0'
    } else { $DhcpScopeId.Trim() }

    $script:DhcpStartRange = if ([string]::IsNullOrWhiteSpace($DhcpStartRange)) {
        Read-RequiredValue -Prompt 'DHCP start range' -DefaultValue '172.20.30.100'
    } else { $DhcpStartRange.Trim() }

    $script:DhcpEndRange = if ([string]::IsNullOrWhiteSpace($DhcpEndRange)) {
        Read-RequiredValue -Prompt 'DHCP end range' -DefaultValue '172.20.30.199'
    } else { $DhcpEndRange.Trim() }

    $script:DhcpSubnetMask = if ([string]::IsNullOrWhiteSpace($DhcpSubnetMask)) {
        Read-RequiredValue -Prompt 'DHCP subnet mask' -DefaultValue '255.255.255.0'
    } else { $DhcpSubnetMask.Trim() }

    $script:DhcpRouter = if ([string]::IsNullOrWhiteSpace($DhcpRouter)) {
        Read-RequiredValue -Prompt 'DHCP router option' -DefaultValue '172.20.30.1'
    } else { $DhcpRouter.Trim() }

    $script:EnterpriseRootOuName = if ([string]::IsNullOrWhiteSpace($EnterpriseRootOuName)) {
        Read-RequiredValue -Prompt 'Enterprise root OU name' -DefaultValue 'GNTECH'
    } else { $EnterpriseRootOuName.Trim() }

    $script:OuTemplateCsvPath = if ([string]::IsNullOrWhiteSpace($OuTemplateCsvPath)) {
        $scriptDirectory = if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
            $PSScriptRoot
        } else {
            (Get-Location).Path
        }

        $defaultCsv = Join-Path -Path $scriptDirectory -ChildPath 'ou-baseline.csv'
        if (Test-Path -LiteralPath $defaultCsv) { $defaultCsv } else { '' }
    } else { $OuTemplateCsvPath.Trim() }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-UpnSuffix {
    $partitionsDn = "CN=Partitions,CN=Configuration,$script:DomainDn"
    $partitionsObject = Get-ADObject $partitionsDn -Properties uPNSuffixes
    if ($partitionsObject.uPNSuffixes -notcontains $script:UpnSuffix) {
        Set-ADObject $partitionsDn -Add @{ uPNSuffixes = $script:UpnSuffix }
    }
}

function Ensure-DnsForwarders {
    $existingForwarders = @(Get-DnsServerForwarder -ErrorAction SilentlyContinue | ForEach-Object {
        if ($_.IPAddress -is [array]) {
            $_.IPAddress | ForEach-Object { $_.IPAddressToString }
        }
        elseif ($_.IPAddress) {
            $_.IPAddress.IPAddressToString
        }
    })

    foreach ($forwarder in $script:DnsForwarders) {
        if ($existingForwarders -notcontains $forwarder) {
            Add-DnsServerForwarder -IPAddress $forwarder
        }
    }
}

function Ensure-DnsZone {
    param([string]$NetworkId)
    $networkAddress = $NetworkId.Split('/')[0]
    $octets = $networkAddress.Split('.')
    $zoneName = "$($octets[2]).$($octets[1]).$($octets[0]).in-addr.arpa"

    if (-not (Get-DnsServerZone -Name $zoneName -ErrorAction SilentlyContinue)) {
        Add-DnsServerPrimaryZone -NetworkId $NetworkId -ReplicationScope Forest
    }
}

function Ensure-DhcpAuthorization {
    $authorized = @(Get-DhcpServerInDC -ErrorAction SilentlyContinue)
    if (-not ($authorized | Where-Object DnsName -eq $script:DcFqdn)) {
        Add-DhcpServerInDC -DnsName $script:DcFqdn -IpAddress $script:DcIp
    }

    & netsh dhcp add securitygroups | Out-Null
    Restart-Service DHCPServer -Force
    Start-Sleep -Seconds 10
}

function Ensure-DhcpScope {
    if (-not (Get-DhcpServerv4Scope -ErrorAction SilentlyContinue | Where-Object ScopeId -eq $script:DhcpScopeId)) {
        Add-DhcpServerv4Scope `
            -Name $script:DhcpScopeName `
            -StartRange $script:DhcpStartRange `
            -EndRange $script:DhcpEndRange `
            -SubnetMask $script:DhcpSubnetMask `
            -State Active
    }

    Set-DhcpServerv4OptionValue `
        -ScopeId $script:DhcpScopeId `
        -Router $script:DhcpRouter `
        -DnsServer $script:DcIp `
        -DnsDomain $script:DomainFqdn

    Set-DhcpServerv4DnsSetting `
        -DynamicUpdates Always `
        -DeleteDnsRRonLeaseExpiry $true `
        -UpdateDnsRRForOlderClients $true
}

function Ensure-OU {
    param(
        [string]$Name,
        [string]$Path,
        [bool]$ProtectedFromAccidentalDeletion = $true
    )

    $ouDn = "OU=$Name,$Path"
    if (-not (Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$ouDn)" -ErrorAction SilentlyContinue)) {
        New-ADOrganizationalUnit -Name $Name -Path $Path -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    }
}

function Get-DefaultOuDefinitions {
    $rootDn = "OU=$($script:EnterpriseRootOuName),$($script:DomainDn)"

    @(
        @{ Name = $script:EnterpriseRootOuName; ParentDn = $script:DomainDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier0'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier1'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier2'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Users'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Workstations'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Servers'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Groups'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'ServiceAccounts'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Admins'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Sites'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Staging'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Quarantine'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Corporate'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Regional'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Contractors'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'PrivilegedExcluded'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Disabled'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'AMER'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'EMEA'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'APAC'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Kiosks'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'VDI'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Quarantine'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Infrastructure'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Applications'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Regional'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'JumpHosts'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'RoleBased'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'ResourceBased'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'MailEnabled'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Delegation'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier0'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier1'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier2'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Applications'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier0'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier1'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Tier2'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'EmergencyAccess'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'AMER'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'EMEA'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'APAC'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'AdminGroups'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Delegation'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'AdminGroups'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Delegation'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'AdminGroups'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'Delegation'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
        @{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
    )
}

function Get-OuDefinitions {
    if (-not [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
        if (-not (Test-Path -LiteralPath $script:OuTemplateCsvPath)) {
            throw "OU template CSV '$($script:OuTemplateCsvPath)' was not found."
        }

        return @(Import-Csv -LiteralPath $script:OuTemplateCsvPath | Where-Object {
            $_.Enabled -match '^(?i:true|1|yes)$'
        })
    }

    return @(Get-DefaultOuDefinitions)
}

function Ensure-OuHierarchy {
    $definitions = @(Get-OuDefinitions)
    foreach ($definition in $definitions) {
        $protected = $true
        if ($definition.PSObject.Properties.Name -contains 'ProtectedFromAccidentalDeletion') {
            $protected = $definition.ProtectedFromAccidentalDeletion -match '^(?i:true|1|yes)$'
        }

        Ensure-OU -Name $definition.Name -Path $definition.ParentDn -ProtectedFromAccidentalDeletion $protected
    }
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name DhcpServer
Ensure-Module -Name DnsServer

$summary = [pscustomobject]@{
    DomainFqdn = $script:DomainFqdn
    DomainDn = $script:DomainDn
    DcFqdn = $script:DcFqdn
    DcIp = $script:DcIp
    UpnSuffix = $script:UpnSuffix
    DnsForwarders = ($script:DnsForwarders -join ', ')
    ServerReverseNetworkId = $script:ServerReverseNetworkId
    WorkstationReverseNetworkId = $script:WorkstationReverseNetworkId
    DhcpScopeId = $script:DhcpScopeId
    DhcpRouter = $script:DhcpRouter
    EnterpriseRootOuName = $script:EnterpriseRootOuName
    OuTemplateCsvPath = $(if ([string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) { 'Built-in default hierarchy' } else { $script:OuTemplateCsvPath })
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. Post-promotion baseline was not applied.'
    return
}

Ensure-UpnSuffix
Ensure-DnsForwarders
Ensure-DnsZone -NetworkId $script:ServerReverseNetworkId
Ensure-DnsZone -NetworkId $script:WorkstationReverseNetworkId
Ensure-DhcpAuthorization
Ensure-DhcpScope
Ensure-OuHierarchy

Write-Output 'Post-promotion baseline completed successfully.'

Configure-HQ-DC01-RecoveryTime.ps1

[CmdletBinding()]
param(
    [string]$DomainFqdn = 'corp.gntech.me',
    [string]$BackupDriveLetter = 'E',
    [int]$BackupDiskNumber = 1,
    [string[]]$NtpPeers = @('time.cloudflare.com,0x8','time.google.com,0x8'),
    [switch]$StartSystemStateBackup,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Get-RecoveryState {
    $feature = Get-ADOptionalFeature -Identity 'Recycle Bin Feature'
    $backupFeature = Get-WindowsFeature Windows-Server-Backup
    $backupVolume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
    $backupDisk = Get-Disk -Number $BackupDiskNumber -ErrorAction SilentlyContinue

    [pscustomobject]@{
        Domain = (Get-ADDomain).DNSRoot
        PdcEmulator = (Get-ADDomain).PDCEmulator
        RecycleBinEnabled = ($feature.EnabledScopes.Count -gt 0)
        BackupFeature = $backupFeature.InstallState
        BackupDiskNumber = if ($backupDisk) { $backupDisk.Number } else { $null }
        BackupDiskStyle = if ($backupDisk) { $backupDisk.PartitionStyle } else { 'Missing' }
        BackupDiskIsBoot = if ($backupDisk) { $backupDisk.IsBoot } else { $null }
        BackupDiskIsSystem = if ($backupDisk) { $backupDisk.IsSystem } else { $null }
        BackupVolume = if ($backupVolume) { "$($backupVolume.DriveLetter):" } else { 'Missing' }
        BackupVolumeLabel = if ($backupVolume) { $backupVolume.FileSystemLabel } else { '' }
        TimeSource = ((w32tm /query /source) | Out-String).Trim()
        W32TimeStatus = (Get-Service W32Time).Status
    }
}

function Assert-ExistingBackupTargetSafe {
    $volume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
    if (-not $volume) { return }

    $partition = Get-Partition -DriveLetter $BackupDriveLetter -ErrorAction Stop
    $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop
    if ($partition.DiskNumber -ne $BackupDiskNumber) {
        throw "Drive ${BackupDriveLetter}: is on Disk $($partition.DiskNumber), not requested Disk $BackupDiskNumber."
    }
    if ($disk.IsBoot -or $disk.IsSystem) {
        throw "Drive ${BackupDriveLetter}: is backed by a boot or system disk. Refusing to continue."
    }
    if ($volume.FileSystemLabel -ne 'DCBackup') {
        throw "Drive ${BackupDriveLetter}: has label '$($volume.FileSystemLabel)', not 'DCBackup'. Refusing to continue."
    }
}

Assert-Elevated
Import-Module ActiveDirectory
Import-Module ServerManager

$domain = Get-ADDomain
if ($domain.DNSRoot -ne $DomainFqdn) {
    throw "Connected domain '$($domain.DNSRoot)' does not match requested domain '$DomainFqdn'."
}
if ($domain.PDCEmulator -ne "$env:COMPUTERNAME.$DomainFqdn") {
    throw "Run the time configuration on the PDC Emulator '$($domain.PDCEmulator)'."
}
if ($BackupDriveLetter.Length -ne 1 -or $BackupDriveLetter -notmatch '^[A-Za-z]$') {
    throw 'BackupDriveLetter must be one alphabetic character.'
}
Assert-ExistingBackupTargetSafe

Write-Output '=== Current State ==='
Get-RecoveryState | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. No recovery or time configuration was changed.'
    return
}

$recycleBin = Get-ADOptionalFeature -Identity 'Recycle Bin Feature'
if ($recycleBin.EnabledScopes.Count -eq 0) {
    Enable-ADOptionalFeature -Identity 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target $DomainFqdn -Confirm:$false
}

if (-not (Get-WindowsFeature Windows-Server-Backup).Installed) {
    Install-WindowsFeature Windows-Server-Backup | Out-Null
}

$backupVolume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
if (-not $backupVolume) {
    $backupDisk = Get-Disk -Number $BackupDiskNumber -ErrorAction Stop
    if ($backupDisk.IsBoot -or $backupDisk.IsSystem) {
        throw "Disk $BackupDiskNumber is a boot or system disk. Refusing to modify it."
    }
    if ($backupDisk.PartitionStyle -ne 'RAW') {
        throw "Disk $BackupDiskNumber is '$($backupDisk.PartitionStyle)', not RAW. Refusing to modify it."
    }

    $partition = $backupDisk |
        Initialize-Disk -PartitionStyle GPT -PassThru |
        New-Partition -DriveLetter $BackupDriveLetter -UseMaximumSize
    Format-Volume -Partition $partition -FileSystem NTFS -NewFileSystemLabel 'DCBackup' -Confirm:$false | Out-Null
}

$peerList = $NtpPeers -join ' '
w32tm /config /manualpeerlist:"$peerList" /syncfromflags:manual /reliable:yes /update | Out-Null
Restart-Service W32Time -Force
Start-Sleep -Seconds 5
w32tm /resync /rediscover | Out-Null

if ($StartSystemStateBackup) {
    wbadmin start systemstatebackup -backupTarget:"${BackupDriveLetter}:" -quiet
    if ($LASTEXITCODE -ne 0) { throw "wbadmin failed with exit code $LASTEXITCODE." }
}

Write-Output '=== Result ==='
Get-RecoveryState | Format-List | Out-String | Write-Output
if ($StartSystemStateBackup) {
    Write-Output '=== Backup Versions ==='
    wbadmin get versions
}

Configure-HQ-ADSites.ps1

[CmdletBinding()]
param(
    [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9 _-]{0,63}$')]
    [string]$SiteName = 'HQ',
    [string]$Location = 'Headquarters',
    [string[]]$Subnets = @(
        '172.20.10.0/24',
        '172.20.20.0/24',
        '172.20.30.0/24',
        '172.20.40.0/24',
        '172.20.50.0/24',
        '172.20.60.0/24',
        '172.20.70.0/24',
        '172.20.80.0/24',
        '172.20.90.0/24',
        '172.20.100.0/24',
        '172.20.110.0/24'
    ),
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Get-SiteState {
    Write-Output '=== Sites ==='
    Get-ADReplicationSite -Filter * | Sort-Object Name | ForEach-Object {
        Write-Output "Site: $($_.Name) / DN=$($_.DistinguishedName)"
    }
    Write-Output '=== Subnets ==='
    Get-ADReplicationSubnet -Filter * | Sort-Object Name | ForEach-Object {
        Write-Output "Subnet: $($_.Name) / Site=$($_.Site) / Location=$($_.Location)"
    }
    Write-Output '=== Local Site Discovery ==='
    nltest /dsgetsite
}

Assert-Elevated
Import-Module ActiveDirectory

$domain = Get-ADDomain
$configDn = "CN=Configuration,$($domain.DistinguishedName)"
$defaultSiteDn = "CN=Default-First-Site-Name,CN=Sites,$configDn"

Get-SiteState
if ($ValidateOnly) {
    Write-Output 'Validation mode only. No site or subnet configuration was changed.'
    return
}

$site = Get-ADReplicationSite -Filter "Name -eq '$SiteName'" -ErrorAction SilentlyContinue
if (-not $site) {
    if (Get-ADObject -Identity $defaultSiteDn -ErrorAction SilentlyContinue) {
        Rename-ADObject -Identity $defaultSiteDn -NewName $SiteName
    }
    else {
        New-ADReplicationSite -Name $SiteName -Description 'Primary headquarters site' | Out-Null
    }
}

foreach ($subnetName in $Subnets) {
    $subnet = Get-ADReplicationSubnet -Filter "Name -eq '$subnetName'" -ErrorAction SilentlyContinue
    if ($subnet) {
        Set-ADReplicationSubnet -Identity $subnet.DistinguishedName -Site $SiteName -Location $Location
    }
    else {
        New-ADReplicationSubnet -Name $subnetName -Site $SiteName -Location $Location | Out-Null
    }
}

Write-Output '=== Result ==='
Get-SiteState

$serverSearchBase = "CN=Servers,CN=$SiteName,CN=Sites,$configDn"
$server = Get-ADObject -LDAPFilter "(cn=$env:COMPUTERNAME)" -SearchBase $serverSearchBase -ErrorAction SilentlyContinue
if (-not $server) { throw "The server object for '$env:COMPUTERNAME' was not found beneath site '$SiteName'." }

Configure-HQ-DC01-UserGPOBaseline.ps1

param(
    [string]$UsersOuDn,
    [string]$UserBaselineGpoName,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:UsersOuDn = if ([string]::IsNullOrWhiteSpace($UsersOuDn)) { Read-RequiredValue 'Users OU DN' 'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $UsersOuDn.Trim() }
    $script:UserBaselineGpoName = if ([string]::IsNullOrWhiteSpace($UserBaselineGpoName)) { Read-RequiredValue 'User baseline GPO name' 'GPO-Users-Baseline' } else { $UserBaselineGpoName.Trim() }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-Gpo {
    param(
        [string]$Name,
        [string]$Comment
    )

    $gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
    if (-not $gpo) {
        $gpo = New-GPO -Name $Name -Comment $Comment
    }
    return $gpo
}

function Ensure-GpoLink {
    param(
        [string]$Name,
        [string]$Target
    )

    $existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
    if ($existingLink.Count -eq 0) {
        New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
    }
}

function Set-UserRegistryValue {
    param(
        [string]$Key,
        [string]$ValueName,
        [string]$Type,
        [object]$Value
    )

    Set-GPRegistryValue `
        -Name $script:UserBaselineGpoName `
        -Key $Key `
        -ValueName $ValueName `
        -Type $Type `
        -Value $Value | Out-Null
}

Resolve-Inputs
Ensure-Module -Name GroupPolicy

$summary = [pscustomobject]@{
    UsersOuDn = $script:UsersOuDn
    UserBaselineGpoName = $script:UserBaselineGpoName
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. User baseline GPO was not applied.'
    return
}

Ensure-Gpo -Name $script:UserBaselineGpoName -Comment 'Baseline for standard user settings' | Out-Null
Ensure-GpoLink -Name $script:UserBaselineGpoName -Target $script:UsersOuDn

# Show file extensions in Explorer.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
    -ValueName 'HideFileExt' `
    -Type DWord `
    -Value 0

# Do not hide hidden files for standard users yet; keep baseline low-risk.

# Remove the Windows consumer "News and interests" style taskbar feed.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Feeds' `
    -ValueName 'ShellFeedsTaskbarViewMode' `
    -Type DWord `
    -Value 2

# Block Control Panel and Settings access as a visible, user-scoped baseline.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' `
    -ValueName 'NoControlPanel' `
    -Type DWord `
    -Value 1

Write-Output 'User baseline GPO completed successfully.'

Configure-HQ-WorkstationHardeningGPO.ps1

[CmdletBinding()]
param(
    [string]$WorkstationsOuDn = 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
    [string]$GpoName = 'GPO-Workstations-Hardening',
    [string]$TranscriptionPath = 'C:\ProgramData\PSLogs',
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Ensure-GpoLink {
    $links = @((Get-GPInheritance -Target $WorkstationsOuDn).GpoLinks | Where-Object DisplayName -eq $GpoName)
    if ($links.Count -eq 0) {
        New-GPLink -Name $GpoName -Target $WorkstationsOuDn -LinkEnabled Yes | Out-Null
        return 'Created'
    }
    if (-not $links[0].Enabled) {
        Set-GPLink -Name $GpoName -Target $WorkstationsOuDn -LinkEnabled Yes | Out-Null
        return 'Enabled'
    }
    return 'Compliant'
}

Assert-Elevated
Import-Module ActiveDirectory
Import-Module GroupPolicy
Get-ADOrganizationalUnit -Identity $WorkstationsOuDn -ErrorAction Stop | Out-Null

$settings = @(
    @{ Key='HKLM\Software\Policies\Microsoft\Windows NT\DNSClient'; Name='EnableMulticast'; Type='DWord'; Value=0 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'; Name='EnableModuleLogging'; Type='DWord'; Value=1 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='EnableTranscripting'; Type='DWord'; Value=1 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='EnableInvocationHeader'; Type='DWord'; Value=1 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='OutputDirectory'; Type='String'; Value=$TranscriptionPath },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\System'; Name='EnableSmartScreen'; Type='DWord'; Value=1 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows\System'; Name='ShellSmartScreenLevel'; Type='String'; Value='Warn' },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows Defender'; Name='DisableAntiSpyware'; Type='DWord'; Value=0 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection'; Name='DisableRealtimeMonitoring'; Type='DWord'; Value=0 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Spynet'; Name='SpynetReporting'; Type='DWord'; Value=2 },
    @{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Spynet'; Name='SubmitSamplesConsent'; Type='DWord'; Value=1 }
)

$gpo = Get-GPO -Name $GpoName -ErrorAction SilentlyContinue
if (-not $gpo -and $ValidateOnly) {
    Write-Output "GPO: Missing / Name=$GpoName"
    Write-Output 'Validation mode only. No GPO changes were applied.'
    return
}
if (-not $gpo) {
    $gpo = New-GPO -Name $GpoName -Comment 'Validated workstation hardening controls'
}

if ($ValidateOnly) {
    $link = @((Get-GPInheritance -Target $WorkstationsOuDn).GpoLinks | Where-Object DisplayName -eq $GpoName)
    Write-Output "GPO: Present / Id=$($gpo.Id)"
    Write-Output "Link: $(if ($link.Count -gt 0 -and $link[0].Enabled) { 'Enabled' } else { 'MissingOrDisabled' })"
    foreach ($setting in $settings) {
        $current = Get-GPRegistryValue -Name $GpoName -Key $setting.Key -ValueName $setting.Name -ErrorAction SilentlyContinue
        [pscustomobject]@{
            Key = $setting.Key
            ValueName = $setting.Name
            Expected = $setting.Value
            Current = if ($current) { $current.Value } else { $null }
            Compliant = [bool]($current -and $current.Value -eq $setting.Value)
        }
    }
    Write-Output 'Validation mode only. No GPO changes were applied.'
    return
}

$linkAction = Ensure-GpoLink
foreach ($setting in $settings) {
    Set-GPRegistryValue -Name $GpoName -Key $setting.Key -ValueName $setting.Name -Type $setting.Type -Value $setting.Value | Out-Null
}

Write-Output "GPO: $GpoName"
Write-Output "Link: $linkAction"
Write-Output "SettingsApplied: $($settings.Count)"
Write-Output 'Workstation hardening GPO completed successfully.'

Configure-HQ-FS01-Shares.ps1

param(
    [string]$ShareRoot,
    [string]$DomainNetbios,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Read-RequiredValue {
    param([string]$Prompt,[string]$DefaultValue)
    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:ShareRoot = if ([string]::IsNullOrWhiteSpace($ShareRoot)) { Read-RequiredValue 'Share root path' 'C:\Shares' } else { $ShareRoot.Trim() }
    $script:DomainNetbios = if ([string]::IsNullOrWhiteSpace($DomainNetbios)) { Read-RequiredValue 'Domain NetBIOS name' 'GNTECH' } else { $DomainNetbios.Trim() }
}

function Ensure-Directory {
    param([string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        if (-not $ValidateOnly) { New-Item -Path $Path -ItemType Directory -Force | Out-Null }
        if ($ValidateOnly) { return 'Planned' }
        return 'Created'
    }
    'Exists'
}

function Set-FolderAcl {
    param([string]$Path,[array]$Rules)
    $acl = New-Object System.Security.AccessControl.DirectorySecurity
    $acl.SetAccessRuleProtection($true, $false)
    $inheritanceFlags = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
    $propagationFlags = [System.Security.AccessControl.PropagationFlags]::None
    foreach ($rule in $Rules) {
        $fsRights = [System.Security.AccessControl.FileSystemRights]$rule.Rights
        $accessType = [System.Security.AccessControl.AccessControlType]::Allow
        $aclRule = New-Object System.Security.AccessControl.FileSystemAccessRule($rule.Identity,$fsRights,$inheritanceFlags,$propagationFlags,$accessType)
        $acl.AddAccessRule($aclRule) | Out-Null
    }
    if (-not $ValidateOnly) { Set-Acl -Path $Path -AclObject $acl }
    if ($ValidateOnly) { 'Planned' } else { 'Applied' }
}

function Ensure-SmbShareState {
    param([string]$Name,[string]$Path,[string]$Description,[string[]]$FullAccess,[string[]]$ChangeAccess,[string[]]$ReadAccess,[string]$FolderEnumerationMode = 'AccessBased')
    $existing = Get-SmbShare -Name $Name -ErrorAction SilentlyContinue
    if ($existing -and $existing.Path -ne $Path) {
        throw "SMB share '$Name' exists at '$($existing.Path)' instead of '$Path'. Correct it manually before rerunning."
    }
    if (-not $ValidateOnly) {
        if (-not $existing) {
            $splat = @{ Name = $Name; Path = $Path; Description = $Description; FolderEnumerationMode = $FolderEnumerationMode }
            if ($FullAccess) { $splat.FullAccess = $FullAccess }
            if ($ChangeAccess) { $splat.ChangeAccess = $ChangeAccess }
            if ($ReadAccess) { $splat.ReadAccess = $ReadAccess }
            New-SmbShare @splat | Out-Null
        }
        else {
            Set-SmbShare -Name $Name -Description $Description -FolderEnumerationMode $FolderEnumerationMode -Force | Out-Null
            $desiredAccess = @()
            $desiredAccess += @($FullAccess | ForEach-Object { @{ Account = $_; Right = 'Full' } })
            $desiredAccess += @($ChangeAccess | ForEach-Object { @{ Account = $_; Right = 'Change' } })
            $desiredAccess += @($ReadAccess | ForEach-Object { @{ Account = $_; Right = 'Read' } })
            foreach ($entry in $desiredAccess) {
                Grant-SmbShareAccess -Name $Name -AccountName $entry.Account -AccessRight $entry.Right -Force | Out-Null
            }
        }
    }
    if ($ValidateOnly) { if ($existing) { 'Existing path validated' } else { 'Planned' } }
    elseif ($existing) { 'Reconciled' } else { 'Created' }
}

function Assert-AccountResolvable {
    param([string[]]$Identity)
    foreach ($item in $Identity) {
        try { ([System.Security.Principal.NTAccount]$item).Translate([System.Security.Principal.SecurityIdentifier]) | Out-Null }
        catch { throw "Required account or group '$item' cannot be resolved on this server." }
    }
}

Resolve-Inputs
Assert-Elevated

$departmentsRoot = Join-Path $script:ShareRoot 'Departments'
$publicRoot = Join-Path $script:ShareRoot 'Public'
$backupsRoot = Join-Path $script:ShareRoot 'Backups'
$itRoot = Join-Path $departmentsRoot 'IT'
$financeRoot = Join-Path $departmentsRoot 'Finance'

$domainUsers = "$($script:DomainNetbios)\Domain Users"
$domainAdmins = "$($script:DomainNetbios)\Domain Admins"
$builtinAdmins = 'BUILTIN\Administrators'
$systemAccount = 'NT AUTHORITY\SYSTEM'

Assert-AccountResolvable -Identity @(
    $domainUsers,$domainAdmins,$builtinAdmins,$systemAccount,
    "$($script:DomainNetbios)\DL-HQ-FS01-Departments-IT-RW",
    "$($script:DomainNetbios)\DL-HQ-FS01-Departments-Finance-RW",
    "$($script:DomainNetbios)\DL-HQ-FS01-Public-RW",
    "$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"
)

$feature = Get-WindowsFeature FS-FileServer
if (-not $feature.Installed -and -not $ValidateOnly) { Install-WindowsFeature FS-FileServer | Out-Null }
Write-Output "FS-FileServer: $(if ($feature.Installed) { 'Installed' } elseif ($ValidateOnly) { 'Missing' } else { 'Installed now' })"

$folders = @($script:ShareRoot,$departmentsRoot,$publicRoot,$backupsRoot,$itRoot,$financeRoot)
$folderResults = foreach ($folder in $folders) {
    [pscustomobject]@{ Path = $folder; Action = (Ensure-Directory -Path $folder) }
}
$folderResults | Format-Table -AutoSize | Out-String | Write-Output

Set-FolderAcl -Path $departmentsRoot -Rules @(
    @{ Identity = $systemAccount; Rights = 'FullControl' },
    @{ Identity = $builtinAdmins; Rights = 'FullControl' },
    @{ Identity = $domainAdmins; Rights = 'FullControl' },
    @{ Identity = $domainUsers; Rights = 'ReadAndExecute, Synchronize' }
)
Set-FolderAcl -Path $itRoot -Rules @(
    @{ Identity = $systemAccount; Rights = 'FullControl' },
    @{ Identity = $builtinAdmins; Rights = 'FullControl' },
    @{ Identity = $domainAdmins; Rights = 'FullControl' },
    @{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Departments-IT-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $financeRoot -Rules @(
    @{ Identity = $systemAccount; Rights = 'FullControl' },
    @{ Identity = $builtinAdmins; Rights = 'FullControl' },
    @{ Identity = $domainAdmins; Rights = 'FullControl' },
    @{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Departments-Finance-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $publicRoot -Rules @(
    @{ Identity = $systemAccount; Rights = 'FullControl' },
    @{ Identity = $builtinAdmins; Rights = 'FullControl' },
    @{ Identity = $domainAdmins; Rights = 'FullControl' },
    @{ Identity = $domainUsers; Rights = 'ReadAndExecute, Synchronize' },
    @{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Public-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $backupsRoot -Rules @(
    @{ Identity = $systemAccount; Rights = 'FullControl' },
    @{ Identity = $builtinAdmins; Rights = 'FullControl' },
    @{ Identity = $domainAdmins; Rights = 'FullControl' },
    @{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"; Rights = 'Modify, Synchronize' }
)

$shareResults = @(
    [pscustomobject]@{
        Name = 'Departments'
        Action = (Ensure-SmbShareState -Name 'Departments' -Path $departmentsRoot -Description 'Departmental data root' -FullAccess @($domainAdmins) -ReadAccess @($domainUsers))
    },
    [pscustomobject]@{
        Name = 'Public'
        Action = (Ensure-SmbShareState -Name 'Public' -Path $publicRoot -Description 'Broad collaboration share' -FullAccess @($domainAdmins) -ChangeAccess @("$($script:DomainNetbios)\DL-HQ-FS01-Public-RW") -ReadAccess @($domainUsers))
    },
    [pscustomobject]@{
        Name = 'Backups'
        Action = (Ensure-SmbShareState -Name 'Backups' -Path $backupsRoot -Description 'Backup staging share' -FullAccess @($domainAdmins) -ChangeAccess @("$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"))
    }
)
$shareResults | Format-Table -AutoSize | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. HQ-FS01 share configuration was not applied.'
} else {
    Write-Output 'HQ-FS01 share configuration completed successfully.'
}

Create-HQ-FS01-Groups.ps1

param(
    [string]$GroupsOuDn,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:GroupsOuDn = if ([string]::IsNullOrWhiteSpace($GroupsOuDn)) {
        Read-RequiredValue 'Groups OU DN' 'OU=Groups,OU=GNTECH,DC=corp,DC=gntech,DC=me'
    } else {
        $GroupsOuDn.Trim()
    }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-Group {
    param(
        [string]$Name,
        [string]$Description
    )

    $existing = Get-ADGroup -LDAPFilter "(cn=$Name)" -SearchBase $script:GroupsOuDn -ErrorAction SilentlyContinue
    if ($existing) {
        return [pscustomobject]@{ Name = $Name; Action = 'Exists' }
    }

    if (-not $ValidateOnly) {
        New-ADGroup `
            -Name $Name `
            -SamAccountName $Name `
            -GroupScope DomainLocal `
            -GroupCategory Security `
            -Path $script:GroupsOuDn `
            -Description $Description | Out-Null
    }

    [pscustomobject]@{ Name = $Name; Action = $(if ($ValidateOnly) { 'Planned' } else { 'Created' }) }
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory

$definitions = @(
    @{ Name = 'DL-HQ-FS01-Public-RW'; Description = 'Modify access to HQ-FS01 Public share' },
    @{ Name = 'DL-HQ-FS01-Departments-IT-RW'; Description = 'Modify access to HQ-FS01 Departments IT folder' },
    @{ Name = 'DL-HQ-FS01-Departments-Finance-RW'; Description = 'Modify access to HQ-FS01 Departments Finance folder' },
    @{ Name = 'DL-HQ-FS01-Backups-RW'; Description = 'Modify access to HQ-FS01 Backups share' }
)

$summary = $definitions | ForEach-Object {
    [pscustomobject]@{
        Name = $_.Name
        Description = $_.Description
        Path = $script:GroupsOuDn
    }
}
$summary | Format-Table -AutoSize | Out-String | Write-Output

$results = foreach ($definition in $definitions) {
    Ensure-Group -Name $definition.Name -Description $definition.Description
}

$results | Format-Table -AutoSize | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. HQ-FS01 groups were not created.'
} else {
    Write-Output 'HQ-FS01 groups completed successfully.'
}

Create-HQ-ServiceAccounts.ps1

param(
    [string]$DomainDn,
    [string]$ServiceAccountsOuDn,
    [string]$ServiceUpnSuffix,
    [hashtable]$AccountPasswords,
    [switch]$PasswordNeverExpires,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-RequiredSecret {
    param([string]$Prompt)

    try {
        return Read-Host -Prompt $Prompt -AsSecureString
    }
    catch {
        throw "A secure value for '$Prompt' is required."
    }
}

function Resolve-Inputs {
    $script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) {
        Read-RequiredValue -Prompt 'Domain DN' -DefaultValue 'DC=corp,DC=gntech,DC=me'
    }
    else {
        $DomainDn.Trim()
    }

    $script:ServiceAccountsOuDn = if ([string]::IsNullOrWhiteSpace($ServiceAccountsOuDn)) {
        Read-RequiredValue -Prompt 'Service accounts OU DN' -DefaultValue 'OU=Tier1,OU=ServiceAccounts,OU=GNTECH,DC=corp,DC=gntech,DC=me'
    }
    else {
        $ServiceAccountsOuDn.Trim()
    }

    $script:ServiceUpnSuffix = if ([string]::IsNullOrWhiteSpace($ServiceUpnSuffix)) {
        ''
    }
    else {
        $ServiceUpnSuffix.Trim()
    }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Assert-AdTargets {
    $domain = Get-ADDomain -ErrorAction Stop
    if ($domain.DistinguishedName -ne $script:DomainDn) {
        throw "Connected domain '$($domain.DistinguishedName)' does not match requested domain '$($script:DomainDn)'."
    }
    Get-ADOrganizationalUnit -Identity $script:ServiceAccountsOuDn -ErrorAction Stop | Out-Null
}

function Get-ServiceAccountUpn {
    param([string]$SamAccountName)

    if ([string]::IsNullOrWhiteSpace($script:ServiceUpnSuffix)) {
        return $null
    }

    return "$SamAccountName@$($script:ServiceUpnSuffix)"
}

function Ensure-ServiceAccount {
    param(
        [hashtable]$Definition,
        [securestring]$Password
    )

    $existing = Get-ADUser -Filter "SamAccountName -eq '$($Definition.SamAccountName)'" -Properties UserPrincipalName,PasswordNeverExpires,Description -ErrorAction SilentlyContinue
    if ($existing) {
        if ($existing.DistinguishedName -notlike "*,$($script:ServiceAccountsOuDn)") {
            throw "Account '$($Definition.SamAccountName)' exists outside the target OU: $($existing.DistinguishedName)"
        }

        $changes = @{}
        $expectedUpn = Get-ServiceAccountUpn -SamAccountName $Definition.SamAccountName
        if ($expectedUpn -and $existing.UserPrincipalName -ne $expectedUpn) { $changes.UserPrincipalName = $expectedUpn }
        if ($existing.Description -ne $Definition.Description) { $changes.Description = $Definition.Description }
        if ($PasswordNeverExpires -and -not $existing.PasswordNeverExpires) { $changes.PasswordNeverExpires = $true }
        if ($changes.Count -gt 0 -and -not $ValidateOnly) { Set-ADUser -Identity $existing @changes }

        return [pscustomobject]@{
            SamAccountName = $Definition.SamAccountName
            Action = if ($changes.Count -eq 0) { 'Compliant' } elseif ($ValidateOnly) { 'Drift' } else { 'Updated' }
            Details = if ($changes.Count -eq 0) { '' } else { ($changes.Keys -join ',') }
        }
    }

    if ($ValidateOnly) {
        return [pscustomobject]@{
            SamAccountName = $Definition.SamAccountName
            Action = 'Missing'
            Details = ''
        }
    }

    $parameters = @{
        Name                  = $Definition.Name
        DisplayName           = $Definition.DisplayName
        SamAccountName        = $Definition.SamAccountName
        Path                  = $script:ServiceAccountsOuDn
        AccountPassword       = $Password
        Enabled               = $true
        ChangePasswordAtLogon = $false
        Description           = $Definition.Description
    }

    $upn = Get-ServiceAccountUpn -SamAccountName $Definition.SamAccountName
    if ($upn) {
        $parameters['UserPrincipalName'] = $upn
    }

    if ($PasswordNeverExpires) {
        $parameters['PasswordNeverExpires'] = $true
    }

    New-ADUser @parameters

    return [pscustomobject]@{
        SamAccountName = $Definition.SamAccountName
        Action = 'Created'
        Details = ''
    }
}

Resolve-Inputs
Assert-Elevated
Ensure-Module -Name ActiveDirectory
Assert-AdTargets

$definitions = @(
    @{
        Name = 'svc.join.hq'
        DisplayName = 'svc.join.hq'
        SamAccountName = 'svc.join.hq'
        Description = 'Delegated domain join service account for workstation and server onboarding.'
    },
    @{
        Name = 'svc.backup.hq'
        DisplayName = 'svc.backup.hq'
        SamAccountName = 'svc.backup.hq'
        Description = 'Backup service account for approved backup tooling and targets.'
    },
    @{
        Name = 'svc.monitor.hq'
        DisplayName = 'svc.monitor.hq'
        SamAccountName = 'svc.monitor.hq'
        Description = 'Monitoring and inventory service account for approved observability tooling.'
    },
    @{
        Name = 'svc.deploy.hq'
        DisplayName = 'svc.deploy.hq'
        SamAccountName = 'svc.deploy.hq'
        Description = 'Deployment automation service account for scripted server and workstation configuration.'
    }
)

$summary = $definitions | ForEach-Object {
    [pscustomobject]@{
        Name = $_.Name
        SamAccountName = $_.SamAccountName
        UserPrincipalName = Get-ServiceAccountUpn -SamAccountName $_.SamAccountName
        Path = $script:ServiceAccountsOuDn
        Description = $_.Description
    }
}

$summary | Format-Table -AutoSize | Out-String | Write-Output

if ($ValidateOnly) {
    $results = foreach ($definition in $definitions) {
        Ensure-ServiceAccount -Definition $definition -Password $null
    }
    $results | Format-Table -AutoSize | Out-String | Write-Output
    Write-Output 'Validation mode only. Service accounts were not created or changed.'
    return
}

$passwords = @{}
foreach ($definition in $definitions) {
    $existing = Get-ADUser -Filter "SamAccountName -eq '$($definition.SamAccountName)'" -ErrorAction SilentlyContinue
    if (-not $existing) {
        if ($AccountPasswords -and $AccountPasswords.ContainsKey($definition.SamAccountName)) {
            $providedPassword = $AccountPasswords[$definition.SamAccountName]
            if ($providedPassword -isnot [securestring]) {
                throw "AccountPasswords entry for '$($definition.SamAccountName)' must be a SecureString."
            }
            $passwords[$definition.SamAccountName] = $providedPassword
        }
        else {
            $passwords[$definition.SamAccountName] = Read-RequiredSecret -Prompt "Password for $($definition.SamAccountName)"
        }
    }
}

$results = foreach ($definition in $definitions) {
    Ensure-ServiceAccount -Definition $definition -Password $passwords[$definition.SamAccountName]
}

$results | Format-Table -AutoSize | Out-String | Write-Output
Write-Output 'Service account creation completed successfully.'

Create-HQ-TestUsers.ps1

param(
    [string]$DomainDn,
    [string]$UsersOuDn,
    [string]$AdminsOuDn,
    [string]$UserUpnSuffix,
    [string]$AdminUpnSuffix,
    [string]$PublicWriteGroupSamAccountName,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Read-RequiredSecret {
    param([string]$Prompt)

    try {
        return Read-Host -Prompt $Prompt -AsSecureString
    }
    catch {
        throw "A secure value for '$Prompt' is required when running non-interactively."
    }
}

function Resolve-Inputs {
    $script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) { Read-RequiredValue 'Domain DN' 'DC=corp,DC=gntech,DC=me' } else { $DomainDn.Trim() }
    $script:UsersOuDn = if ([string]::IsNullOrWhiteSpace($UsersOuDn)) { Read-RequiredValue 'Users OU DN' 'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $UsersOuDn.Trim() }
    $script:AdminsOuDn = if ([string]::IsNullOrWhiteSpace($AdminsOuDn)) { Read-RequiredValue 'Admins OU DN' 'OU=Admins,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $AdminsOuDn.Trim() }
    $script:UserUpnSuffix = if ([string]::IsNullOrWhiteSpace($UserUpnSuffix)) { Read-RequiredValue 'Standard user UPN suffix' 'gntech.me' } else { $UserUpnSuffix.Trim() }
    $script:AdminUpnSuffix = if ([string]::IsNullOrWhiteSpace($AdminUpnSuffix)) { Read-RequiredValue 'Admin user UPN suffix' 'gntech.me' } else { $AdminUpnSuffix.Trim() }
    $script:PublicWriteGroupSamAccountName = if ([string]::IsNullOrWhiteSpace($PublicWriteGroupSamAccountName)) { Read-RequiredValue 'Public write group SamAccountName' 'DL-HQ-FS01-Public-RW' } else { $PublicWriteGroupSamAccountName.Trim() }

    if (-not $ValidateOnly) {
        $script:DefaultUserPassword = Read-RequiredSecret -Prompt 'Standard user password'
        $script:DefaultAdminPassword = Read-RequiredSecret -Prompt 'Admin user password'
    }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-TestUser {
    param(
        [string]$Name,
        [string]$GivenName,
        [string]$Surname,
        [string]$DisplayName,
        [string]$UserPrincipalName,
        [string]$SamAccountName,
        [string]$EmployeeId,
        [string]$Path,
        [securestring]$Password
    )

    $existing = Get-ADUser -Filter "SamAccountName -eq '$SamAccountName'" -ErrorAction SilentlyContinue
    if (-not $existing) {
        New-ADUser `
            -Name $Name `
            -GivenName $GivenName `
            -Surname $Surname `
            -DisplayName $DisplayName `
            -UserPrincipalName $UserPrincipalName `
            -SamAccountName $SamAccountName `
            -EmployeeID $EmployeeId `
            -Path $Path `
            -AccountPassword $Password `
            -Enabled $true `
            -ChangePasswordAtLogon $false
    }
}

function Ensure-GroupMembership {
    param(
        [string]$GroupSamAccountName,
        [string]$MemberSamAccountName
    )

    $group = Get-ADGroup -Filter "SamAccountName -eq '$GroupSamAccountName'" -ErrorAction SilentlyContinue
    if (-not $group) {
        throw "The group '$GroupSamAccountName' was not found."
    }

    $member = Get-ADUser -Filter "SamAccountName -eq '$MemberSamAccountName'" -ErrorAction SilentlyContinue
    if (-not $member) {
        throw "The user '$MemberSamAccountName' was not found."
    }

    $isMember = Get-ADGroupMember -Identity $group.DistinguishedName -Recursive | Where-Object SamAccountName -eq $MemberSamAccountName
    if ($isMember) {
        return [pscustomobject]@{
            GroupSamAccountName = $GroupSamAccountName
            MemberSamAccountName = $MemberSamAccountName
            Action = 'Exists'
        }
    }

    if (-not $ValidateOnly) {
        Add-ADGroupMember -Identity $group.DistinguishedName -Members $member.DistinguishedName
    }

    [pscustomobject]@{
        GroupSamAccountName = $GroupSamAccountName
        MemberSamAccountName = $MemberSamAccountName
        Action = $(if ($ValidateOnly) { 'Planned' } else { 'Added' })
    }
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory

$definitions = @(
    @{
        Name = 'Miguel Perez'
        GivenName = 'Miguel'
        Surname = 'Perez'
        DisplayName = 'Miguel Perez'
        UserPrincipalName = "miguel.perez@$($script:UserUpnSuffix)"
        SamAccountName = 'miguel.perez'
        EmployeeId = 'GNT-000123'
        Path = $script:UsersOuDn
        IsAdmin = $false
    },
    @{
        Name = 'Ana Garcia'
        GivenName = 'Ana'
        Surname = 'Garcia'
        DisplayName = 'Ana Garcia'
        UserPrincipalName = "ana.garcia@$($script:UserUpnSuffix)"
        SamAccountName = 'ana.garcia'
        EmployeeId = 'GNT-000124'
        Path = $script:UsersOuDn
        IsAdmin = $false
    },
    @{
        Name = 'Admin Miguel Perez'
        GivenName = 'Miguel'
        Surname = 'Perez'
        DisplayName = 'Admin Miguel Perez'
        UserPrincipalName = "adm.miguel.perez@$($script:AdminUpnSuffix)"
        SamAccountName = 'adm.miguel.perez'
        EmployeeId = 'GNT-A000123'
        Path = $script:AdminsOuDn
        IsAdmin = $true
    },
    @{
        Name = 'Admin Gerlin Nolasco'
        GivenName = 'Gerlin'
        Surname = 'Nolasco'
        DisplayName = 'Admin Gerlin Nolasco'
        UserPrincipalName = "adm.gerlin.nolasco@$($script:AdminUpnSuffix)"
        SamAccountName = 'adm.gerlin.nolasco'
        EmployeeId = 'GNT-A000124'
        Path = $script:AdminsOuDn
        IsAdmin = $true
    }
)

$summary = $definitions | ForEach-Object {
    [pscustomobject]@{
        Name = $_.Name
        UserPrincipalName = $_.UserPrincipalName
        SamAccountName = $_.SamAccountName
        EmployeeId = $_.EmployeeId
        Path = $_.Path
    }
}
$summary | Format-Table -AutoSize | Out-String | Write-Output

[pscustomobject]@{
    PublicWriteGroupSamAccountName = $script:PublicWriteGroupSamAccountName
    PublicWriteMemberSamAccountName = 'miguel.perez'
    PublicDeniedMemberSamAccountName = 'ana.garcia'
} | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. Test users and group memberships were not created.'
    return
}

foreach ($definition in $definitions) {
    $password = if ($definition.IsAdmin) { $script:DefaultAdminPassword } else { $script:DefaultUserPassword }
    Ensure-TestUser `
        -Name $definition.Name `
        -GivenName $definition.GivenName `
        -Surname $definition.Surname `
        -DisplayName $definition.DisplayName `
        -UserPrincipalName $definition.UserPrincipalName `
        -SamAccountName $definition.SamAccountName `
        -EmployeeId $definition.EmployeeId `
        -Path $definition.Path `
        -Password $password
}

$membershipResult = Ensure-GroupMembership `
    -GroupSamAccountName $script:PublicWriteGroupSamAccountName `
    -MemberSamAccountName 'miguel.perez'

$membershipResult | Format-Table -AutoSize | Out-String | Write-Output

Write-Output 'Test users completed successfully.'

Deploy-HQ-DC01-Stage1.ps1

param(
    [string]$TargetName,
    [string]$TimeZone,
    [string]$IPv4,
    [Nullable[int]]$PrefixLength,
    [string]$Gateway,
    [string[]]$DnsServers,
    [switch]$RestartIfNeeded
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-RequiredInt {
    param(
        [string]$Prompt,
        [int]$DefaultValue
    )

    while ($true) {
        $value = Read-RequiredValue -Prompt $Prompt -DefaultValue $DefaultValue
        $parsed = 0
        if ([int]::TryParse($value, [ref]$parsed)) {
            return $parsed
        }
    }
}

function Read-DnsServerList {
    param(
        [string[]]$DefaultServers
    )

    while ($true) {
        $defaultValue = $DefaultServers -join ','
        $value = Read-RequiredValue -Prompt 'DNS servers (comma-separated)' -DefaultValue $defaultValue
        $servers = @(
            $value.Split(',') |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )

        if ($servers.Count -gt 0) {
            return $servers
        }
    }
}

function Resolve-DeploymentInputs {
    $script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) {
        Read-RequiredValue -Prompt 'Target hostname' -DefaultValue 'HQ-DC01'
    } else {
        $TargetName.Trim()
    }

    $script:TimeZone = if ([string]::IsNullOrWhiteSpace($TimeZone)) {
        Read-RequiredValue -Prompt 'Windows time zone ID' -DefaultValue 'SA Western Standard Time'
    } else {
        $TimeZone.Trim()
    }

    $script:IPv4 = if ([string]::IsNullOrWhiteSpace($IPv4)) {
        Read-RequiredValue -Prompt 'IPv4 address' -DefaultValue '172.20.20.11'
    } else {
        $IPv4.Trim()
    }

    $script:PrefixLength = if ($null -eq $PrefixLength) {
        Read-RequiredInt -Prompt 'IPv4 prefix length' -DefaultValue 24
    } else {
        [int]$PrefixLength
    }

    $script:Gateway = if ([string]::IsNullOrWhiteSpace($Gateway)) {
        Read-RequiredValue -Prompt 'Default gateway' -DefaultValue '172.20.20.1'
    } else {
        $Gateway.Trim()
    }

    $script:DnsServers = if ($DnsServers.Count -eq 0) {
        Read-DnsServerList -DefaultServers @('127.0.0.1', '1.1.1.1')
    } else {
        @(
            $DnsServers |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )
    }
}

function Get-PrimaryAdapter {
    $adapter = Get-NetAdapter |
        Where-Object Status -eq 'Up' |
        Sort-Object ifIndex |
        Select-Object -First 1

    if (-not $adapter) {
        throw 'No active network adapter was found.'
    }

    return $adapter
}

function Set-ExactDnsServers {
    param(
        [int]$InterfaceIndex,
        [string[]]$DesiredServers
    )

    $current = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
    if (@($current) -join ',' -ne @($DesiredServers) -join ',') {
        Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $DesiredServers
    }
}

function Set-ExactIPv4Configuration {
    param(
        [int]$InterfaceIndex,
        [string]$DesiredIPv4,
        [int]$DesiredPrefixLength,
        [string]$DesiredGateway
    )

    $existing = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue

    foreach ($address in $existing) {
        if ($address.IPAddress -eq $DesiredIPv4 -and $address.PrefixLength -eq $DesiredPrefixLength) {
            continue
        }

        if ($address.IPAddress -like '169.254.*') {
            continue
        }

        Remove-NetIPAddress `
            -InterfaceIndex $InterfaceIndex `
            -AddressFamily IPv4 `
            -IPAddress $address.IPAddress `
            -Confirm:$false `
            -ErrorAction SilentlyContinue
    }

    $desiredAddress = Get-NetIPAddress `
        -InterfaceIndex $InterfaceIndex `
        -AddressFamily IPv4 `
        -ErrorAction SilentlyContinue |
        Where-Object {
            $_.IPAddress -eq $DesiredIPv4 -and
            $_.PrefixLength -eq $DesiredPrefixLength
        } |
        Select-Object -First 1

    if (-not $desiredAddress) {
        New-NetIPAddress `
            -InterfaceIndex $InterfaceIndex `
            -IPAddress $DesiredIPv4 `
            -PrefixLength $DesiredPrefixLength
    }

    $defaultRoutes = Get-NetRoute `
        -InterfaceIndex $InterfaceIndex `
        -DestinationPrefix '0.0.0.0/0' `
        -AddressFamily IPv4 `
        -ErrorAction SilentlyContinue

    foreach ($route in $defaultRoutes) {
        if ($route.NextHop -ne $DesiredGateway) {
            Remove-NetRoute `
                -InterfaceIndex $InterfaceIndex `
                -DestinationPrefix '0.0.0.0/0' `
                -NextHop $route.NextHop `
                -Confirm:$false `
                -ErrorAction SilentlyContinue
        }
    }

    $desiredRoute = Get-NetRoute `
        -InterfaceIndex $InterfaceIndex `
        -DestinationPrefix '0.0.0.0/0' `
        -AddressFamily IPv4 `
        -ErrorAction SilentlyContinue |
        Where-Object NextHop -eq $DesiredGateway |
        Select-Object -First 1

    if (-not $desiredRoute) {
        New-NetRoute `
            -InterfaceIndex $InterfaceIndex `
            -DestinationPrefix '0.0.0.0/0' `
            -NextHop $DesiredGateway `
            -RouteMetric 256 | Out-Null
    }
}

function Install-RequiredFeatures {
    $requiredFeatures = 'AD-Domain-Services', 'DNS', 'DHCP'
    $missingFeatures = Get-WindowsFeature $requiredFeatures |
        Where-Object InstallState -ne 'Installed' |
        Select-Object -ExpandProperty Name

    if ($missingFeatures) {
        Install-WindowsFeature -Name $missingFeatures -IncludeManagementTools | Out-Null
    }
}

Resolve-DeploymentInputs

$adapter = Get-PrimaryAdapter
$restartRequired = $false

if ((Get-TimeZone).Id -ne $TimeZone) {
    Set-TimeZone -Id $TimeZone
}

Set-ExactIPv4Configuration `
    -InterfaceIndex $adapter.ifIndex `
    -DesiredIPv4 $IPv4 `
    -DesiredPrefixLength $PrefixLength `
    -DesiredGateway $Gateway

Set-ExactDnsServers `
    -InterfaceIndex $adapter.ifIndex `
    -DesiredServers $DnsServers

if ($env:COMPUTERNAME -ne $TargetName) {
    Rename-Computer -NewName $TargetName -Force
    $restartRequired = $true
}

Install-RequiredFeatures

$summary = [pscustomobject]@{
    Hostname = $env:COMPUTERNAME
    TargetName = $TargetName
    Adapter = $adapter.Name
    IPv4 = $IPv4
    PrefixLength = $PrefixLength
    Gateway = $Gateway
    DnsServers = ($DnsServers -join ', ')
    RestartRequired = $restartRequired
}

$summary | Format-List | Out-String | Write-Output

if ($restartRequired -and $RestartIfNeeded) {
    Write-Output 'Restarting to complete hostname change.'
    Restart-Computer -Force
}

Deploy-HQ-FS01-Stage1.ps1

param(
    [string]$TargetName,
    [string]$IPv4,
    [Nullable[int]]$PrefixLength,
    [string]$Gateway,
    [string[]]$DnsServers,
    [string]$DomainName,
    [string]$OuPath,
    [string]$JoinCredentialUser,
    [switch]$RestartIfNeeded
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)

    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Read-RequiredValue {
    param([string]$Prompt,[string]$DefaultValue)
    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Read-RequiredInt {
    param([string]$Prompt,[int]$DefaultValue)
    while ($true) {
        $value = Read-RequiredValue -Prompt $Prompt -DefaultValue $DefaultValue
        $parsed = 0
        if ([int]::TryParse($value, [ref]$parsed)) { return $parsed }
    }
}

function Read-DnsServerList {
    param([string[]]$DefaultServers)
    while ($true) {
        $value = Read-RequiredValue -Prompt 'DNS servers (comma-separated)' -DefaultValue ($DefaultServers -join ',')
        $servers = @($value.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        if ($servers.Count -gt 0) { return $servers }
    }
}

function Resolve-Inputs {
    $script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) { Read-RequiredValue 'Target hostname' 'HQ-FS01' } else { $TargetName.Trim() }
    $script:IPv4 = if ([string]::IsNullOrWhiteSpace($IPv4)) { Read-RequiredValue 'IPv4 address' '172.20.20.21' } else { $IPv4.Trim() }
    $script:PrefixLength = if ($null -eq $PrefixLength) { Read-RequiredInt 'IPv4 prefix length' 24 } else { [int]$PrefixLength }
    $script:Gateway = if ([string]::IsNullOrWhiteSpace($Gateway)) { Read-RequiredValue 'Default gateway' '172.20.20.1' } else { $Gateway.Trim() }
    $script:DnsServers = if ($DnsServers.Count -eq 0) { Read-DnsServerList -DefaultServers @('172.20.20.11') } else { @($DnsServers | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
    $script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainName.Trim() }
    $script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) { Read-RequiredValue 'Target server OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $OuPath.Trim() }
    $script:JoinCredentialUser = if ([string]::IsNullOrWhiteSpace($JoinCredentialUser)) { Read-RequiredValue 'Domain join user' 'corp.gntech.me\\svc.join.hq' } else { $JoinCredentialUser.Trim() }
}

function Get-PrimaryAdapter {
    $adapter = Get-NetAdapter | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1
    if (-not $adapter) { throw 'No active network adapter was found.' }
    return $adapter
}

function Test-DomainReadiness {
    param([int]$InterfaceIndex)

    $dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
    if (-not $dnsServers -or $dnsServers.Count -eq 0) {
        throw 'No IPv4 DNS servers are configured on the active adapter.'
    }

    try {
        Resolve-DnsName $script:DomainName -ErrorAction Stop | Out-Null
    }
    catch {
        throw "DNS validation failed for domain '$($script:DomainName)'."
    }
}

function Set-ExactDnsServers {
    param([int]$InterfaceIndex,[string[]]$DesiredServers)
    $current = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
    if (@($current) -join ',' -ne @($DesiredServers) -join ',') {
        Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $DesiredServers
    }
}

function Set-ExactIPv4Configuration {
    param([int]$InterfaceIndex,[string]$DesiredIPv4,[int]$DesiredPrefixLength,[string]$DesiredGateway)
    $existing = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
    foreach ($address in $existing) {
        if ($address.IPAddress -eq $DesiredIPv4 -and $address.PrefixLength -eq $DesiredPrefixLength) { continue }
        if ($address.IPAddress -like '169.254.*') { continue }
        Remove-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -IPAddress $address.IPAddress -Confirm:$false -ErrorAction SilentlyContinue
    }
    $desiredAddress = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $DesiredIPv4 -and $_.PrefixLength -eq $DesiredPrefixLength } | Select-Object -First 1
    if (-not $desiredAddress) {
        New-NetIPAddress -InterfaceIndex $InterfaceIndex -IPAddress $DesiredIPv4 -PrefixLength $DesiredPrefixLength | Out-Null
    }
    $defaultRoutes = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue
    foreach ($route in $defaultRoutes) {
        if ($route.NextHop -ne $DesiredGateway) {
            Remove-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop $route.NextHop -Confirm:$false -ErrorAction SilentlyContinue
        }
    }
    $desiredRoute = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object NextHop -eq $DesiredGateway | Select-Object -First 1
    if (-not $desiredRoute) {
        New-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop $DesiredGateway -RouteMetric 256 | Out-Null
    }
}

Resolve-Inputs
Assert-Elevated

$adapter = Get-PrimaryAdapter
Set-ExactIPv4Configuration -InterfaceIndex $adapter.ifIndex -DesiredIPv4 $script:IPv4 -DesiredPrefixLength $script:PrefixLength -DesiredGateway $script:Gateway
Set-ExactDnsServers -InterfaceIndex $adapter.ifIndex -DesiredServers $script:DnsServers
Set-DnsClient -InterfaceIndex $adapter.ifIndex -ConnectionSpecificSuffix $script:DomainName -RegisterThisConnectionsAddress $true -UseSuffixWhenRegistering $true
Test-DomainReadiness -InterfaceIndex $adapter.ifIndex

$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $env:COMPUTERNAME -ne $script:TargetName
$joinRequired = -not $computerSystem.PartOfDomain
$restartRequired = $false

if ($renameRequired -or $joinRequired) {
    $credential = Get-Credential -UserName $script:JoinCredentialUser -Message 'Enter domain join credential for HQ-FS01'
    if ($renameRequired) {
        Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -NewName $script:TargetName -Force
        $restartRequired = $true
        Write-Output 'The server was renamed and joined to the domain.'
    } else {
        Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -Force
        $restartRequired = $true
        Write-Output 'The server was joined to the domain.'
    }
} else {
    Write-Output 'No rename or domain join action is required.'
}

Write-Output "TargetName: $($script:TargetName)"
Write-Output "IPv4: $($script:IPv4)/$($script:PrefixLength)"
Write-Output "Gateway: $($script:Gateway)"
Write-Output "DnsServers: $($script:DnsServers -join ', ')"
Write-Output "DomainValidation: OK"

if ($restartRequired -and $RestartIfNeeded) {
    Restart-Computer -Force
} elseif ($restartRequired) {
    Write-Output 'A restart is required to complete HQ-FS01 stage 1.'
}

Deploy-HQ-MGMT01-Stage1.ps1

param(
    [string]$TargetName,
    [string]$DomainName,
    [string]$OuPath,
    [string]$JoinCredentialUser,
    [string]$ExpectedDnsServer,
    [string]$ExpectedGateway,
    [switch]$EnableRdp,
    [switch]$SkipDomainJoin,
    [switch]$RestartIfNeeded
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)

    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) { Read-RequiredValue 'Target hostname' 'HQ-MGMT01' } else { $TargetName.Trim() }
    $script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainName.Trim() }
    $script:ExpectedDnsServer = if ([string]::IsNullOrWhiteSpace($ExpectedDnsServer)) { Read-RequiredValue 'Expected DNS server IPv4' '172.20.20.11' } else { $ExpectedDnsServer.Trim() }
    $script:ExpectedGateway = if ([string]::IsNullOrWhiteSpace($ExpectedGateway)) { Read-RequiredValue 'Expected default gateway IPv4' '172.20.20.1' } else { $ExpectedGateway.Trim() }
}

function Get-PrimaryAdapter {
    $adapter = Get-NetAdapter |
        Where-Object Status -eq 'Up' |
        Sort-Object ifIndex |
        Select-Object -First 1

    if (-not $adapter) { throw 'No active network adapter was found.' }
    return $adapter
}

function Test-FirstBootState {
    $setupState = Get-ItemProperty 'HKLM:\SYSTEM\Setup'
    $imageState = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State'

    if ($setupState.OOBEInProgress -ne 0) { throw 'OOBE is still in progress. Stop before joining the domain.' }
    if ($setupState.SystemSetupInProgress -ne 0) { throw 'System setup is still in progress. Stop before joining the domain.' }
    if ($imageState.ImageState -ne 'IMAGE_STATE_COMPLETE') { throw "Unexpected image state '$($imageState.ImageState)'." }
}

function Test-NetworkReadiness {
    param([Microsoft.Management.Infrastructure.CimInstance]$Adapter)

    $ipConfig = Get-NetIPConfiguration -InterfaceIndex $Adapter.ifIndex
    $ipv4Address = $ipConfig.IPv4Address | Select-Object -First 1
    $gateways = @($ipConfig.IPv4DefaultGateway | ForEach-Object NextHop | Where-Object { $_ })
    $dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4).ServerAddresses

    if (-not $ipv4Address) { throw 'No IPv4 address is assigned on the active adapter.' }
    if ($ExpectedGateway -notin $gateways) { throw "Expected gateway $ExpectedGateway was not found." }
    if ($ExpectedDnsServer -notin $dnsServers) { throw "Expected DNS server $ExpectedDnsServer was not found." }

    Resolve-DnsName $script:DomainName -ErrorAction Stop | Out-Null

    Write-Output "ActiveAdapter: $($Adapter.Name)"
    Write-Output "IPv4: $($ipv4Address.IPAddress)"
    Write-Output "Gateway: $($gateways -join ', ')"
    Write-Output "DnsServers: $($dnsServers -join ', ')"
}

function Enable-RemoteDesktopAccess {
    Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0
    Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
    Write-Output 'RemoteDesktop: Enabled'
}

function Join-ManagementServer {
    if ($SkipDomainJoin) {
        return [pscustomobject]@{
            RestartRequired = $false
            Message = 'DomainJoin: Skipped'
        }
    }

    $computerSystem = Get-CimInstance Win32_ComputerSystem
    $renameRequired = $env:COMPUTERNAME -ne $script:TargetName
    $joinRequired = -not $computerSystem.PartOfDomain

    if (-not $renameRequired -and -not $joinRequired) {
        return [pscustomobject]@{
            RestartRequired = $false
            Message = 'No rename or domain join action is required.'
        }
    }

    $script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) { Read-RequiredValue 'Target server OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $OuPath.Trim() }
    $script:JoinCredentialUser = if ([string]::IsNullOrWhiteSpace($JoinCredentialUser)) { Read-RequiredValue 'Domain join user' 'corp.gntech.me\svc.join.hq' } else { $JoinCredentialUser.Trim() }

    $credential = Get-Credential -UserName $script:JoinCredentialUser -Message 'Enter domain join credential for HQ-MGMT01'

    if ($renameRequired) {
        Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -NewName $script:TargetName -Force
        return [pscustomobject]@{
            RestartRequired = $true
            Message = 'The management server was renamed and joined to the domain.'
        }
    }

    Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -Force
    return [pscustomobject]@{
        RestartRequired = $true
        Message = 'The management server was joined to the domain.'
    }
}

Resolve-Inputs
Assert-Elevated
Test-FirstBootState

$adapter = Get-PrimaryAdapter
Test-NetworkReadiness -Adapter $adapter

if ($EnableRdp) {
    Enable-RemoteDesktopAccess
}

$joinResult = Join-ManagementServer
$restartRequired = [bool]$joinResult.RestartRequired
$computerSystem = Get-CimInstance Win32_ComputerSystem

Write-Output "TargetName: $($script:TargetName)"
Write-Output "DomainName: $($script:DomainName)"
Write-Output "CurrentComputerName: $($computerSystem.Name)"
Write-Output "CurrentDomain: $($computerSystem.Domain)"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
if (-not $SkipDomainJoin) {
    Write-Output "OuPath: $($script:OuPath)"
}
Write-Output $joinResult.Message
Write-Output "RestartRequired: $restartRequired"
Write-Output 'ManagementServerStage1: Complete'

if ($restartRequired -and $RestartIfNeeded) {
    Restart-Computer -Force
} elseif ($restartRequired) {
    Write-Output 'A restart is required to complete HQ-MGMT01 domain onboarding.'
}

Grant-HQ-JoinDelegation.ps1

param(
    [string]$JoinAccountSam = 'svc.join.hq',
    [string[]]$TargetOuDns,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-StringList {
    param(
        [string]$Prompt,
        [string[]]$DefaultValues
    )

    while ($true) {
        $defaultValue = $DefaultValues -join ','
        $value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
        $items = @(
            $value.Split(',') |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )

        if ($items.Count -gt 0) {
            return $items
        }
    }
}

function Resolve-Inputs {
    $script:JoinAccountSam = if ([string]::IsNullOrWhiteSpace($JoinAccountSam)) {
        Read-RequiredValue -Prompt 'Join account SamAccountName' -DefaultValue 'svc.join.hq'
    }
    else {
        $JoinAccountSam.Trim()
    }

    $script:TargetOuDns = if ($TargetOuDns.Count -eq 0) {
        Read-StringList -Prompt 'Target OU distinguished names (comma-separated)' -DefaultValues @(
            'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me'
        )
    }
    else {
        @($TargetOuDns | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Assert-JoinAccountExists {
    param([string]$SamAccountName)

    $account = Get-ADUser -Filter "SamAccountName -eq '$SamAccountName'" -Properties SID -ErrorAction SilentlyContinue
    if (-not $account) {
        throw "The join account '$SamAccountName' was not found in Active Directory."
    }

    return $account
}

function Assert-OuExists {
    param([string]$DistinguishedName)

    $ou = Get-ADOrganizationalUnit -Identity $DistinguishedName -ErrorAction SilentlyContinue
    if (-not $ou) {
        throw "The OU '$DistinguishedName' was not found."
    }

    return $ou
}

function Grant-JoinAcl {
    param(
        [string]$TargetOuDn,
        [System.Security.Principal.SecurityIdentifier]$Sid
    )

    $computerClassGuid = [Guid]'bf967a86-0de6-11d0-a285-00aa003049e2'
    $emptyGuid = [Guid]::Empty
    $aclPath = "AD:$TargetOuDn"
    $acl = Get-Acl -Path $aclPath

    $rules = @(
        (New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
            $Sid,
            [System.DirectoryServices.ActiveDirectoryRights]::CreateChild,
            [System.Security.AccessControl.AccessControlType]::Allow,
            $computerClassGuid,
            [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
        )),
        (New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
            $Sid,
            [System.DirectoryServices.ActiveDirectoryRights]::DeleteChild,
            [System.Security.AccessControl.AccessControlType]::Allow,
            $computerClassGuid,
            [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
        )),
        (New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
            $Sid,
            [System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
            [System.Security.AccessControl.AccessControlType]::Allow,
            $emptyGuid,
            [System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
            $computerClassGuid
        ))
    )

    $results = foreach ($rule in $rules) {
        $present = @($acl.Access | Where-Object {
            $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $Sid.Value -and
            $_.AccessControlType -eq $rule.AccessControlType -and
            ($_.ActiveDirectoryRights -band $rule.ActiveDirectoryRights) -eq $rule.ActiveDirectoryRights -and
            $_.ObjectType -eq $rule.ObjectType -and
            $_.InheritanceType -eq $rule.InheritanceType -and
            $_.InheritedObjectType -eq $rule.InheritedObjectType
        }).Count -gt 0

        if (-not $present -and -not $ValidateOnly) { $acl.AddAccessRule($rule) | Out-Null }

        [pscustomobject]@{
            TargetOuDn = $TargetOuDn
            IdentitySid = $Sid.Value
            Rights = $rule.ActiveDirectoryRights
            InheritanceType = $rule.InheritanceType
            Action = if ($present) { 'Compliant' } elseif ($ValidateOnly) { 'Missing' } else { 'Granted' }
        }
    }

    if (-not $ValidateOnly -and @($results | Where-Object Action -eq 'Granted').Count -gt 0) {
        Set-Acl -Path $aclPath -AclObject $acl
    }

    return $results
}

Resolve-Inputs
Assert-Elevated
Ensure-Module -Name ActiveDirectory

$joinAccount = Assert-JoinAccountExists -SamAccountName $script:JoinAccountSam

$summary = [pscustomobject]@{
    JoinAccountSamAccountName = $joinAccount.SamAccountName
    JoinAccountDn = $joinAccount.DistinguishedName
    TargetOus = ($script:TargetOuDns -join ' | ')
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

foreach ($targetOuDn in $script:TargetOuDns) {
    Assert-OuExists -DistinguishedName $targetOuDn | Out-Null
    Grant-JoinAcl -TargetOuDn $targetOuDn -Sid $joinAccount.SID |
        Format-Table -AutoSize | Out-String | Write-Output
}

if ($ValidateOnly) {
    Write-Output 'Validation mode only. No delegation changes were applied.'
}
else {
    Write-Output 'Join delegation completed successfully.'
}

Join-HQ-Workstation.ps1

param(
    [string]$TargetName,
    [string]$DomainName,
    [string]$OuPath,
    [string]$DomainControllerFqdn,
    [string]$ExpectedDnsServer,
    [string]$ExpectedGateway,
    [string]$LegacyBuildGateway = '172.20.110.1',
    [string]$InternetTestHost,
    [pscredential]$DomainCredential,
    [switch]$RemoveLegacyBuildGateway,
    [switch]$RestartIfNeeded,
    [switch]$SkipInternetTest
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Resolve-Inputs {
    $script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) {
        Read-RequiredValue -Prompt 'Target workstation name' -DefaultValue $env:COMPUTERNAME
    }
    else {
        $TargetName.Trim()
    }

    $script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) {
        Read-RequiredValue -Prompt 'Domain FQDN' -DefaultValue 'corp.gntech.me'
    }
    else {
        $DomainName.Trim()
    }

    $script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) {
        Read-RequiredValue -Prompt 'Target OU distinguished name' -DefaultValue 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me'
    }
    else {
        $OuPath.Trim()
    }

    $script:DomainControllerFqdn = if ([string]::IsNullOrWhiteSpace($DomainControllerFqdn)) {
        Read-RequiredValue -Prompt 'Domain controller FQDN for validation' -DefaultValue 'hq-dc01.corp.gntech.me'
    }
    else {
        $DomainControllerFqdn.Trim()
    }

    $script:ExpectedDnsServer = if ([string]::IsNullOrWhiteSpace($ExpectedDnsServer)) {
        Read-RequiredValue -Prompt 'Expected DNS server IPv4' -DefaultValue '172.20.20.11'
    }
    else {
        $ExpectedDnsServer.Trim()
    }

    $script:ExpectedGateway = if ([string]::IsNullOrWhiteSpace($ExpectedGateway)) {
        Read-RequiredValue -Prompt 'Expected default gateway IPv4' -DefaultValue '172.20.30.1'
    }
    else {
        $ExpectedGateway.Trim()
    }

    $script:InternetTestHost = if ([string]::IsNullOrWhiteSpace($InternetTestHost)) {
        Read-RequiredValue -Prompt 'Internet validation host' -DefaultValue 'github.com'
    }
    else {
        $InternetTestHost.Trim()
    }
}

function Assert-Elevated {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)

    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Run this script from an elevated PowerShell session.'
    }
}

function Get-PrimaryAdapter {
    $adapter = Get-NetAdapter |
        Where-Object Status -eq 'Up' |
        Sort-Object ifIndex |
        Select-Object -First 1

    if (-not $adapter) {
        throw 'No active network adapter was found.'
    }

    return $adapter
}

function Test-FirstBootState {
    $setupState = Get-ItemProperty 'HKLM:\SYSTEM\Setup'
    $imageState = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State'

    if ($setupState.OOBEInProgress -ne 0) {
        throw 'OOBE is still in progress. Stop and fix the source template before domain join.'
    }

    if ($setupState.SystemSetupInProgress -ne 0) {
        throw 'System setup is still in progress. Stop and fix the source template before domain join.'
    }

    if ($imageState.ImageState -ne 'IMAGE_STATE_COMPLETE') {
        throw "Unexpected image state '$($imageState.ImageState)'. Stop and fix the source template before domain join."
    }
}

function Test-NetworkReadiness {
    param(
        [Microsoft.Management.Infrastructure.CimInstance]$Adapter
    )

    $ipConfig = Get-NetIPConfiguration -InterfaceIndex $Adapter.ifIndex
    $ipv4Address = $ipConfig.IPv4Address | Select-Object -First 1
    $defaultGateways = @($ipConfig.IPv4DefaultGateway)
    $dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4).ServerAddresses

    if (-not $ipv4Address) {
        throw 'No IPv4 address is assigned on the active adapter.'
    }

    if (-not $ipv4Address.PrefixOrigin -or $ipv4Address.PrefixOrigin -ne 'Dhcp') {
        throw 'The active adapter is not using DHCP for IPv4. Workstation onboarding requires DHCP.'
    }

    if ($defaultGateways.Count -eq 0) {
        throw 'No default gateway is present on the active adapter.'
    }

    $gatewayAddresses = @($defaultGateways | ForEach-Object NextHop | Where-Object { $_ })
    if ($ExpectedGateway -notin $gatewayAddresses) {
        throw "Expected gateway $ExpectedGateway was not found on the active adapter."
    }
    $unexpectedGateways = @($gatewayAddresses | Where-Object { $_ -ne $ExpectedGateway })
    if ($unexpectedGateways.Count -gt 0) {
        throw "Unexpected default gateway(s) detected: $($unexpectedGateways -join ', '). Correct the template or use the explicit legacy-build cleanup switch."
    }

    if (-not $dnsServers -or $dnsServers.Count -eq 0) {
        throw 'No IPv4 DNS servers are configured on the active adapter.'
    }

    if ($ExpectedDnsServer -notin $dnsServers) {
        throw "Expected DNS server $ExpectedDnsServer was not found on the active adapter."
    }

    Resolve-DnsName $DomainControllerFqdn | Out-Null

    if (-not $SkipInternetTest) {
        $internetTest = Test-NetConnection $InternetTestHost -Port 443 -WarningAction SilentlyContinue
        if (-not $internetTest.TcpTestSucceeded) {
            throw "Internet validation failed for $InternetTestHost on TCP 443."
        }
    }

    Write-Output "ActiveAdapter: $($Adapter.Name)"
    Write-Output "IPv4: $($ipv4Address.IPAddress)"
    Write-Output "Gateway: $($gatewayAddresses -join ', ')"
    Write-Output "DnsServers: $($dnsServers -join ', ')"
}

function Get-DomainJoinCredential {
    if ($DomainCredential) { return $DomainCredential }

    $defaultUser = "Administrator@$($script:DomainName)"

    try {
        return Get-Credential -UserName $defaultUser -Message 'Enter domain join credential'
    }
    catch {
        throw 'A valid domain credential is required to join the workstation.'
    }
}

function Remove-LegacyBuildRoute {
    param([Microsoft.Management.Infrastructure.CimInstance]$Adapter)

    if (-not $RemoveLegacyBuildGateway) { return }
    if ([string]::IsNullOrWhiteSpace($LegacyBuildGateway)) {
        throw 'LegacyBuildGateway is required when RemoveLegacyBuildGateway is used.'
    }

    $interface = Get-NetIPInterface -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4
    if ($interface.Dhcp -ne 'Enabled') {
        throw 'Legacy build-route cleanup is allowed only on a DHCP-enabled adapter.'
    }

    $legacyRoutes = @(Get-NetRoute `
        -PolicyStore PersistentStore `
        -InterfaceIndex $Adapter.ifIndex `
        -DestinationPrefix '0.0.0.0/0' `
        -ErrorAction SilentlyContinue |
        Where-Object NextHop -eq $LegacyBuildGateway)

    foreach ($route in $legacyRoutes) {
        Remove-NetRoute `
            -PolicyStore PersistentStore `
            -InterfaceIndex $Adapter.ifIndex `
            -DestinationPrefix $route.DestinationPrefix `
            -NextHop $route.NextHop `
            -Confirm:$false
    }

    $activeLegacyRoutes = @(Get-NetRoute `
        -PolicyStore ActiveStore `
        -InterfaceIndex $Adapter.ifIndex `
        -DestinationPrefix '0.0.0.0/0' `
        -ErrorAction SilentlyContinue |
        Where-Object NextHop -eq $LegacyBuildGateway)

    foreach ($route in $activeLegacyRoutes) {
        Remove-NetRoute `
            -PolicyStore ActiveStore `
            -InterfaceIndex $Adapter.ifIndex `
            -DestinationPrefix $route.DestinationPrefix `
            -NextHop $route.NextHop `
            -Confirm:$false
    }

    Write-Output "LegacyBuildPersistentRoutesRemoved: $($legacyRoutes.Count)"
    Write-Output "LegacyBuildActiveRoutesRemoved: $($activeLegacyRoutes.Count)"
}

Resolve-Inputs
Assert-Elevated
Test-FirstBootState

$adapter = Get-PrimaryAdapter
Remove-LegacyBuildRoute -Adapter $adapter
Test-NetworkReadiness -Adapter $adapter

$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $computerSystem.Name -ne $TargetName
$joinRequired = -not $computerSystem.PartOfDomain
$restartRequired = $false

Write-Output "CurrentComputerName: $($computerSystem.Name)"
Write-Output "TargetComputerName: $TargetName"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"

if (-not $renameRequired -and -not $joinRequired) {
    Write-Output 'No rename or domain join action is required.'
    return
}

$credential = Get-DomainJoinCredential

if ($renameRequired -and $joinRequired) {
    Add-Computer `
        -DomainName $DomainName `
        -Credential $credential `
        -OUPath $OuPath `
        -NewName $TargetName `
        -Force

    $restartRequired = $true
    Write-Output 'The workstation was renamed and joined to the domain.'
}
elseif ($joinRequired) {
    Add-Computer `
        -DomainName $DomainName `
        -Credential $credential `
        -OUPath $OuPath `
        -Force

    $restartRequired = $true
    Write-Output 'The workstation was joined to the domain.'
}
elseif ($renameRequired) {
    Rename-Computer -NewName $TargetName -DomainCredential $credential -Force
    $restartRequired = $true
    Write-Output 'The workstation was renamed.'
}

if ($restartRequired -and $RestartIfNeeded) {
    Restart-Computer -Force
}
elseif ($restartRequired) {
    Write-Output 'A restart is required to complete the workstation onboarding.'
}

Promote-HQ-DC01-Forest.ps1

param(
    [string]$DomainName,
    [string]$NetBIOSName,
    [switch]$InstallDns = $true,
    [switch]$NoRebootOnCompletion,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-RequiredSecret {
    param(
        [string]$Prompt
    )

    try {
        return (Read-Host -Prompt $Prompt -AsSecureString)
    }
    catch {
        throw "A secure value for '$Prompt' is required when running non-interactively."
    }
}

function Convert-SecureStringToLength {
    param(
        [Security.SecureString]$SecureValue
    )

    $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureValue)
    try {
        return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr).Length
    }
    finally {
        if ($bstr -ne [IntPtr]::Zero) {
            [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
        }
    }
}

function Resolve-Inputs {
    $script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) {
        Read-RequiredValue -Prompt 'Forest/domain FQDN' -DefaultValue 'corp.gntech.me'
    }
    else {
        $DomainName.Trim()
    }

    $script:NetBIOSName = if ([string]::IsNullOrWhiteSpace($NetBIOSName)) {
        Read-RequiredValue -Prompt 'NetBIOS name' -DefaultValue 'GNTECH'
    }
    else {
        $NetBIOSName.Trim()
    }

    if (-not $ValidateOnly) {
        $script:LocalAdministratorPassword = Read-RequiredSecret `
            -Prompt 'Local Administrator password'

        $script:SafeModeAdministratorPassword = Read-RequiredSecret `
            -Prompt 'DSRM password'
    }
}

function Test-Prerequisites {
    $computerSystem = Get-CimInstance Win32_ComputerSystem
    if ($computerSystem.PartOfDomain) {
        throw "Server is already joined to domain '$($computerSystem.Domain)'."
    }

    $addsFeature = Get-WindowsFeature AD-Domain-Services
    if ($addsFeature.InstallState -ne 'Installed') {
        throw 'AD-Domain-Services feature is not installed.'
    }

    $localAdministrator = Get-LocalUser | Where-Object SID -like 'S-1-5-21-*-500' | Select-Object -First 1
    if (-not $localAdministrator) {
        throw 'Built-in local Administrator account with RID 500 was not found.'
    }

    if (-not $ValidateOnly) {
        $localAdminLength = Convert-SecureStringToLength -SecureValue $script:LocalAdministratorPassword
        $safeModeLength = Convert-SecureStringToLength -SecureValue $script:SafeModeAdministratorPassword

        if ($localAdminLength -lt 12) {
            throw 'Local Administrator password must be at least 12 characters.'
        }

        if ($safeModeLength -lt 12) {
            throw 'DSRM password must be at least 12 characters.'
        }
    }

    [pscustomobject]@{
        Hostname = $env:COMPUTERNAME
        DomainName = $script:DomainName
        NetBIOSName = $script:NetBIOSName
        PartOfDomain = $computerSystem.PartOfDomain
        AddsInstalled = $addsFeature.InstallState
        InstallDns = [bool]$InstallDns
        NoRebootOnCompletion = [bool]$NoRebootOnCompletion
        ValidateOnly = [bool]$ValidateOnly
    }
}

Resolve-Inputs
$summary = Test-Prerequisites
$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. Promotion was not started.'
    return
}

$localAdministrator = Get-LocalUser | Where-Object SID -like 'S-1-5-21-*-500' | Select-Object -First 1
$localAdministrator | Set-LocalUser -Password $script:LocalAdministratorPassword

$promotionParams = @{
    DomainName                    = $script:DomainName
    DomainNetbiosName             = $script:NetBIOSName
    SafeModeAdministratorPassword = $script:SafeModeAdministratorPassword
    InstallDns                    = [bool]$InstallDns
    Force                         = $true
    NoRebootOnCompletion          = [bool]$NoRebootOnCompletion
}

Install-ADDSForest @promotionParams

Validate-HQ-DC01.ps1

param(
    [string]$ExpectedHostname,
    [string]$ExpectedDomainFqdn,
    [string]$ExpectedNetBIOSName,
    [string]$ExpectedUpnSuffix,
    [string]$ExpectedDcIp,
    [string[]]$ExpectedDnsForwarders,
    [string[]]$ExpectedDnsZones,
    [string]$ExpectedDhcpAuthorizedDnsName,
    [string]$ExpectedDhcpAuthorizedIp,
    [string]$ExpectedDhcpScopeId,
    [string]$ExpectedEnterpriseRootOuDn,
    [string]$OuTemplateCsvPath,
    [string[]]$ExpectedOuDns
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) {
        $fullPrompt = "$Prompt [$DefaultValue]"
    }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value.Trim()
        }

        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
            return $DefaultValue
        }
    }
}

function Read-StringList {
    param(
        [string]$Prompt,
        [string[]]$DefaultValues
    )

    while ($true) {
        $defaultValue = $DefaultValues -join ','
        $value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
        $items = @(
            $value.Split(',') |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )

        if ($items.Count -gt 0) {
            return $items
        }
    }
}

function Resolve-Inputs {
    $script:ExpectedHostname = if ([string]::IsNullOrWhiteSpace($ExpectedHostname)) {
        Read-RequiredValue -Prompt 'Expected hostname' -DefaultValue 'HQ-DC01'
    } else { $ExpectedHostname.Trim() }

    $script:ExpectedDomainFqdn = if ([string]::IsNullOrWhiteSpace($ExpectedDomainFqdn)) {
        Read-RequiredValue -Prompt 'Expected domain FQDN' -DefaultValue 'corp.gntech.me'
    } else { $ExpectedDomainFqdn.Trim() }

    $script:ExpectedNetBIOSName = if ([string]::IsNullOrWhiteSpace($ExpectedNetBIOSName)) {
        Read-RequiredValue -Prompt 'Expected NetBIOS name' -DefaultValue 'GNTECH'
    } else { $ExpectedNetBIOSName.Trim() }

    $script:ExpectedUpnSuffix = if ([string]::IsNullOrWhiteSpace($ExpectedUpnSuffix)) {
        Read-RequiredValue -Prompt 'Expected UPN suffix' -DefaultValue 'gntech.me'
    } else { $ExpectedUpnSuffix.Trim() }

    $script:ExpectedDcIp = if ([string]::IsNullOrWhiteSpace($ExpectedDcIp)) {
        Read-RequiredValue -Prompt 'Expected DC IPv4' -DefaultValue '172.20.20.11'
    } else { $ExpectedDcIp.Trim() }

    $script:ExpectedDnsForwarders = if ($ExpectedDnsForwarders.Count -eq 0) {
        Read-StringList -Prompt 'Expected DNS forwarders (comma-separated)' -DefaultValues @('1.1.1.1','9.9.9.9')
    } else {
        @($ExpectedDnsForwarders | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }

    $script:ExpectedDnsZones = if ($ExpectedDnsZones.Count -eq 0) {
        Read-StringList -Prompt 'Expected DNS zones (comma-separated)' -DefaultValues @('corp.gntech.me','20.20.172.in-addr.arpa','30.20.172.in-addr.arpa')
    } else {
        @($ExpectedDnsZones | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }

    $script:ExpectedDhcpAuthorizedDnsName = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpAuthorizedDnsName)) {
        Read-RequiredValue -Prompt 'Expected DHCP authorized DNS name' -DefaultValue 'hq-dc01.corp.gntech.me'
    } else { $ExpectedDhcpAuthorizedDnsName.Trim() }

    $script:ExpectedDhcpAuthorizedIp = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpAuthorizedIp)) {
        Read-RequiredValue -Prompt 'Expected DHCP authorized IP' -DefaultValue '172.20.20.11'
    } else { $ExpectedDhcpAuthorizedIp.Trim() }

    $script:ExpectedDhcpScopeId = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpScopeId)) {
        Read-RequiredValue -Prompt 'Expected DHCP scope ID' -DefaultValue '172.20.30.0'
    } else { $ExpectedDhcpScopeId.Trim() }

    $script:ExpectedEnterpriseRootOuDn = if ([string]::IsNullOrWhiteSpace($ExpectedEnterpriseRootOuDn)) {
        Read-RequiredValue -Prompt 'Expected enterprise root OU DN' -DefaultValue 'OU=GNTECH,DC=corp,DC=gntech,DC=me'
    } else { $ExpectedEnterpriseRootOuDn.Trim() }

    $script:OuTemplateCsvPath = if ([string]::IsNullOrWhiteSpace($OuTemplateCsvPath)) {
        $scriptDirectory = if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
            $PSScriptRoot
        } else {
            (Get-Location).Path
        }

        $defaultCsv = Join-Path -Path $scriptDirectory -ChildPath 'ou-baseline.csv'
        if (Test-Path -LiteralPath $defaultCsv) { $defaultCsv } else { '' }
    } else { $OuTemplateCsvPath.Trim() }

    $script:ExpectedOuDns = if ($ExpectedOuDns.Count -eq 0 -and [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
        @(
            'OU=Tier0,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Tier1,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Tier2,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Groups,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=ServiceAccounts,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Admins,OU=GNTECH,DC=corp,DC=gntech,DC=me',
            'OU=Sites,OU=GNTECH,DC=corp,DC=gntech,DC=me'
        )
    } else {
        @($ExpectedOuDns | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Get-ForwarderStrings {
    @(Get-DnsServerForwarder -ErrorAction SilentlyContinue | ForEach-Object {
        if ($_.IPAddress -is [array]) {
            $_.IPAddress | ForEach-Object { $_.IPAddressToString }
        }
        elseif ($_.IPAddress) {
            $_.IPAddress.IPAddressToString
        }
    })
}

function Get-ExpectedOuDnsFromCsv {
    param([string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "OU template CSV '$Path' was not found."
    }

    @(Import-Csv -LiteralPath $Path | Where-Object {
        $_.Enabled -match '^(?i:true|1|yes)$'
    } | ForEach-Object {
        "OU=$($_.Name),$($_.ParentDn)"
    })
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name DhcpServer
Ensure-Module -Name DnsServer

if (-not [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
    $script:ExpectedOuDns = @(Get-ExpectedOuDnsFromCsv -Path $script:OuTemplateCsvPath | Where-Object { $_ -ne $script:ExpectedEnterpriseRootOuDn })
}

$forest = Get-ADForest
$domain = Get-ADDomain
$computerSystem = Get-CimInstance Win32_ComputerSystem
$features = Get-WindowsFeature AD-Domain-Services, DNS, DHCP
$services = Get-Service NTDS, DNS, DHCPServer
$forwarders = Get-ForwarderStrings
$zones = @(Get-DnsServerZone -ErrorAction SilentlyContinue | Where-Object ZoneName -in $script:ExpectedDnsZones)
$authorizedDhcp = @(Get-DhcpServerInDC -ErrorAction SilentlyContinue)
$scope = Get-DhcpServerv4Scope -ScopeId $script:ExpectedDhcpScopeId -ErrorAction SilentlyContinue
$scopeOptions = @(Get-DhcpServerv4OptionValue -ScopeId $script:ExpectedDhcpScopeId -ErrorAction SilentlyContinue)
$rootOu = Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$($script:ExpectedEnterpriseRootOuDn))" -ErrorAction SilentlyContinue
$expectedOusFound = @()
foreach ($ouDn in $script:ExpectedOuDns) {
    $found = Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$ouDn)" -ErrorAction SilentlyContinue
    if ($found) {
        $expectedOusFound += $ouDn
    }
}

Write-Output '=== Identity ==='
Write-Output "Hostname: $env:COMPUTERNAME"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
Write-Output "ComputerSystemDomain: $($computerSystem.Domain)"
Write-Output "ForestRootDomain: $($forest.RootDomain)"
Write-Output "ForestMode: $($forest.ForestMode)"
Write-Output "UPNSuffixes: $($forest.UPNSuffixes -join ', ')"
Write-Output "DomainDNSRoot: $($domain.DNSRoot)"
Write-Output "DomainNetBIOSName: $($domain.NetBIOSName)"
Write-Output "DomainMode: $($domain.DomainMode)"

Write-Output '=== Features ==='
foreach ($feature in $features) {
    Write-Output "$($feature.Name): $($feature.InstallState)"
}

Write-Output '=== Services ==='
foreach ($service in $services) {
    Write-Output "$($service.Name): $($service.Status)"
}

Write-Output '=== DNS ==='
if ($forwarders.Count -eq 0) {
    Write-Output 'Forwarders: none found'
} else {
    foreach ($forwarder in $forwarders) {
        Write-Output "Forwarder: $forwarder"
    }
}

if ($zones.Count -eq 0) {
    Write-Output 'Zones: none found'
} else {
    foreach ($zone in $zones) {
        Write-Output "Zone: $($zone.ZoneName) / Type=$($zone.ZoneType) / DsIntegrated=$($zone.IsDsIntegrated)"
    }
}

Write-Output '=== DHCP ==='
if ($authorizedDhcp.Count -eq 0) {
    Write-Output 'Authorized DHCP servers: none found'
} else {
    foreach ($entry in $authorizedDhcp) {
        Write-Output "Authorized DHCP: $($entry.DnsName) / $($entry.IPAddress.IPAddressToString)"
    }
}

if (-not $scope) {
    Write-Output "Scope $($script:ExpectedDhcpScopeId): missing"
} else {
    Write-Output "Scope: $($scope.ScopeId.IPAddressToString) / Name=$($scope.Name) / State=$($scope.State) / Start=$($scope.StartRange.IPAddressToString) / End=$($scope.EndRange.IPAddressToString)"
}

if ($scopeOptions.Count -eq 0) {
    Write-Output "DHCP options for scope $($script:ExpectedDhcpScopeId): none found"
} else {
    foreach ($option in $scopeOptions) {
        $valueText = if ($option.Value -is [array]) { $option.Value -join ', ' } else { $option.Value }
        Write-Output "DHCP Option $($option.OptionId): $($option.Name) = $valueText"
    }
}

Write-Output '=== OU Structure ==='
if (-not $rootOu) {
    Write-Output "Enterprise root OU missing: $($script:ExpectedEnterpriseRootOuDn)"
} else {
    Write-Output "Enterprise root OU: $($rootOu.DistinguishedName)"
}

foreach ($ouDn in $script:ExpectedOuDns) {
    if ($expectedOusFound -contains $ouDn) {
        Write-Output "OU Present: $ouDn"
    } else {
        Write-Output "OU Missing: $ouDn"
    }
}

Validate-WS2025-GoldenImage.ps1

[CmdletBinding()]
param(
    [string]$ExpectedCaptionPattern = '*Windows Server 2025 Standard*',
    [string]$ExpectedPowerShellVersion = '7.6.3'
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$failures = [System.Collections.Generic.List[string]]::new()

Write-Output '=== Operating system ==='
$operatingSystem = Get-CimInstance Win32_OperatingSystem
Write-Output "Caption: $($operatingSystem.Caption)"
Write-Output "Version: $($operatingSystem.Version)"
Write-Output "Build: $($operatingSystem.BuildNumber)"
if ($operatingSystem.Caption -notlike $ExpectedCaptionPattern) {
    $failures.Add("Unexpected Windows edition: $($operatingSystem.Caption)")
}

Write-Output '=== QEMU Guest Agent ==='
$qemuAgent = Get-Service -Name 'QEMU-GA' -ErrorAction Stop
Write-Output "QEMU-GA: $($qemuAgent.Status) / $($qemuAgent.StartType)"
if ($qemuAgent.Status -ne 'Running') { $failures.Add('QEMU-GA is not running.') }
if ($qemuAgent.StartType -ne 'Automatic') { $failures.Add('QEMU-GA is not automatic.') }

Write-Output '=== OpenSSH ==='
$sshd = Get-Service -Name 'sshd' -ErrorAction SilentlyContinue
if ($sshd) {
    Write-Output "sshd: $($sshd.Status) / $($sshd.StartType)"
    if ($sshd.Status -ne 'Stopped') { $failures.Add('sshd is not stopped.') }
    if ($sshd.StartType -ne 'Disabled') { $failures.Add('sshd is not disabled.') }
} else {
    Write-Output 'sshd: Not installed'
}

Write-Output '=== PowerShell 7 ==='
$pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe'
if (-not (Test-Path $pwshPath)) {
    $failures.Add('PowerShell 7 executable is missing.')
} else {
    $pwshVersion = & $pwshPath -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'
    Write-Output "PowerShell 7 version: $pwshVersion"
    if ($pwshVersion -ne $ExpectedPowerShellVersion) {
        $failures.Add("Unexpected PowerShell 7 version: $pwshVersion")
    }
}

Write-Output '=== Problem devices ==='
$pnpJob = Start-Job -ScriptBlock {
    @(Get-PnpDevice -PresentOnly | Where-Object Status -ne 'OK')
}
if (-not (Wait-Job -Job $pnpJob -Timeout 60)) {
    Stop-Job -Job $pnpJob
    Remove-Job -Job $pnpJob -Force
    $failures.Add('PnP device enumeration exceeded 60 seconds.')
    $problemDevices = @()
} else {
    $problemDevices = @(Receive-Job -Job $pnpJob)
    Remove-Job -Job $pnpJob -Force
}
if ($problemDevices.Count -gt 0) {
    $problemDevices | Select-Object Class, FriendlyName, Status, Problem |
        Format-Table -AutoSize | Out-String | Write-Output
    $failures.Add("$($problemDevices.Count) problem device(s) detected.")
} else {
    Write-Output 'No problem devices detected.'
}

Write-Output '=== Forbidden roles ==='
$forbiddenRoles = @(
    Get-WindowsFeature AD-Domain-Services, DNS, DHCP, Web-Server, FS-FileServer, Hyper-V |
        Where-Object InstallState -eq 'Installed'
)
if ($forbiddenRoles.Count -gt 0) {
    $forbiddenRoles | Select-Object Name, DisplayName |
        Format-Table -AutoSize | Out-String | Write-Output
    $failures.Add('One or more forbidden roles are installed.')
} else {
    Write-Output 'No forbidden roles detected.'
}

Write-Output '=== Domain membership ==='
$computerSystem = Get-CimInstance Win32_ComputerSystem
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
Write-Output "Domain or workgroup: $($computerSystem.Domain)"
if ($computerSystem.PartOfDomain) { $failures.Add('Golden image clone is joined to a domain.') }

Write-Output '=== Network state ==='
$manualAddresses = @(
    Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
        Where-Object { $_.InterfaceAlias -ne 'Loopback Pseudo-Interface 1' -and $_.PrefixOrigin -eq 'Manual' }
)
$persistentDefaultRoutes = @(
    Get-NetRoute -PolicyStore PersistentStore -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue
)
Write-Output "Manual IPv4 addresses: $($manualAddresses.Count)"
Write-Output "Persistent default routes: $($persistentDefaultRoutes.Count)"
if ($manualAddresses.Count -gt 0) { $failures.Add('A manual IPv4 address remains in the image clone.') }
if ($persistentDefaultRoutes.Count -gt 0) { $failures.Add('A persistent default route remains in the image clone.') }

Write-Output '=== Pending reboot ==='
$pendingReboot = (
    (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') -or
    (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') -or
    (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations')
)
Write-Output "Pending reboot: $pendingReboot"
if ($pendingReboot) { $failures.Add('A reboot is pending.') }

Write-Output '=== Result ==='
if ($failures.Count -gt 0) {
    $failures | ForEach-Object { Write-Error $_ }
    exit 1
}

Write-Output 'Golden image validation passed.'
exit 0