PSCreateADForest.psm1

#Requires -RunAsAdministrator

function Get-OSTarget {
  <#
    .SYNOPSIS
        This function checks if the operating system is Windows Server.

    .DESCRIPTION
        The `Get-OSTarget` function checks the CIM instance class for the Operating System type.
        If it is not ProductType 2 or 3, it throws an exception.

    .PARAMETER None

    .INPUTS

    .OUTPUTS

    .EXAMPLE
        Get-OSTarget

        This command will return no output if it is Windows Server. If it is not Windows Server,
        an exception will be thrown.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Get-OSTarget.md
  #>

  $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
  if (-not ($osInfo.ProductType -eq 2 -or $osInfo.ProductType -eq 3)) {
    throw 'This script must run on Windows Server.'
  }
}

function Get-AdapterCount {
  <#
    .SYNOPSIS
        This function checks the total amount of network adapters that currently have an active network
        connection.

    .DESCRIPTION
        The `Get-AdapterCount` function checks the total number of active network adapters on the server.
        If there is no active network adapters (or more than one), an exception will be thrown.
        This function assumes that the network adapters are not WiFi Connections, as this is not ideal
        for a Domain Controller.

    .PARAMETER None

    .INPUTS

    .OUTPUTS

    .EXAMPLE
        Get-AdapterCount

        This command will return no output if all requirements are met.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Get-AdapterCount.md
    #>

  $activeAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' }
  $adapterCount = $activeAdapters.Count
  if ($adapterCount -eq 0) {
    throw 'A domain controller requires at least one active network interface.'
  }
  elseif ($adapterCount -gt 1) {
    throw 'Multiple active network interfaces detected. Please disable additional interfaces.'
  }
}

function Get-IPv4NetworkID {
  <#
    .SYNOPSIS
        Retrieves the IPv4 network ID for the active network adapter.

    .DESCRIPTION
        The `Get-IPv4NetworkID` function calculates the network ID for the active IPv4 network adapter
        by performing a bitwise AND operation between the IP address and subnet mask. It returns the
        network ID in CIDR notation (e.g., 192.168.1.0/24).

    .PARAMETER None
        This function does not take any parameters.

    .INPUTS
        None. This function does not take input.

    .OUTPUTS
        [string]
            The network ID in CIDR notation, such as "192.168.1.0/24".

    .EXAMPLE
        Get-IPv4NetworkID
        192.168.1.0/24

        This example retrieves the network ID for the current IPv4 network adapter.

    .EXAMPLE
        $networkID = Get-IPv4NetworkID
        Write-Output $networkID
        192.168.1.0/24

        This example stores the network ID in the `$networkID` variable and outputs it.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Get-IPv4NetworkID.md

    #>

  try {
    $adapterConfig = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled }
    $ipAddress = $adapterConfig.IPaddress[0]
    $ipSubnetMask = $adapterConfig.IPSubnet[0]
    $ipAddressBytes = [System.Net.IPAddress]::Parse($ipAddress).GetAddressBytes()
    $ipSubnetMaskBytes = [System.Net.IPAddress]::Parse($ipSubnetMask).GetAddressBytes()
    $networkBytes = @()
    for ($byteIndex = 0; $byteIndex -lt $ipAddressBytes.Length; $byteIndex++) {
      $networkBytes += $ipAddressBytes[$byteIndex] -band $ipSubnetMaskBytes[$byteIndex]
    }
    $firstAddressInSubnet = [System.Net.IPAddress]::new($networkBytes).IPAddressToString
    $networkAdapterPrefix = (Get-NetAdapter).ifIndex
    $cidrPrefix = (Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex $networkAdapterPrefix | Select-Object PrefixLength).PrefixLength
    $networkID = "$firstAddressInSubnet/$cidrPrefix"
  }
  catch {
    throw "$_"
  }
  return $networkID
}

function Get-DomainInfo {
  <#
    .SYNOPSIS
      This function gets information about the current Windows domain and its domain controller.

    .DESCRIPTION
        The `Get-DomainInfo` function checks whether the computer is joined to a Windows domain. If it is,
        the function retrieves details about the domain controller, including the domain name, site name,
        and hostname. If the computer is not joined to a domain, the function throws an error.

        If there are multiple domain controllers, it gets the information from the last domain controller
        listed (sorted by hostname in alphanumerical order).

    .PARAMETER None
        This function does not take any parameters.

    .INPUTS
        None. This function does not take input.

    .OUTPUTS
        [PSCustomObject]
            A Custom Object with:
            - DomainName: FQDN of the Windows domain.
            - SiteName: Site associated with the Domain Controller.
            - HostName: Domain Controller's hostname.

    .EXAMPLE
        $domainInfo = Get-DomainInfo
        Write-Output $domainInfo.DomainName

        This example stores the domain information in the `$domainInfo` variable and outputs the domain name.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Get-DomainInfo.md
    #>

  $domainInfo = Get-CimInstance -Namespace root\cimv2 -ClassName Win32_ComputerSystem
  if (-not $domainInfo.PartOfDomain) {
    throw 'Server must already be joined to a Windows Domain.'
  }
  try {
    $domainController = Get-ADDomainController -Filter * | Sort-Object -Property HostName | Select-Object -Last 1
    return [PSCustomObject]@{
      DomainName = $domainController.Domain
      SiteName   = $domainController.Site
      Hostname   = $domainController.HostName
    }
  }
  catch {
    throw "Error getting domain controller info: $($_.Exception.Message)"
  }
}

function Approve-DomainVariables {
  <#
    .SYNOPSIS
        This function checks to see if the variables provided about the new domain are in correct format.

    .DESCRIPTION
        The `Approve-DomainVariables` function takes the Domain parameter (the domain name you're creating),
        and verifies that it is in the format of a Fully Qualified Domain Name (FQDN), if it isn't it
        throws an exception.

        It also takes the NetBios parameter. It verifies the parameter is no more than 15 characters, and
        does not contain any other special characters. If it doesn't meet this criteria, it throws an
        exception.

        Finally, it takes the ModeType parameter. It verifies that the parameter is an accepted value. If
        it is not, an exception is thrown. Accepted values can be: 'Win2008', 'Win2008R2', 'Win2012',
        'Win2012R2', 'WinThreshold', and 'Default'. In most cases this can be WinThreshold or Default -
        unless you are working with legacy systems.

    .PARAMETER Domain
        A domain is a logical grouping of objects (users, computers, and devices) that share a common
        directory database, security policies, and trust boundaries.

        It is defined by a a domain name, which this parameter is checking. This must be in a FQDN
        format. (ie. my.domain.com, mydomain.net, etc.)

    .PARAMETER NetBios
        The NetBIOS name is a short name used for backward compatibility with older Windows systems and
        legacy applications to identify the domain.

    .PARAMETER ModeType
        The ModeType (or functional level of the domain) determines the available features and compatibility
        based on the version of Windows Server used by the domain controllers.

        Accepted values can be: 'Win2008', 'Win2008R2', 'Win2012', 'Win2012R2', 'WinThreshold', and 'Default'.
        In most cases this can be WinThreshold or Default - unless you are working with legacy systems.

    .INPUTS
        [string]$Domain
        [string]$NetBios
        [string]$ModeType

    .OUTPUTS
        None. This function performs operations but does not return output.

    .EXAMPLE
        Approve-DomainVariables -Domain mydomain.com -NetBios mydomain -ModeType Win2008

        This command will verify that mydomain.com is a valid FQDN, NetBios doesn't have special characters and isn't
        over 15 characters, and that the ModeType is valid (in this case, Win2008).
  
    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree
  
    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Approve-DomainVariables.md
    #>

  param(
    [Parameter(Mandatory = $true)]
    [string]$Domain,
    [Parameter(Mandatory = $true)]
    [string]$NetBios,
    [Parameter(Mandatory = $true)]
    [string]$ModeType
  )
  $fqdnRegex = '^(?=.{1,255}$)([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$'
  if ($Domain -notmatch $fqdnRegex) {
    throw "Domain Name doesn't match FQDN format: $Domain"
  }
  $netBiosRegex = '^[A-Za-z0-9-]+$'
  if ($NetBios -notmatch $netBiosRegex -or $NetBios.Count -gt 15) {
    throw 'Invalid NetBIOS name. Must not have spaces, special characters or exceed 15 characters in length: '
  }
  $modeArray = @(
    'Win2008',
    'Win2008R2'
    'Win2012',
    'Win2012R2',
    'WinThreshold',
    'Default'
  )
  if ($modeArray -notcontains $ModeType) {
    throw "$ModeType is not supported, must use $modeArray"
  }
}

function Approve-IPVariables {
  <#
    .SYNOPSIS
        This function validates an array of IP variables to ensure it is in the expected formats.

    .DESCRIPTION
        The `Approve-IPVariables` function takes an array of IP variables to ensure it is in
        the expected formats.

        The first three items in the array must be valid IP addresses, and the final item in
        the array must be a subnet prefix between 8 and 24.

    .PARAMETER IpVars
        An array containing four elements:
            - The first three elements should be valid IPv4 addresses.
            - The fourth element should be a number representing the network prefix, ranging from 8 to 30.

    .INPUTS
        [array]$IpVars
          The function expects an array input with exactly four elements.

    .OUTPUTS
        None. This script performs operations but does not return output.

    .EXAMPLE
        Approve-IPVariables -IpVars @('192.168.0.1', '192.168.0.2', '192.168.0.3', 24)

        Validates the provided array of IP variables. If all conditions are met, the function completes silently.
 
    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Approve-IPVariables.md
    #>

  param (
    [Parameter(Mandatory = $true)]
    [array]$IpVars
  )
  $ipRegex = '^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$'
  if ($IpVars.Count -ne 4) {
    throw "Too many items in array ipVars array. Expecting 4 - got $($ipVars.Count)"
  }
  elseif (-not ($IpVars[-1] -ge 8 -and $IpVars[-1] -le 30)) {
    throw "The value of the network prefix is not between 8 and 30. Got $($IpVars[-1])"
  }
  else {
    $IpVars[0..2] | ForEach-Object {
      if ($_ -notmatch $ipRegex) {
        throw "$_ is not a valid IPv4 address."
      }
    }
  }
}

function Add-NetworkConfig {
  <#
    .SYNOPSIS
        This function turns off IPv6 Random & Temporary IP Assignments, as well as IPv6 Transition Technologies.

    .DESCRIPTION
        The `Add-NetworkConfig` function disables several IPv6 features on the Windows Server. This includes
        IPv6 Random & Temporary IP Assignments, as well as IPv6 Transition Technologies.

        This typically isn't a necessar step, but if IPv6 isn't enabled or supported by your network, it is
        best to disable it.

    .PARAMETER None

    .INPUTS

    .OUTPUTS

    .EXAMPLE
        Add-NetworkConfig

        This command turns off IPv6 Random & Temporary IP Assignments, as well as IPv6 Transition Technologies.
    
    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Add-NetworkConfig.md
    #>

  try {
    Set-NetIPv6Protocol -RandomizeIdentifiers Disabled
    Set-NetIPv6Protocol -UseTemporaryAddresses Disabled
    Set-Net6to4Configuration -State Disabled
    Set-NetIsatapConfiguration -State Disabled
    Set-NetTeredoConfiguration -Type Disabled
  }
  catch {
    throw "Failed to set network configuration: $($_.Exception.Message)"
  }
}

function Invoke-DomainControllerNetworkSettings {
  <#
    .SYNOPSIS
        Configures a network interface on a Windows Server for use as a domain controller, setting
        the hostname, IPv4 address, subnet prefix, gateway, and DNS server.

    .DESCRIPTION
        This script configures the network settings required for setting up a domain controller. It
        takes the following parameters: `Hostname`, `IPv4Address`, `IPv4Prefix`, `IPv4Gateway`,
        and `IPv4DNS`. The function validates the input parameters, ensures the system is running
        Windows Server, checks for active network interfaces, and disables unnecessary IPv6 configurations.

        It then sets the IPv4 address, default gateway, and DNS server for the network interface. If the
        hostname is different from the current computer name, it renames the computer to match the
        specified `Hostname`.

    .PARAMETER Hostname
        The name that the machine will be assigned as its hostname.
        If the current hostname is already set, enter the existing hostname.

    .PARAMETER IPv4Address
        The IP address that will be configured for the domain controller. This address will be used to
        uniquely identify the domain controller on the network.

    .PARAMETER IPv4Prefix
        Specifies the subnet mask for the IPv4 address, which defines the size of the network portion
        of the address.

        This value must be between 8 and 30, with 24 being a typical value for most networks.

    .PARAMETER IPv4Gateway
        The IP address of the device (such as a router or switch) that will act as the intermediary
        for communication between devices within the network and external networks.

    .PARAMETER IPv4DNS
        The IP address of the DNS server for the domain controller. For domain controllers, this is
        often an upstream DNS server (8.8.8.8, 1.1.1.1), where external domain names can resolve.

    .INPUTS
        [string]$Hostname
        [string]$IPv4Address
        [int]$IPv4Prefix
        [string]$IPv4Gateway
        [string]$IPv4DNS

    .OUTPUTS

    .EXAMPLE
        Invoke-DomainControllerNetworkSettings -Hostname "DC1" -IPv4Address "192.168.1.10" -IPv4Prefix 24 -IPv4Gateway "192.168.1.1" -IPv4DNS "8.8.8.8"

        This command will configure the network interface of the domain controller, set the hostname
        to `DC1`, configure the IPv4 address to `192.168.1.10`, with a subnet prefix of `24`, the
        gateway `192.168.1.1`, and set the upstream DNS server to Google's DNS IP address (`8.8.8.8`).

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Invoke-DomainControllerNetworkSettings.md
 #>

  param(
    [Parameter(Mandatory = $true)]
    [string]$Hostname,
    [Parameter(Mandatory = $true)]
    [string]$IPv4Address,
    [Parameter(Mandatory = $true)]
    [int]$IPv4Prefix,
    [Parameter(Mandatory = $true)]
    [string]$IPv4Gateway,
    [Parameter(Mandatory = $true)]
    [string]$IPv4DNS
  )
  $ipVariables = @(
    $IPv4Address,
    $IPv4Gateway,
    $IPv4DNS,
    $IPv4Prefix
  )

  try {
    Get-OSTarget
    Get-AdapterCount
    Approve-IPVariable -IpVars $ipVariables
    Add-NetworkConfig
    $networkAdapterPrefix = (Get-NetAdapter).ifIndex
    New-NetIPAddress -InterfaceIndex $networkAdapterPrefix -IPAddress $IPv4Address -PrefixLength $IPv4Prefix -DefaultGateway $IPv4Gateway | Out-Null
    Set-DNSClientServerAddress -interfaceIndex $networkAdapterPrefix -ServerAddresses $IPv4DNS | Out-Null
    if ($Hostname -ne $env:ComputerName) {
      Rename-Computer -NewName $Hostname -Force -WarningAction SilentlyContinue
    }
    # output message here
  }
  catch {
    throw "Error completing this script: $($_.Exception.Message)"
  }
}

function Install-NewAdForestAndPromote {
  <#
    .SYNOPSIS
        Installs a new Active Directory Domain Services forest with the specified domain name,
        NetBIOS name, and operating system mode.

    .DESCRIPTION
        This function installs and configures a new Active Directory Domain Services (AD DS) forest.

        It takes the following parameters: `DomainName`, `NetBiosName`, and `Mode`. The function
        checks that the system is running Windows Server, validates the domain and NetBIOS names,
        verifies that the provided operating system mode is supported, and installs the necessary
        features to configure AD DS.

    .PARAMETER DomainName
        The Fully Qualified Domain Name (FQDN) for the new domain in the forest. This should be in
        the format of a valid FQDN (e.g., "example.com").

        The function validates the domain name to ensure it matches the correct format.

    .PARAMETER NetBiosName
        The NetBIOS name for the domain, which must be a single-word name without spaces or special
        characters. This name is used for backward compatibility in networking.

    .PARAMETER Mode
        The operating system mode to use for the new domain forest.
        Supported values are: `Win2008`, `Win2008R2`, `Win2012`, `Win2012R2`, `WinThreshold`, `Default`.
        This parameter determines the version of Active Directory to be used in the forest.

    .INPUTS
        [string]$DomainName
        [string]$NetBiosName
        [string]$Mode

    .OUTPUTS

    .EXAMPLE
        Install-New-AdForestAndPromote -DomainName "example.com" -NetBiosName "EXAMPLE" -Mode "Win2012"

        This command will install a new Active Directory Domain Services forest with the domain
        "example.com" and the NetBIOS name "EXAMPLE" using the "Win2016" AD mode.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Install-NewAdForestAndPromote.md
    #>

  param(
    [Parameter(Mandatory = $true)]
    [string]$DomainName,
    [Parameter(Mandatory = $true)]
    [string]$NetBiosName,
    [Parameter(Mandatory = $true)]
    [string]$Mode
  )
  try {
    Get-OSTarget
    Approve-DomainVariables -Domain $DomainName -NetBios $NetBiosName -ModeType $Mode
    $password = Read-Host 'Enter Safe Mode Administrator password' -AsSecureString
    Install-WindowsFeature AD-Domain-Services -IncludeAllSubFeature -IncludeManagementTools -Confirm:$false -Verbose:$false | Out-Null
    Import-Module ADDSDeployment
    $forestProperties = @{
      DomainName                    = $domainName
      DomainNetbiosName             = $netBIOSname
      ForestMode                    = $Mode
      DomainMode                    = $Mode
      CreateDnsDelegation           = $false
      InstallDns                    = $true
      DatabasePath                  = 'C:\Windows\NTDS'
      LogPath                       = 'C:\Windows\NTDS'
      SysvolPath                    = 'C:\Windows\SYSVOL'
      NoRebootOnCompletion          = $true
      Force                         = $true
      SafeModeAdministratorPassword = $password
    }
    Install-ADDSForest @forestProperties -Confirm:$false -Verbose:$false | Out-Null
  }
  catch {
    throw "Installation of New Forest Failed: $($_.Exception.Message)"
  }
}

function Install-DnsService {
  <#
    .SYNOPSIS
        This function installs DNS service and other features, configures DNS server zones,
        and sets up time synchronization.

    .DESCRIPTION
        The `Install-DnsService` function installs the required DNS server features and tools,
        configures the DNS server settings, and sets up Active Directory replication. It also
        configures automatic DNS scavenging, aging for DNS zones, and time synchronization using
        an NTP server.

        This function assumes the system is running Windows Server and has internet access to
        download required features. It also assumes the presence of an Active Directory environment
        for configuring replication sites and subnets.

    .PARAMETER SiteName
        The name of the Active Directory site to be configured. This name will be used to rename
        the default site.

    .PARAMETER SiteLocation
        The location associated with the Active Directory site. This is used when creating a new
        replication subnet for the site.

    .INPUTS
        [string]$SiteName
        [string]$SiteLocation

    .OUTPUTS

    .EXAMPLE
        Install-DnsService -SiteName "NewYork" -SiteLocation "NYC Datacenter"

        This command installs the DNS service, configures the Active Directory site to "NewYork",
        and sets the location to "NYC Datacenter". It also configures DNS scavenging, aging, and
        time synchronization.

    .EXAMPLE
        $result = Install-DnsService -SiteName "London" -SiteLocation "London Datacenter"

        This stores the result of the DNS installation and configuration into the `$result` variable.
        However, the function does not return output, so `$result` will be `$null`.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Install-DnsService.md
  #>

  param(
    [Parameter(Mandatory = $true)]
    [string]$SiteName,
    [Parameter(Mandatory = $true)]
    [string]$SiteLocation
  )
  try {
    Add-WindowsCapability -Online -Name Rsat.Dns.Tools~~~~0.0.1.0 | Out-Null
    Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 | Out-Null
    Install-WindowsFeature -Name DNS -IncludeManagementTools -IncludeAllSubFeature -Confirm:$false -Verbose:$false | Out-Null
    Install-WindowsFeature -Name RSAT-AD-PowerShell -IncludeAllSubFeature -IncludeManagementTools -Confirm:$false -Verbose:$false | Out-Null
    Import-Module -Name ActiveDirectory -Force
    Import-Module -Name DnsServer -Force

    $IPv4netID = Get-IPv4NetworkID
    Add-DNSServerPrimaryZone -NetworkID $IPv4netID -ReplicationScope 'Forest' -DynamicUpdate 'Secure'
    $defaultSite = (Get-ADReplicationSite | Select-Object DistinguishedName).DistinguishedName
    Rename-ADObject $defaultSite -NewName $SiteName
    New-ADReplicationSubnet -Name $IPv4netID -site $SiteName -Location $SiteLocation
    Register-DnsClient
    Set-DnsServerScavenging -ScavengingState $True -ScavengingInterval 7:00:00:00 -ApplyOnAllZones
    $Zones = Get-DnsServerZone | Where-Object { $_.IsAutoCreated -eq $False -and $_.ZoneName -ne 'TrustAnchors' }
    $Zones | Set-DnsServerZoneAging -Aging $True
    $timePeerList = '0.us.pool.ntp.org 1.us.pool.ntp.org'
    w32tm /config /manualpeerlist:$timePeerList /syncfromflags:manual /reliable:yes /update | Out-Null
  }
  catch {
    throw "Error completing this script: $($_.Exception.Message)"
  }
}

function Add-NewDomainController {
  <#
    .SYNOPSIS
        Installs and configures a new domain controller in an existing Active Directory domain.

    .DESCRIPTION
        The `Add-NewDomainController` function installs the required features for Active Directory Domain Services,
        DNS, and RSAT (Remote Server Administration Tools). It then uses the provided credentials to join a machine
        to an existing Active Directory domain and configure it as a new domain controller.

        The function also gathers domain information, prepares the necessary settings (such as database paths, log
        paths, and replication settings), and runs the `Install-ADDSDomainController` cmdlet to configure the server
        as a domain controller.

        This function assumes the machine is part of an existing Active Directory forest and has internet access to
        download required features. The user running the script must have the necessary administrative privileges to
        install Active Directory Domain Services and configure DNS.

    .PARAMETER DomainName
        The name of the domain in which the new domain controller will be installed.

    .PARAMETER SiteName
        The name of the Active Directory site where the new domain controller will be located.

    .PARAMETER DomainCredential
        The credentials used to join the domain and promote the server to a domain controller.

    .INPUTS

    .OUTPUTS

    .EXAMPLE
        Add-NewDomainController

        This command installs the required features, prompts for user credentials, and configures the current server as
        a new domain controller in the existing Active Directory domain.

    .EXAMPLE
        $domainController = Add-NewDomainController

        This stores the result of the domain controller promotion into the `$domainController` variable, but since the
        function doesn't return output, `$domainController` will be `$null`.

    .NOTES
        - Author: Michael Free (c) 2024
        - Website: https://github.com/Michael-Free
        - Social: https://mastodon.social/@MichaelFree

    .LINK
        https://github.com/Michael-Free/PSCreateADForest/Docs/Add-NewDomainController.md
  #>

  try {
    $currentUsername = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    $password = Read-Host -AsSecureString "Enter password for $currentUsername"
    # Maybe add safe mode password here somewhere?
    $domainCredential = New-Object System.Management.Automation.PSCredential ($currentUsername, $password)
    Install-WindowsFeature AD-Domain-Services -IncludeAllSubFeature -IncludeManagementTools -Confirm:$false -Verbose:$false | Out-Null

    Add-WindowsCapability -Online -Name Rsat.Dns.Tools~~~~0.0.1.0 | Out-Null
    Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 | Out-Null
    Install-WindowsFeature -Name DNS -IncludeManagementTools -IncludeAllSubFeature -Confirm:$false -Verbose:$false | Out-Null
    Install-WindowsFeature -Name RSAT-AD-PowerShell -IncludeAllSubFeature -IncludeManagementTools -Confirm:$false -Verbose:$false | Out-Null

    Import-Module ADDSDeployment
    $domainDetails = Get-DomainInfo
    $forestProperties = @{
      DomainName              = $domainDetails.DomainName
      SiteName                = $domainDetails.SiteName
      CreateDnsDelegation     = $false
      InstallDns              = $true
      DatabasePath            = 'C:\Windows\NTDS'
      LogPath                 = 'C:\Windows\NTDS'
      SysvolPath              = 'C:\Windows\SYSVOL'
      NoRebootOnCompletion    = $true
      Force                   = $true
      Credential              = $domainCredential
      CriticalReplicationOnly = $false
      NoGlobalCatalog         = $false
      ReplicationSourceDC     = $domainDetails.HostName
      #SafeModeAdministratorPassword = $password
    }
    Install-ADDSDomainController @forestProperties
  }
  catch {
    throw "Error completing this script: $($_.Exception.Message)"
  }
}