commit 0cf09c5ff96f38c327a1775fdb3310e5a2dee05b Author: djpbessems Date: Sat Jan 23 16:04:42 2021 +0100 Delete commit history (containing proprietary code) diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..25a743c --- /dev/null +++ b/.drone.yml @@ -0,0 +1,37 @@ +kind: pipeline +type: kubernetes +name: 'Packer Build' + +steps: +- name: Active Directory Domain Services + image: bv11-cr01.bessems.eu/library/packer-extended + commands: + - | + packer validate \ + -var-file=packer/variables.vsphere.json \ + -var vm_name=${DRONE_COMMIT_SHA:0:10}-$DRONE_BUILD_NUMBER \ + -var vsphere_password=$${VSPHERE_PASSWORD} \ + -var winrm_password=$${WINRM_PASSWORD} \ + packer/adds.json + - | + packer build \ + -on-error=cleanup \ + -var-file=packer/variables.vsphere.json \ + -var vm_name=${DRONE_COMMIT_SHA:0:10}-$DRONE_BUILD_NUMBER \ + -var vsphere_password=$${VSPHERE_PASSWORD} \ + -var winrm_password=$${WINRM_PASSWORD} \ + packer/adds.json + environment: + VSPHERE_PASSWORD: + from_secret: vsphere_password + WINRM_PASSWORD: + from_secret: winrm_password + # PACKER_LOG: 1 + volumes: + - name: output + path: /output + +volumes: +- name: output + claim: + name: flexvolsmb-drone-output \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..198852d --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Packer.Images [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg)](https://ci.spamasaurus.com/djpbessems/Packer.Images) + diff --git a/packer/adds.json b/packer/adds.json new file mode 100644 index 0000000..cdb1259 --- /dev/null +++ b/packer/adds.json @@ -0,0 +1,90 @@ +{ + "builders": [ + { + "type": "vsphere-clone", + "name": "adds", + + "vcenter_server": "{{user `vcenter_server`}}", + "username": "{{user `vsphere_username`}}", + "password": "{{user `vsphere_password`}}", + "insecure_connection": "true", + + "vm_name": "adds-{{user `vm_name`}}", + "datastore": "{{user `vsphere_datastore`}}", + "folder": "{{user `vsphere_folder`}}", + "datacenter": "{{user `vsphere_datacenter`}}", + "host": "{{user `vsphere_host`}}", + "boot_order": "disk,cdrom", + + "communicator": "winrm", + "winrm_username": "administrator", + "winrm_password": "{{user `winrm_password`}}", + "winrm_timeout": "10m", + + "cpus": 2, + "RAM": 8192, + + "template": "Windows-Server-2019-LTSC", + + "floppy_files": [ + "packer/preseed/ADDS/Sysprep_Unattend.xml" + ], + + "boot_command": "", + "boot_wait": "2m30s", + + "shutdown_command": "C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /oobe /unattend:A:\\Sysprep_Unattend.xml", + "shutdown_timeout": "1h", + + "export": { + "images": false + } + } + ], + "provisioners": [ + { + "type": "powershell", + "inline": [ + "New-Item -Path 'C:\\Payload\\Scripts' -ItemType 'Directory' -Force:$True -Confirm:$False" + ] + }, + { + "type": "file", + "source": "scripts/ADDS/payload/", + "destination": "C:\\Payload\\" + }, + { + "type": "powershell", + "scripts": [ + "scripts/ADDS/Install-Prerequisites.ps1", + "scripts/ADDS/Register-ScheduledTask.ps1" + ] + } + ], + "post-processors": [[ + { + "type": "shell-local", + "inline": [ + "pwsh -file scripts/Update-OvfConfiguration.ps1 \\", + " -OVFFile './output-adds/adds-{{user `vm_name`}}.ovf'", + "pwsh -file scripts/Update-Manifest.ps1 \\", + " -ManifestFileName './output-adds/adds-{{user `vm_name`}}.mf'", + "ovftool --acceptAllEulas --allowExtraConfig --overwrite \\", + " './output-adds/adds-{{user `vm_name`}}.ovf' \\", + " /output/ADDS-appliance.ova" + ] + } + ], + [ + { + "type": "shell-local", + "inline": [ + "pwsh -file scripts/Remove-Resources.ps1 \\", + " -VMName 'adds-{{user `vm_name`}}' \\", + " -VSphereFQDN '{{user `vcenter_server`}}' \\", + " -VSphereUsername '{{user `vsphere_username`}}' \\", + " -VSpherePassword '{{user `vsphere_password`}}'" + ] + } + ]] +} diff --git a/packer/preseed/ADDS/Sysprep_Unattend.xml b/packer/preseed/ADDS/Sysprep_Unattend.xml new file mode 100644 index 0000000..c0d5cf8 --- /dev/null +++ b/packer/preseed/ADDS/Sysprep_Unattend.xml @@ -0,0 +1,27 @@ + + + + + 1 + + + true + true + + + + + + true + true + true + true + true + Work + 1 + true + true + + + + \ No newline at end of file diff --git a/packer/variables.vsphere.json b/packer/variables.vsphere.json new file mode 100644 index 0000000..cd6c507 --- /dev/null +++ b/packer/variables.vsphere.json @@ -0,0 +1,11 @@ +{ + "vcenter_server": "bv11-vc01.bessems.lan", + "vsphere_username": "administrator@vsphere.local", + "vsphere_datacenter": "DeSchakel", + "vsphere_host": "bv11-esx.bessems.eu", + "vsphere_hostip": "192.168.11.200", + "vsphere_datastore": "Datastore02.SSD", + "vsphere_folder": "/Packer", + "vsphere_templatefolder": "/Templates", + "vsphere_network": "LAN" +} \ No newline at end of file diff --git a/scripts/ADDS/Install-Prerequisites.ps1 b/scripts/ADDS/Install-Prerequisites.ps1 new file mode 100644 index 0000000..0a1f63d --- /dev/null +++ b/scripts/ADDS/Install-Prerequisites.ps1 @@ -0,0 +1,50 @@ +[CmdletBinding()] +Param( + # No parameters +) + +$InstallWindowsFeatureSplat = @{ + Name = 'AD-Domain-Services', 'DHCP', 'RSAT-DNS-Server' + IncludeAllSubFeature = $True + IncludeManagementTools = $True + Restart = $False + Confirm = $False +} +Install-WindowsFeature @InstallWindowsFeatureSplat + +$InstallPackageProviderSplat = @{ + Name = 'NuGet' + MinimumVersion = '2.8.5.201' + Force = $True + Confirm = $False +} +Install-PackageProvider @InstallPackageProviderSplat +$SetPSRepositorySplat = @{ + Name = 'PSGallery' + InstallationPolicy = 'Trusted' +} +Set-PSRepository @SetPSRepositorySplat +$InstallModuleSplat = @{ + Name = 'powershell-yaml','gpwmifilter' + Force = $True + Confirm = $False +} +Install-Module @InstallModuleSplat +$SetPSRepositorySplat = @{ + Name = 'PSGallery' + InstallationPolicy = 'Untrusted' +} +Set-PSRepository @SetPSRepositorySplat + +# Double check whether the required PowerShell modules are available +$RequiredModules = @( + 'powershell-yaml', # Provides cmdlets 'ConvertTo-Yaml' and 'ConvertFrom-Yaml' + 'gpwmifilter', # Provides cmdlets '*-GPWmiFilter' and '*-GPWmiFilterAssignment' + 'psframework' # Dependency for GMWmiFilter +) +ForEach ($Module in $RequiredModules) { + If ([boolean](Get-Module -Name $Module -ListAvailable) -ne $True) { + Write-Error -Message "Missing PowerShell module '$($Module)'" + Exit 1 + } +} \ No newline at end of file diff --git a/scripts/ADDS/Register-ScheduledTask.ps1 b/scripts/ADDS/Register-ScheduledTask.ps1 new file mode 100644 index 0000000..57b7986 --- /dev/null +++ b/scripts/ADDS/Register-ScheduledTask.ps1 @@ -0,0 +1,7 @@ +[CmdletBinding()] +Param( + # No parameters +) + +# Create scheduled task +& schtasks.exe /Create /TN 'OVF-Properties' /SC ONSTART /RU SYSTEM /TR "powershell.exe -file C:\Payload\Apply-OVFProperties.ps1" \ No newline at end of file diff --git a/scripts/ADDS/payload/Apply-OVFProperties.ps1 b/scripts/ADDS/payload/Apply-OVFProperties.ps1 new file mode 100644 index 0000000..fb0dc71 --- /dev/null +++ b/scripts/ADDS/payload/Apply-OVFProperties.ps1 @@ -0,0 +1,269 @@ +#Requires -Modules 'ADDSDeployment' +[CmdletBinding()] +Param( + # No parameters +) + +$NewEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + ErrorAction = 'SilentlyContinue' +} +New-EventLog @NewEventLogSplat +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 1 + Message = 'OVF-Properties sequence initiated' +} +Write-EventLog @WriteEventLogSplat + +$VMwareToolsExecutable = "C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" + +[xml]$ovfEnv = & $VMwareToolsExecutable --cmd "info-get guestinfo.ovfEnv" | Out-String +$ovfProperties = $ovfEnv.ChildNodes.NextSibling.PropertySection.Property + +$ovfPropertyValues = @{} +foreach ($ovfProperty in $ovfProperties) { + $ovfPropertyValues[$ovfProperty.key] = $ovfProperty.Value +} + +# Check for mandatory values +If (!($ovfPropertyValues['guestinfo.hostname'] -and + $ovfPropertyValues['guestinfo.ipaddress'] -and + $ovfPropertyValues['guestinfo.dnsserver'] -and + $ovfPropertyValues['guestinfo.prefixlength'] -and + $ovfPropertyValues['guestinfo.gateway'] -and + $ovfPropertyValues['addsconfig.domainname'] -and + $ovfPropertyValues['addsconfig.netbiosname'] -and + $ovfPropertyValues['addsconfig.administratorpw'] -and + $ovfPropertyValues['addsconfig.safemodepw'])) { + # Mandatory values missing, cannot provision. + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Error' + EventID = 66 + Message = 'Mandatory values missing, cannot provision.' + } + Write-EventLog @WriteEventLogSplat + & schtasks.exe /Change /TN 'OVF-Properties' /DISABLE + Stop-Computer -Force + Exit +} + +# Set hostname and description +If ($Env:ComputerName -ne $ovfPropertyValues['guestinfo.hostname']) { + $RenameComputerSplat = @{ + NewName = $ovfPropertyValues['guestinfo.hostname'] + Force = $True + Confirm = $False + } + Rename-Computer @RenameComputerSplat + $SetCimInstanceSplat = @{ + InputObject = (Get-CimInstance -ClassName 'Win32_OperatingSystem') + Property = @{ + Description = $ovfPropertyValues['guestinfo.hostname'] + } + } + Set-CimInstance @SetCimInstanceSplat + + # Restart the computer to apply changes + Restart-Computer -Force + Exit +} + +# Configure network interface +If ((Get-WmiObject -Class 'Win32_NetworkAdapterConfiguration').IPAddress -NotContains $ovfPropertyValues['guestinfo.ipaddress']) { + $NewNetIPAddressSplat = @{ + InterfaceAlias = (Get-NetAdapter).Name + AddressFamily = 'IPv4' + IPAddress = $ovfPropertyValues['guestinfo.ipaddress'] + PrefixLength = $ovfPropertyValues['guestinfo.prefixlength'] + DefaultGateway = $ovfPropertyValues['guestinfo.gateway'] + } + New-NetIPAddress @NewNetIPAddressSplat + + # Wait for network connection to become available + $Timestamp, $TimeoutMinutes = (Get-Date), 5 + Do { + If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Warning' + EventID = 13 + Message = "Timeout after $($TimeoutMinutes) minutes waiting for network connection to become available." + } + Write-EventLog @WriteEventLogSplat + Break + } + + Start-Sleep -Milliseconds 250 + + $GetNetIPAddressSplat = @{ + InterfaceAlias = (Get-NetAdapter).Name + AddressFamily = 'IPv4' + ErrorAction = 'SilentlyContinue' + } + } Until ((Get-NetIPAddress @GetNetIPAddressSplat).AddressState -eq 'Preferred') + + $OldErrorActionPreference, $ErrorActionPreference = $ErrorActionPreference, 'SilentlyContinue' + $TestNetConnectionSplat = @{ + ComputerName = ([IPAddress]$ovfPropertyValues['guestinfo.dnsserver']).IPAddressToString + InformationLevel = 'Quiet' + } + $SetDnsClientServerAddressSplat = @{ + InterfaceAlias = (Get-NetAdapter).Name + ServerAddresses = If ( + [boolean]($ovfPropertyValues['guestinfo.dnsserver'] -as [IPaddress]) -and (Test-NetConnection @TestNetConnectionSplat)) { + ($ovfPropertyValues['guestinfo.dnsserver']) + } else { + ('127.0.0.1') + } + Validate = $False + } + Set-DnsClientServerAddress @SetDnsClientServerAddressSplat + $ErrorActionPreference, $OldErrorActionPreference = $OldErrorActionPreference, $NULL +} + +# Promote to Domain Controller +If ((4,5) -NotContains (Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole) { + # Change password of built-in Administrator + $BuiltinAdministrator = (Get-LocalUser | Where-Object {$_.SID -match '-500'}) + $ConvertToSecureStringSplat = @{ + String = $ovfPropertyValues['addsconfig.administratorpw'] + AsPlainText = $True + Force = $True + } + $SetLocalUserSplat = @{ + InputObject = $BuiltinAdministrator + Password = ConvertTo-SecureString @ConvertToSecureStringSplat + PasswordNeverExpires = $True + AccountNeverExpires = $True + ### This setting is not allowed on the last administrator + #UserMayChangePassword = $False + Confirm = $False + } + Set-LocalUser @SetLocalUserSplat + + $ResolveDNSNameSplat = @{ + Name = "_ldap._tcp.dc._msdcs.$($ovfPropertyValues['addsconfig.domainname'])" + ErrorAction = 'SilentlyContinue' + } + $DNSRecord = Resolve-DnsName @ResolveDNSNameSplat + If ([boolean]$DNSRecord.PrimaryServer -eq $False) { + # No Primary Domain Controller found, installing as primary + $InstallADDSForestSplat = @{ + DomainName = $ovfPropertyValues['addsconfig.domainname'] + DomainNetbiosName = $ovfPropertyValues['addsconfig.netbiosname'] + SafeModeAdministratorPassword = ConvertTo-SecureString $ovfPropertyValues['addsconfig.safemodepw'] -AsPlainText -Force + InstallDns = $True + DomainMode = 'WinThreshold' + ForestMode = 'WinThreshold' + Confirm = $False + Force = $True + ErrorAction = 'Stop' + } + Try { + Install-ADDSForest @InstallADDSForestSplat + + # Previous cmdlet performs a reboot on completion; so these are commented out + # Restart-Computer -Force + # Exit + } + Catch { + & schtasks.exe /Change /TN 'OVF-Properties' /DISABLE + Stop-Computer -Force + Exit + } + } + Else { + # Primary Domain Controller is present, installing as secondary + $InstallADDSDomainControllerSplat = @{ + DomainName = $ovfPropertyValues['addsconfig.domainname'] + Credential = New-Object System.Management.Automation.PSCredential("$($ovfPropertyValues['addsconfig.netbiosname'])\$($BuiltinAdministrator.Name)", (ConvertTo-SecureString @ConvertToSecureStringSplat)) + SafeModeAdministratorPassword = ConvertTo-SecureString $ovfPropertyValues['addsconfig.safemodepw'] -AsPlainText -Force + InstallDns = $True + Confirm = $False + Force = $True + ErrorAction = 'Stop' + } + Try { + Install-ADDSDomainController @InstallADDSDomainControllerSplat + + # Previous cmdlet performs a reboot on completion; so these are commented out + # Restart-Computer -Force + # Exit + } + Catch { + & schtasks.exe /Change /TN 'OVF-Properties' /DISABLE + Stop-Computer -Force + Exit + } + } +} + +# Wait for Active Directory to become available +$Timestamp, $TimeoutMinutes = (Get-Date), 15 +Do { + If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Warning' + EventID = 13 + Message = "Timeout after $($TimeoutMinutes) minutes waiting for Active Directory to become available." + } + Write-EventLog @WriteEventLogSplat + Break + } + + Start-Sleep -Seconds 30 + + $GetADComputerSplat = @{ + Identity = $Env:ComputerName + ErrorAction = 'SilentlyContinue' + } + Get-ADComputer @GetADComputerSplat | Out-Null +} Until ($?) + +# Iterate through and invoke all payload scripts +#! TODO: add registry values to determine which scripts have already been invoked (in case of intermediate reboots) +$GetItemSplat = @{ + Path = "$($PSScriptRoot)\Scripts\*.ps1" +} +Get-Item @GetItemSplat | ForEach-Object { + Try { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 4 + Message = "Running script: '$($_.FullName)'" + } + Write-EventLog @WriteEventLogSplat + & $_.FullName -Parameter $ovfPropertyValues + } + Catch { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Error' + EventID = 66 + Message = $_.Exception.Message + } + Write-EventLog @WriteEventLogSplat + } +} + +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 42 + Message = 'OVF-Properties sequence applied and finished' +} +Write-EventLog @WriteEventLogSplat +& schtasks.exe /Change /TN 'OVF-Properties' /DISABLE diff --git a/scripts/ADDS/payload/scripts/01.Organizational units.csv b/scripts/ADDS/payload/scripts/01.Organizational units.csv new file mode 100644 index 0000000..8f152e7 --- /dev/null +++ b/scripts/ADDS/payload/scripts/01.Organizational units.csv @@ -0,0 +1,16 @@ +"DistinguishedName","Description" +"OU=Computer accounts","" +"OU=Clients,OU=Computer accounts","" +"OU=Desktops,OU=Clients,OU=Computer accounts","" +"OU=Laptops,OU=Clients,OU=Computer accounts","" +"OU=Servers,OU=Computer accounts","" +"OU=Groups","" +"OU=Resources,OU=Groups","" +"OU=Roles,OU=Groups","" +"OU=User accounts","" +"OU=Privileged,OU=User accounts","" +"OU=Administrators,OU=Privileged,OU=User accounts","" +"OU=Service accounts,OU=Privileged,OU=User accounts","" +"OU=Non-privileged,OU=User accounts","" +"OU=Employees,OU=Non-privileged,OU=User accounts","" +"OU=Contractors,OU=Non-privileged,OU=User accounts","" diff --git a/scripts/ADDS/payload/scripts/01.Organizational units.ps1 b/scripts/ADDS/payload/scripts/01.Organizational units.ps1 new file mode 100644 index 0000000..692ef1c --- /dev/null +++ b/scripts/ADDS/payload/scripts/01.Organizational units.ps1 @@ -0,0 +1,29 @@ +#Requires -Modules 'ActiveDirectory' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".csv") + } + $CSVImport = (Get-Content @GetContentSplat) | ConvertFrom-Csv + + ForEach ($OU in $CSVImport) { + $OUName, $OUPath = $OU.DistinguishedName -split ',', 2 + If ($OUPath.Length -ne 0) { + $OUPath += ',' + } + + $NewADOrganizationalUnitSplat = @{ + Name = $OUName.Substring(3) + Path = $OUPath + 'DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + Description = $OU.Description + ProtectedFromAccidentalDeletion = $False + ErrorAction = 'SilentlyContinue' + } + New-ADOrganizationalUnit @NewADOrganizationalUnitSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/02.Groups.csv b/scripts/ADDS/payload/scripts/02.Groups.csv new file mode 100644 index 0000000..f657c84 --- /dev/null +++ b/scripts/ADDS/payload/scripts/02.Groups.csv @@ -0,0 +1,6 @@ +"DistinguishedName","Description" +"CN=RemoteDesktop - Management servers,OU=Resources,OU=Groups","" +"CN=ContentLibraryAdmin - vSphere servers,OU=Resources,OU=Groups","" +"CN=DatastoreAdmin - vSphere servers,OU=Resources,OU=Groups","" +"CN=Hypervisor administrators,OU=Roles,OU=Groups","" +"CN=Firewall administrators,OU=Roles,OU=Groups","" diff --git a/scripts/ADDS/payload/scripts/02.Groups.ps1 b/scripts/ADDS/payload/scripts/02.Groups.ps1 new file mode 100644 index 0000000..0525525 --- /dev/null +++ b/scripts/ADDS/payload/scripts/02.Groups.ps1 @@ -0,0 +1,25 @@ +#Requires -Modules 'ActiveDirectory' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".csv") + } + $CSVImport = (Get-Content @GetContentSplat) | ConvertFrom-Csv + + ForEach ($Group in $CSVImport) { + $NewADGroupSplat = @{ + Name = ($Group.DistinguishedName -split ',', 2)[0].Substring(3) + Path = ($Group.DistinguishedName -split ',', 2)[1] + ',DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + Description = $Group.Description + GroupCategory = 'Security' + GroupScope = 'Global' + ErrorAction = 'SilentlyContinue' + } + New-ADGroup @NewADGroupSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/03.Users.csv b/scripts/ADDS/payload/scripts/03.Users.csv new file mode 100644 index 0000000..86eeca4 --- /dev/null +++ b/scripts/ADDS/payload/scripts/03.Users.csv @@ -0,0 +1,5 @@ +"DistinguishedName","Password","MemberOf" +"CN=Jane Doe,OU=Employees,OU=Non-privileged,OU=User accounts","Complex42!","" +"CN=John Doe,OU=Contractors,OU=Non-privileged,OU=User accounts","Complex42!","" +"CN=admJaneD,OU=Administrators,OU=Privileged,OU=User accounts","Complex42!","" +"CN=zzLDAP,OU=Service accounts,OU=Privileged,OU=User accounts","Complex42!","" diff --git a/scripts/ADDS/payload/scripts/03.Users.ps1 b/scripts/ADDS/payload/scripts/03.Users.ps1 new file mode 100644 index 0000000..ff2eed4 --- /dev/null +++ b/scripts/ADDS/payload/scripts/03.Users.ps1 @@ -0,0 +1,42 @@ +#Requires -Modules 'ActiveDirectory' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".csv") + } + $CSVImport = (Get-Content @GetContentSplat) | ConvertFrom-Csv + + ForEach ($User in $CSVImport) { + # Create new user + $NewADUserSplat = @{ + Name = ($User.DistinguishedName -split ',', 2)[0].Substring(3) + Path = ($User.DistinguishedName -split ',', 2)[1] + ',DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + AccountPassword = ConvertTo-SecureString $User.Password -AsPlainText -Force + PassThru = $True + ErrorAction = 'SilentlyContinue' + } + $NewADUser = New-ADUser @NewADUserSplat + # Add user to group(s) + If ($User.MemberOf -ne '') { + ForEach ($Group in $User.MemberOf.Split('|')) { + $AddADGroupMemberSplat = @{ + Identity = $Group + ',DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + Members = $NewADUser.DistinguishedName + ErrorAction = 'SilentlyContinue' + } + Add-ADGroupMember @AddADGroupMemberSplat + } + } + # Enable user + $EnableADAccountSplat = @{ + Identity = $NewADUser.DistinguishedName + ErrorAction = 'Continue' + } + Enable-ADAccount @EnableADAccountSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/04.Delegation of Control.ps1 b/scripts/ADDS/payload/scripts/04.Delegation of Control.ps1 new file mode 100644 index 0000000..c0ea438 --- /dev/null +++ b/scripts/ADDS/payload/scripts/04.Delegation of Control.ps1 @@ -0,0 +1,126 @@ +#Requires -Modules 'ActiveDirectory' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $PSDrive = Get-PSDrive -Name 'AD' + If ([boolean]$PSDrive -eq $False) { + $NewPSDriveSplat = @{ + Name = 'ADDS' + Root = '' + PSProvider = 'ActiveDirectory' + } + $PSDrive = New-PSDrive @NewPSDriveSplat + } + + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml") + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + + # Check if the respective .yml file declared substitutions which need to be parsed + If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $Delegations = $YamlDocuments[0..($YamlDocuments.Count - 2)] + } + Else { + $Delegations = $YamlDocuments + } + + # Store GUIDs for all known AD schema classes + $GUIDMap, $GetADObjectSplat = @{}, @{ + SearchBase = (Get-ADRootDSE).SchemaNamingContext + LDAPFilter = '(schemaidguid=*)' + Properties = 'lDAPDisplayName','schemaIDGUID' + } + Get-ADObject @GetADObjectSplat | ForEach-Object { + $GUIDMap[$_.lDAPDisplayName] = [GUID]$_.schemaIDGUID + } + # Store GUIDs for all extended rights + $GetADObjectSplat = @{ + SearchBase = (Get-ADRootDSE).ConfigurationNamingContext + LDAPFilter = '(&(objectclass=controlAccessRight)(rightsguid=*))' + Properties = 'displayName','rightsGuid' + } + Get-ADObject @GetADObjectSplat | ForEach-Object { + $GUIDMap[$_.displayName] = [GUID]$_.rightsGuid + } + $GUIDMap['null'] = [Guid]::Empty + + ForEach ($Entry in $Delegations.DelegationEntries) { + $GetADObjectSplat = @{ + Filter = '*' + SearchBase = 'DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + SearchScope = 'OneLevel' + } + $OU = Get-ADObject @GetADObjectSplat | Where-Object {$_.DistinguishedName -match $Entry.OrganizationalUnit} + $GetACLSPlat = @{ + Path = "$($PSDrive.Name):\$($OU.DistinguishedName)" + } + $ACL = Get-ACL @GetACLSPlat + + $GetADObjectSplat = @{ + Filter = "sAMAccountName -eq '$($Entry.Principal)'" + Properties = 'objectSID' + } + $Principal = Get-ADObject @GetADObjectSplat + + ForEach ($Rule in $Entry.AccessRules) { + If ($Rule.ObjectType -eq '') { + $Rule.ObjectType = 'null' + } + If ($Rule.InheritedObjectType -eq '') { + $Rule.InheritedObjectType = 'null' + } + + $NewACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( + # An IdentityReference object that identifies the trustee of the access rule. + [System.Security.Principal.IdentityReference]$Principal.objectSID, + # A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule. + [System.DirectoryServices.ActiveDirectoryRights]$Rule.ActiveDirectoryRights, + # One of the AccessControlType enumeration values that specifies the access rule type. + [System.Security.AccessControl.AccessControlType]$Rule.AccessControlType, + # The schema GUID of the object to which the access rule applies. + [Guid]$GUIDMap[$Rule.ObjectType], + # One of the ActiveDirectorySecurityInheritance enumeration values that specifies the inheritance type of the access rule. + [System.DirectoryServices.ActiveDirectorySecurityInheritance]$Rule.ActiveDirectorySecurityInheritance, + # The schema GUID of the child object type that can inherit this access rule. + [Guid]$GUIDMap[$Rule.InheritedObjectType] + ) + $ACL.AddAccessRule($NewACE) + } + + $SetAclSplat = @{ + Path = "$($PSDrive.Name):\$($OU.DistinguishedName)" + AclObject = $ACL + ErrorAction = 'Continue' + } + Set-Acl @SetAclSplat + } + + If ([boolean]($PSDrive.Name -eq 'ADDS') -eq $True) { + $RemovePSDriveSplat = @{ + Name = 'ADDS' + Force = $True + Confirm = $False + } + Remove-PSDrive @RemovePSDriveSplat | Out-Null + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/04.Delegation of Control.yml b/scripts/ADDS/payload/scripts/04.Delegation of Control.yml new file mode 100644 index 0000000..c2f448e --- /dev/null +++ b/scripts/ADDS/payload/scripts/04.Delegation of Control.yml @@ -0,0 +1,73 @@ +DelegationEntries: +- Principal: admJaneD + OrganizationalUnit: CN=Computers # Entries will be concatenated with ',DC=,DC=' automatically + AccessRules: + - ActiveDirectoryRights: Self # A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule. + AccessControlType: Allow # One of the AccessControlType enumeration values that specifies the access rule type. + ActiveDirectorySecurityInheritance: Descendents # One of the ActiveDirectorySecurityInheritance enumeration values that specifies the inheritance type of the access rule. + ObjectType: Validated write to DNS host name # The object type to which the access rule applies. + InheritedObjectType: Computer # The child object type that can inherit this access rule. + - ActiveDirectoryRights: Self + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: Validated write to service principal name + InheritedObjectType: Computer + - ActiveDirectoryRights: WriteProperty, WriteDacl + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: '' + InheritedObjectType: Computer + - ActiveDirectoryRights: ExtendedRight + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: Reset Password + InheritedObjectType: Computer + - ActiveDirectoryRights: ExtendedRight + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: Change Password + InheritedObjectType: Computer + - ActiveDirectoryRights: ReadProperty + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: '' + InheritedObjectType: Computer + - ActiveDirectoryRights: WriteProperty + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: '' + InheritedObjectType: Computer + - ActiveDirectoryRights: CreateChild, DeleteChild + AccessControlType: Allow + ActiveDirectorySecurityInheritance: All + ObjectType: Computer + InheritedObjectType: '' + - ActiveDirectoryRights: GenericAll + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: Computer + InheritedObjectType: '' +- Principal: admJaneD + OrganizationalUnit: OU=Clients,OU=Computer accounts + AccessRules: + - ActiveDirectoryRights: CreateChild, DeleteChild + AccessControlType: Allow + ActiveDirectorySecurityInheritance: All + ObjectType: User + InheritedObjectType: '' + - ActiveDirectoryRights: GenericAll + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: '' + InheritedObjectType: '' + - ActiveDirectoryRights: WriteProperty, ReadProperty + AccessControlType: Allow + ActiveDirectorySecurityInheritance: Descendents + ObjectType: Member + InheritedObjectType: Group + +# --- +# Variables: +# - Name: foo +# Expression: | +# Write-Host 'bar' diff --git a/scripts/ADDS/payload/scripts/05.Firewall.ps1 b/scripts/ADDS/payload/scripts/05.Firewall.ps1 new file mode 100644 index 0000000..7d4a897 --- /dev/null +++ b/scripts/ADDS/payload/scripts/05.Firewall.ps1 @@ -0,0 +1,109 @@ +#Requires -Modules 'NetSecurity' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml') + Raw = $true + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + + # Check if the respective .yml file declared substitutions which need to be parsed + If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $Settings = $YamlDocuments[0..($YamlDocuments.Count - 2)] + } + Else { + $Settings = $YamlDocuments + } + + $NewGPOSplat = @{ + Name = 'COMP: Firewall (Servers)' + } + $NewGPO = New-GPO @NewGPOSplat + + $OpenNetGPOSplat = @{ + PolicyStore = "$($Parameter['addsconfig.domainname'])\$($NewGPO.DisplayName)" + } + $GPOSession = Open-NetGPO @OpenNetGPOSplat + + ForEach ($Rule in $Settings.FirewallRules) { + $NewNetFirewallRuleSplat = @{ + # Using so-called string formatting with the '-f' operator (looks more complicated than it is) to create consistent policy names: + # Examples: + # 'DENY: Inbound port 443 (TCP)' + # 'ALLOW: Inbound 'D:\MSSQL\bin\sqlservr.exe' + DisplayName = ("{0}: {1} {2} {3} {4}" -f + $Rule.Action.ToUpper(), + $Rule.Direction, + ("'$($Rule.Program)'", $NULL)[!($Rule.Program)], + ("Port $($Rule.Port)", $NULL)[!($Rule.Port)], + ("($($Rule.Protocol))", $NULL)[!($Rule.Protocol)] + ) -replace '\s+',' ' + Description = $Rule.Description + Action = $Rule.Action + Direction = $Rule.Direction + Program = ($Rule.Program, 'Any')[!($Rule.Program)] + LocalPort = ($Rule.Port.Split(','), 'Any')[!($Rule.Port)] + Protocol = ($Rule.Protocol, 'Any')[!($Rule.Protocol)] + GPOSession = $GPOSession + PolicyStore = $NewGPO.DisplayName + Confirm = $False + } + New-NetFirewallRule @NewNetFirewallRuleSplat + } + + ForEach ($Profile in $Settings.FirewallProfiles) { + $SetNetFirewallProfileSplat = @{ + Name = $Profile.Name + Enabled = $Profile.Enabled + DefaultInboundAction = $Profile.Connections.Inbound + DefaultOutboundAction = $Profile.Connections.Outbound + LogAllowed = $Profile.Logging.LogSuccessfullConnections + LogBlocked = $Profile.Logging.LogDroppedPackets + LogFileName = $Profile.Logging.Name + LogMaxSizeKilobytes = $Profile.Logging.SizeLimit + AllowLocalFirewallRules = $Profile.Settings.ApplyLocalFirewallRules + AllowLocalIPsecRules = $Profile.Settings.ApplyLocalConnectionSecurityRules + NotifyOnListen = $Profile.Settings.DisplayNotification + GPOSession = $GPOSession + PolicyStore = $NewGPO.DisplayName + Confirm = $False + } + Set-NetFirewallProfile @SetNetFirewallProfileSplat + } + + $SaveNetGPOSplat = @{ + GPOSession = $GPOSession + } + Save-NetGPO @SaveNetGPOSplat + + $NewGPLinkSplat = @{ + Name = $NewGPO.DisplayName +# Should probably be configurable through yml + Target = 'OU=Servers,OU=Computer accounts,DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + } + New-GPLink @NewGPLinkSplat + $NewGPLinkSplat = @{ + Name = $NewGPO.DisplayName + Target = 'OU=Domain Controllers,DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + } + New-GPLink @NewGPLinkSplat +} diff --git a/scripts/ADDS/payload/scripts/05.Firewall.yml b/scripts/ADDS/payload/scripts/05.Firewall.yml new file mode 100644 index 0000000..3a70610 --- /dev/null +++ b/scripts/ADDS/payload/scripts/05.Firewall.yml @@ -0,0 +1,62 @@ +FirewallRules: +- Description: Rule A + Action: Block + Direction: Inbound + Program: '' + Port: '21-22,25' + Protocol: TCP +- Description: Rule B + Action: Allow + Direction: Inbound + Program: D:\MSSQL\sqlsvr.exe + Port: '' + Protocol: '' +FirewallProfiles: +- Name: Domain + Enabled: 'True' + Connections: + Inbound: Block + Outbound: Allow + Settings: + DisplayNotification: 'False' + ApplyLocalFirewallRules: 'True' + ApplyLocalConnectionSecurityRules: 'True' + Logging: + Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\domainfw.log' + SizeLimit: 16384 + LogDroppedPackets: 'True' + LogSuccessfullConnections: 'False' +- Name: Private + Enabled: 'True' + Connections: + Inbound: Block + Outbound: Allow + Settings: + DisplayNotification: 'False' + ApplyLocalFirewallRules: 'True' + ApplyLocalConnectionSecurityRules: 'True' + Logging: + Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\privatefw.log' + SizeLimit: 16384 + LogDroppedPackets: 'True' + LogSuccessfullConnections: 'False' +- Name: Public + Enabled: 'True' + Connections: + Inbound: Block + Outbound: Allow + Settings: + DisplayNotification: 'False' + ApplyLocalFirewallRules: 'True' + ApplyLocalConnectionSecurityRules: 'True' + Logging: + Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\publicfw.log' + SizeLimit: 16384 + LogDroppedPackets: 'True' + LogSuccessfullConnections: 'False' + +# --- +# Variables: +# - Name: foo +# Expression: | +# Write-Host 'bar' \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/06.DHCP service.ps1 b/scripts/ADDS/payload/scripts/06.DHCP service.ps1 new file mode 100644 index 0000000..6aa0260 --- /dev/null +++ b/scripts/ADDS/payload/scripts/06.DHCP service.ps1 @@ -0,0 +1,27 @@ +#Requires -Modules 'DhcpServer' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Configure DHCP (if and only if this server is not already an authorized DHCP server) +If ((Get-DHCPServerInDC).IPAddress -NotContains $Parameter['guestinfo.ipaddress']) { + # Add DHCP security groups + & netsh dhcp add securitygroups + + # Authorize DHCP server + $AddDhcpServerInDCSplat = @{ + DnsName = "$($Parameter['guestinfo.hostname']).$($Parameter['addsconfig.domainname'])" + IPAddress = $($Parameter['guestinfo.ipaddress']) + Confirm = $False + } + Add-DhcpServerInDC @AddDhcpServerInDCSplat + + # Notify Server Manager post-install configuration has completed + $SetItemPropertySplat = @{ + Path = 'HKLM:\SOFTWARE\Microsoft\ServerManager\Roles\12' + Name = 'ConfigurationState' + Value = 2 + } + Set-ItemProperty @SetItemPropertySplat +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/07.DHCP scopes.ps1 b/scripts/ADDS/payload/scripts/07.DHCP scopes.ps1 new file mode 100644 index 0000000..bcd906b --- /dev/null +++ b/scripts/ADDS/payload/scripts/07.DHCP scopes.ps1 @@ -0,0 +1,54 @@ +#Requires -Modules 'DhcpServer' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on secondary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 4) { + $AddDhcpServerv4ScopeSplat = @{ + Name = 'Default DHCP scope' + StartRange = [ipaddress]$Parameter['dhcpconfig.startip'] + EndRange = [ipaddress]$Parameter['dhcpconfig.endip'] + SubnetMask = [ipaddress]$Parameter['dhcpconfig.subnetmask'] + LeaseDuration = [timespan]$Parameter['dhcpconfig.leaseduration'] + State = 'Active' + PassThru = $True + Confirm = $False + } + $DhcpScope = Add-DhcpServerv4Scope @AddDhcpServerv4ScopeSplat + + $ScopeOptions = @( + @{ + # 003 Router + OptionId = 3 + Value = $Parameter['dhcpconfig.gateway'] + }, + @{ + # 004 Time Server + OptionId = 4 + Value = (Resolve-DnsName -Name $Parameter['addsconfig.domainname']).IPAddress + }, + @{ + # 006 DNS Server + OptionId = 6 + Value = (Resolve-DnsName -Name $Parameter['addsconfig.domainname']).IPAddress + }, + @{ + # 015 DNS Domain Name + OptionId = 15 + Value = $Parameter['addsconfig.domainname'] + } + ) + + ForEach ($Option in $ScopeOptions) { + $SetDhcpServerv4OptionValueSplat = @{ + ScopeId = $DhcpScope.ScopeId + OptionId = $Option.OptionId + Value = $Option.Value + Force = $True + Confirm = $False + } + Set-DhcpServerv4OptionValue @SetDhcpServerv4OptionValueSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/08.DHCP failover.ps1 b/scripts/ADDS/payload/scripts/08.DHCP failover.ps1 new file mode 100644 index 0000000..b3405bd --- /dev/null +++ b/scripts/ADDS/payload/scripts/08.DHCP failover.ps1 @@ -0,0 +1,43 @@ +#Requires -Modules 'DhcpServer' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on secondary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 4) { + # Wait for secondary DHCP server to be registered in DNS + $Timestamp, $TimeoutMinutes = (Get-Date), 5 + Do { + If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Warning' + EventID = 13 + Message = "Timeout after $($TimeoutMinutes) minutes waiting for secondary Domain Controller to be registered in DNS." + } + Write-EventLog @WriteEventLogSplat + Break + } + + Start-Sleep -Seconds 5 + + } Until ((Get-DhcpServerInDC).Count -gt 1) + + $NewCimSessionSplat = @{ + Credential = New-Object System.Management.Automation.PSCredential( + ###! TODO: Replace this with code to automagically find required accountname (this hardcoded value might not be correct due to GPO's) + (Get-ADUser -Filter * | Where-Object {$_.SID -match '-500'}).SamAccountName, + (ConvertTo-SecureString $Parameter['addsconfig.administratorpw'] -AsPlainText -Force) + ) + } + $AddDhcpServerv4FailoverSplat = @{ + Name = 'Failover #42' + PartnerServer = (Get-DhcpServerInDC).DnsName | Where-Object {$_ -ne "$($Parameter['guestinfo.hostname']).$($Parameter['addsconfig.domainname'])"} + ServerRole = 'Active' + ScopeId = (Get-DhcpServerv4Scope).ScopeId.IPAddressToString + CimSession = New-CimSession @NewCimSessionSplat + } + Add-DhcpServerv4Failover @AddDhcpServerv4FailoverSplat +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/09.DNS records.ps1 b/scripts/ADDS/payload/scripts/09.DNS records.ps1 new file mode 100644 index 0000000..ce4ef60 --- /dev/null +++ b/scripts/ADDS/payload/scripts/09.DNS records.ps1 @@ -0,0 +1,88 @@ +#Requires -Modules 'DnsServer' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on secondary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 4) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml') + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + + # Check if the respective .yml file declared substitutions which need to be parsed + If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $Records = $YamlDocuments[0..($YamlDocuments.Count - 2)] + } + Else { + $Records = $YamlDocuments + } + + ForEach ($Record in $Records.Entries) { + $AddDnsServerResourceRecordSplat = @{ + ComputerName = $Parameter['guestinfo.dnsserver'] + ZoneName = $Parameter['addsconfig.domainname'] + Name = [string]$Record.Name + TimeToLive = (New-TimeSpan -Hours 1) + AgeRecord = $False + Confirm = $False + } + Switch ($Record.Type) { + 'A' { + $AddDnsServerResourceRecordSplat.Add('A', $True) + $AddDnsServerResourceRecordSplat.Add('IPv4Address', $Record.Value) + } + 'AAAA' { + $AddDnsServerResourceRecordSplat.Add('AAAA', $True) + $AddDnsServerResourceRecordSplat.Add('IPv6Address', $Record.Value) + } + 'CNAME' { + $AddDnsServerResourceRecordSplat.Add('CNAME', $True) + $AddDnsServerResourceRecordSplat.Add('HostNameAlias', $Record.Value) + } + 'MX' { + $AddDnsServerResourceRecordSplat.Add('MX', $True) + # Value should match pattern ':' + # ie. 'mail.contoso.com:10' + $MailExch = $Record.Value -split ':' + $AddDnsServerResourceRecordSplat.Add('MailExchange', $MailExch[0]) + $AddDnsServerResourceRecordSplat.Add('Preference', $MailExch[1]) + } + 'NS' { + $AddDnsServerResourceRecordSplat.Add('NS', $True) + $AddDnsServerResourceRecordSplat.Add('NameServer', $Record.Value) + } + 'SRV' { + $AddDnsServerResourceRecordSplat.Add('SRV', $True) + # Value should match pattern ':::' + # ie. 'sipserver.contoso.com:0:0:5060' + $SrvLocator = $Record.Value -split ':' + $AddDnsServerResourceRecordSplat.Add('DomainName', $SrvLocator[0]) + $AddDnsServerResourceRecordSplat.Add('Priority', $SrvLocator[1]) + $AddDnsServerResourceRecordSplat.Add('Weight', $SrvLocator[2]) + $AddDnsServerResourceRecordSplat.Add('Port', $SrvLocator[3]) + } + 'TXT' { + $AddDnsServerResourceRecordSplat.Add('TXT', $True) + $AddDnsServerResourceRecordSplat.Add('DescriptiveText', $Record.Value) + } + } + Add-DnsServerResourceRecord @AddDnsServerResourceRecordSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/09.DNS records.yml b/scripts/ADDS/payload/scripts/09.DNS records.yml new file mode 100644 index 0000000..3857c12 --- /dev/null +++ b/scripts/ADDS/payload/scripts/09.DNS records.yml @@ -0,0 +1,27 @@ +Entries: +- Name: ldap + Type: A + Value: "{{ primarydc }}" +- Name: ldap + Type: A + Value: "{{ secondarydc }}" +- Name: timeserver + Type: A + Value: "{{ primarydc }}" +- Name: timeserver + Type: A + Value: "{{ secondarydc }}" +# - Name: mail +# Type: MX +# Value: mail.contoso.com:10 # Value should match pattern ':' +# - Name: voipserver +# Type: SRV +# Value: sip.contoso.com:0:0:5060 # Value should match pattern ':::' +--- +Variables: +- Name: primarydc + Expression: | + (Resolve-DnsName -Name $Parameter['addsconfig.domainname'] | Sort-Object)[0].IPAddress +- Name: secondarydc + Expression: | + (Resolve-DnsName -Name $Parameter['addsconfig.domainname'] | Sort-Object)[1].IPAddress \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.ps1 b/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.ps1 new file mode 100644 index 0000000..0c0495b --- /dev/null +++ b/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.ps1 @@ -0,0 +1,47 @@ +#Requires -Modules 'GPWmiFilter' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml') + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + + # Check if the respective .yml file declared substitutions which need to be parsed + If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $WmiFilters = $YamlDocuments[0..($YamlDocuments.Count - 2)] + } + Else { + $WmiFilters = $YamlDocuments + } + + ForEach ($Filter in $WmiFilters) { + $NewGPWmiFilterSplat = @{ + Name = $Filter.Name + Description = $Filter.Description + Expression = $Filter.Expressions + Server = $Parameter['addsconfig.domainname'] + ErrorAction = 'SilentlyContinue' + } + New-GPWmiFilter @NewGPWmiFilterSplat + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.yml b/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.yml new file mode 100644 index 0000000..59cd29f --- /dev/null +++ b/scripts/ADDS/payload/scripts/10.Group Policy WMI Filters.yml @@ -0,0 +1,9 @@ +- Name: PDC Emulator + Description: Primary Domain Controller Emulator only + Expressions: + - 'SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5' +# --- +# Variables: +# - Name: foo +# Expression: | +# Write-Host 'bar' \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP.Disable Server Manager.yml b/scripts/ADDS/payload/scripts/11.GPO+GPP.Disable Server Manager.yml new file mode 100644 index 0000000..d53d716 --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP.Disable Server Manager.yml @@ -0,0 +1,9 @@ +Name: 'COMP: Loopback processing (Merge)' +Type: Object +LinkedOUs: OU=Servers,OU=Computer accounts +WMIFilters: [] +RegistryEntries: +- Key: HKLM\Software\Policies\Microsoft\Windows\Server\ServerManager + Type: Dword + ValueName: DoNotOpenAtLogon + Value: 1 diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP.LoopbackProcessing.yml b/scripts/ADDS/payload/scripts/11.GPO+GPP.LoopbackProcessing.yml new file mode 100644 index 0000000..cf511dd --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP.LoopbackProcessing.yml @@ -0,0 +1,19 @@ +Name: 'COMP: Loopback processing (Merge)' +Type: Object +LinkedOUs: [] +WMIFilters: [] +RegistryEntries: +- Key: HKLM\Software\Policies\Microsoft\Windows\System + Type: Dword + ValueName: UserPolicyMode + Value: 1 +--- +Name: 'COMP: Loopback processing (Replace)' +Type: Object +LinkedOUs: [] +WMIFilters: [] +RegistryEntries: +- Key: HKLM\Software\Policies\Microsoft\Windows\System + Type: Dword + ValueName: UserPolicyMode + Value: 2 \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP.PDC Timeserver.yml b/scripts/ADDS/payload/scripts/11.GPO+GPP.PDC Timeserver.yml new file mode 100644 index 0000000..5f0725f --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP.PDC Timeserver.yml @@ -0,0 +1,36 @@ +Name: 'COMP: Timeserver configuration (W32Time)' +Type: Object +LinkedOUs: +- OU=Domain Controllers +WMIFilters: +- PDC Emulator +RegistryEntries: +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Parameters + Type: String + ValueName: + - Type + - NtpServer + Value: + - NTP + - "{{ addsconfig.ntpserver }}" +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config + Type: DWord + ValueName: AnnounceFlags + Value: 0xA +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config + Type: DWord + ValueName: MaxPosPhaseCorrection + Value: 0xFFFFFFFF +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config + Type: DWord + ValueName: MaxNegPhaseCorrection + Value: 0xFFFFFFFF +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer + Type: DWord + ValueName: Enabled + Value: 1 +--- +Variables: +- Name: addsconfig.ntpserver + Expression: | + ($Parameter['addsconfig.ntpserver'] -split ',' | ForEach-Object {'{0},0x1' -f $_}) -join ' ' \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP.Restrict Internet Communication.yml b/scripts/ADDS/payload/scripts/11.GPO+GPP.Restrict Internet Communication.yml new file mode 100644 index 0000000..42fd715 --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP.Restrict Internet Communication.yml @@ -0,0 +1,116 @@ +Name: 'COMP: Restrict Internet Communication' +Type: Object +LinkedOUs: +- OU=Servers +WMIFilters: [] +RegistryEntries: +- Key: HKLM\Software\Policies\Microsoft\InternetManagement + Type: DWord + ValueName: RestrictCommunication + Value: 1 +# All below settings are set such that their respective features cannot access the Internet +# If any of these settings are in conflict with the above setting, gpmc.msc will behave erratic! +- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer + Type: Dword + ValueName: NoPublishingWizard + Value: 1 +- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer + Type: Dword + ValueName: NoWebServices + Value: 1 +- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer + Type: DWord + ValueName: NoOnlinePrintsWizard + Value: 1 +- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer + Type: DWord + ValueName: NoInternetOpenWith + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\EventViewer + Type: DWord + ValueName: MicrosoftEventVwrDisableLinks + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Messenger\Client + Type: DWord + ValueName: CEIP + Value: 2 +- Key: HKLM\Software\Policies\Microsoft\PCHealth\ErrorReporting + Type: DWord + ValueName: DoReport + Value: 0 +- Key: HKLM\Software\Policies\Microsoft\PCHealth\HelpSvc + Type: DWord + ValueName: Headlines + Value: 0 +- Key: HKLM\Software\Policies\Microsoft\PCHealth\HelpSvc + Type: DWord + ValueName: MicrosoftKBSearch + Value: 0 +- Key: HKLM\Software\Policies\Microsoft\SearchCompanion + Type: DWord + ValueName: DisableContentFileUpdates + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\SystemCertificates\AuthRoot + Type: DWord + ValueName: DisableRootAutoUpdate + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\SQMClient\Windows + Type: DWord + ValueName: CEIPEnable + Value: 0 +- Key: HKLM\Software\Policies\Microsoft\Windows\DriverSearching + Type: DWord + ValueName: DontSearchWindowsUpdate + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\HandwritingErrorReports + Type: DWord + ValueName: PreventHandwritingErrorReports + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\Internet Connection Wizard + Type: DWord + ValueName: ExitOnMSICW + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\NetworkConnectivityStatusIndicator + Type: Dword + ValueName: NoActiveProbe + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\Registration Wizard Control + Type: DWord + ValueName: NoRegistration + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\TabletPC + Type: DWord + ValueName: PreventHandwritingDataSharing + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\Windows Error Reporting + Type: DWord + ValueName: Disabled + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate + Type: DWord + ValueName: DisableWindowsUpdateAccess + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows NT\CurrentVersion\Software Protection Platform + Type: DWord + ValueName: NoGenTicket + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows NT\Printers + Type: DWord + ValueName: DisableHTTPPrinting + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\Windows NT\Printers + Type: DWord + ValueName: DisableWebPnPDownload + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker + Type: DWord + ValueName: WebHelp + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker + Type: DWord + ValueName: CodecDownload + Value: 1 +- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker + Type: DWord + ValueName: WebPublish + Value: 1 \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP._GPOexample b/scripts/ADDS/payload/scripts/11.GPO+GPP._GPOexample new file mode 100644 index 0000000..13f0b75 --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP._GPOexample @@ -0,0 +1,44 @@ +Name: 'COMP: Example GPO' # Prefix the name with either 'COMP:' or 'USER:' +Type: Object # Either 'Object' or 'Preference' (respectively for GPO or GPP) +LinkedOUs: # Entries will be concatenated with ',DC=,DC=' automatically +- OU=Servers +WMIFilters: +- FilterA +- FilterB +RegistryEntries: +- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System + Type: DWord + ValueName: PropertyA + Value: 1 +- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System + Type: DWord + ValueName: PropertyB + Value: 0xFFFFFFFF # Hexadecimal values are prefixed with '0x' +- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Parameters + Type: String + ValueName: # Multiple entries are possible, but *only* for the data type 'String' and 'ExpandString' (REG_SZ and REG_EXPAND_SZ) + - PropertyP + - PropertyQ + - PropertyR + Value: # The amount of entries must match with 'ValueName' + - ValueP + - ValueQ + - ValueR +- Key: HKLM\Software\Test + Type: String + ValueName: + - PropertyX + - PropertyDate + - PropertyOVF + Value: # Values can contain variablenames (respective entries must be declared under 'Variables' below) + - ValueX + - "{{ date }}" + - "{{ guestinfo.dnsserver }}" +--- +Variables: # Each variable consists of a name that is used as a placeholder in the yaml file above, and a PowerShell expression +- Name: date + Expression: | # The PowerShell script's output must evaluate to a [string] + Get-Date +- Name: guestinfo.dnsserver + Expression: | # The variable '$Parameter' will automatically contain all defined OVF Properties + $Parameter['guestinfo.dnsserver'] \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP._GPPexample b/scripts/ADDS/payload/scripts/11.GPO+GPP._GPPexample new file mode 100644 index 0000000..08c6927 --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP._GPPexample @@ -0,0 +1,34 @@ +Name: 'COMP: Example GPO' # Prefix the name with either 'COMP:' or 'USER:' +Type: Preference # Either 'Object' or 'Preference' (respectively for GPO or GPP) +LinkedOUs: # Entries will be concatenated with ',DC=,DC=' automatically +- OU=Servers +WMIFilters: +- FilterA +- FilterB +RegistryEntries: +- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System + Type: DWord + ValueName: PropertyA + Value: 1 + Action: Replace # Valid values are: Create, Update, Replace or Delete + Context: Computer # Valid values are: User or Computer + Disable: False # Change to 'True' when GPP entry should not be applied +- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System + Type: DWord + ValueName: PropertyB + Value: 0xFFFFFFFF # Hexadecimal values are prefixed with '0x' + Action: Replace + Context: Computer + Disable: False +- Key: HKLM\Software\Test + Type: String + ValueName: PropertyOVF + Value: "{{ guestinfo.dnsserver }}" # Values can contain variablenames (respective entries must be declared under 'Variables' below) + Action: Replace + Context: Computer + Disable: False +--- +Variables: # Each variable consists of a name that is used as a placeholder in the yaml file above, and a PowerShell expression +- Name: guestinfo.dnsserver + Expression: | # The variable '$Parameter' will automatically contain all defined OVF Properties + $Parameter['guestinfo.dnsserver'] \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/11.GPO+GPP.ps1 b/scripts/ADDS/payload/scripts/11.GPO+GPP.ps1 new file mode 100644 index 0000000..f653a66 --- /dev/null +++ b/scripts/ADDS/payload/scripts/11.GPO+GPP.ps1 @@ -0,0 +1,175 @@ +#Requires -Modules 'powershell-yaml' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $NewPSSessionSplat = @{ + ComputerName = $Parameter['guestinfo.hostname'] + Credential = New-Object System.Management.Automation.PSCredential( + (Get-ADUser -Filter * | Where-Object {$_.SID -match '-500'}).SamAccountName, + (ConvertTo-SecureString $Parameter['addsconfig.administratorpw'] -AsPlainText -Force) + ) + } + $PSSession = New-PSSession @NewPSSessionSplat + + $GetItemSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.*.yml') + } + Get-Item @GetItemSplat | ForEach-Object { + Write-Host "Loading/parsing file '$($_)' ..." + $GetContentSplat = @{ + Path = $_ + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + + # Check if the respective .yml file declared substitutions which need to be parsed + If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $GroupPolicies = $YamlDocuments[0..($YamlDocuments.Count - 2)] + } + Else { + $GroupPolicies = $YamlDocuments + } + + ForEach ($GroupPolicy in $GroupPolicies) { + Write-Host "Initiating policy '$($GroupPolicy.Name)' ..." + $NewGPOSplat = @{ + Name = $GroupPolicy.Name + ErrorAction = 'SilentlyContinue' + ErrorVariable = 'Failure' + } + $NewGPO = New-GPO @NewGPOSplat + If ($Failure) { + Continue + } + + Switch ($GroupPolicy.Type) { + 'Object' { + ForEach ($ValueSet in $GroupPolicy.RegistryEntries) { + Write-Host "Adding key/value to policy '$($NewGPO.DisplayName)' ...`n [$($ValueSet.Key)/$($ValueSet.ValueName)]" + $SetGPRegistryValueSplat = @{ + Name = $NewGPO.DisplayName + Key = $ValueSet.Key + ValueName = $ValueSet.ValueName + Type = $ValueSet.Type + Value = Switch ($ValueSet.Type) { + 'Binary' { + # Accepted formats: + # 000A0F0100 + # 00 0A 0F 01 00 + # 00,0A,0F,01,00 + [byte[]]([regex]::split(($ValueSet.Value -replace '[ ,]'), '([0-9a-eA-E]{2})') | Where-Object {$_} | ForEach-Object {'0x{0}' -f $_}) + } + 'DWord' { + [uint32]$ValueSet.Value + } + 'QWord' { + [uint64]$ValueSet.Value + } + Default { + $ValueSet.Value + } + } + ErrorAction = 'SilentlyContinue' + } + Set-GPRegistryValue @SetGPRegistryValueSplat | Out-Null + } + } + 'Preference' { + ForEach ($ValueSet in $GroupPolicy.RegistryEntries) { + Write-Host "Adding key/value to policy '$($NewGPO.DisplayName)' ...`n [$($ValueSet.Key)/$($ValueSet.ValueName)]" + $SetGPPrefRegistryValueSplat = @{ + Name = $NewGPO.DisplayName + Key = $ValueSet.Key + Context = $ValueSet.Context + Action = $ValueSet.Action + ValueName = $ValueSet.ValueName + Type = $ValueSet.Type + Value = Switch ($ValueSet.Type) { + 'Binary' { + # Accepted formats: + # 000A0F0100 + # 00 0A 0F 01 00 + # 00,0A,0F,01,00 + [byte[]]([regex]::split(($ValueSet.Value -replace '[ ,]'), '([0-9a-eA-E]{2})') | Where-Object {$_} | ForEach-Object {'0x{0}' -f $_}) + } + 'DWord' { + [uint32]$ValueSet.Value + } + 'QWord' { + [uint64]$ValueSet.Value + } + Default { + $ValueSet.Value + } + } + Disable = [Convert]::ToBoolean($ValueSet.Disable) + ErrorAction = 'SilentlyContinue' + } + Set-GPPrefRegistryValue @SetGPPrefRegistryValueSplat | Out-Null + } + } + } + + ForEach ($Filter in $GroupPolicy.WMIFilters) { + $InvokeCommandSplat = @{ + Session = $PSSession + ArgumentList = $Filter, $Parameter, $NewGPO + ScriptBlock = { + #Requires -Modules 'GPWmiFilter' + Param( + $Filter, + $Parameter, + $NewGPO + ) + + $GetGPWmiFilterSplat = @{ + Name = $Filter + Server = $Parameter['addsconfig.domainname'] + ErrorAction = 'SilentlyContinue' + } + If (Get-GPWMIFilter @GetGPWmiFilterSplat) { + $SetGPWmiFilterAssignmentSplat = @{ + Policy = $NewGPO + Filter = $Filter + EnableException = $True + ErrorAction = 'SilentlyContinue' + } + Set-GPWmiFilterAssignment @SetGPWmiFilterAssignmentSplat + } + } + } + Invoke-Command @InvokeCommandSplat + } + + ForEach ($OU in $GroupPolicy.LinkedOUs) { + If (Test-Path "AD:\$($OU + ',DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC='))") { + Write-Host "Linking policy '$($NewGPO.DisplayName)' to OU '$($OU)' ..." + $NewGPLinkSplat = @{ + Name = $NewGPO.DisplayName + Target = $OU + ',DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + ErrorAction = 'SilentlyContinue' + } + New-GPLink @NewGPLinkSplat | Out-Null + } + } + } + } +} diff --git a/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.ps1 b/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.ps1 new file mode 100644 index 0000000..d5029b6 --- /dev/null +++ b/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.ps1 @@ -0,0 +1,83 @@ +#Requires -Modules 'ActiveDirectory','powershell-yaml' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $PSDrive = Get-PSDrive -Name 'AD' + If ([boolean]$PSDrive -eq $False) { + $NewPSDriveSplat = @{ + Name = 'ADDS' + Root = '' + PSProvider = 'ActiveDirectory' + } + $PSDrive = New-PSDrive @NewPSDriveSplat + } + + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml') + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $WhiteList = ConvertFrom-Yaml @ConvertFromYamlSplat + + $GetADObjectSplat = @{ + Filter = '*' + SearchBase = 'DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=') + SearchScope = 'OneLevel' + } + $WhiteListedOUs = @() + ForEach ($OU in $WhiteList.WhiteListedOUs) { + $WhiteListedOUs += Get-ADObject @GetADObjectSplat | Where-Object { + $_.DistinguishedName -match $OU + } + } + $ParentContainers = Get-ADObject @GetADObjectSplat | Where-Object { + ('builtinDomain', 'container', 'organizationalUnit', <#'lostAndFound',#> 'msDS-QuotaContainer', 'msTPM-InformationObjectsContainer') -contains $_.ObjectClass + } + + ForEach ($Parent in $ParentContainers) { + If ($WhiteListedOUs.DistinguishedName -notcontains $Parent.DistinguishedName) { + ForEach ($SecurityPrincipal in $WhiteList.LimitedSecurityPrincipals) { + $GetACLSPlat = @{ + Path = "$($PSDrive.Name):\$($Parent.DistinguishedName)" + } + $ACL = Get-ACL @GetACLSPlat + + $GetADObjectSplat = @{ + Filter = "sAMAccountName -eq '$($SecurityPrincipal)'" + Properties = 'objectSID' + } + $NewACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( + (Get-ADObject @GetADObjectSplat).objectSID, + [System.DirectoryServices.ActiveDirectoryRights]"GenericAll", + [System.Security.AccessControl.AccessControlType]"Deny", + [System.DirectoryServices.ActiveDirectorySecurityInheritance]"All" + ) + $ACL.AddAccessRule($NewACE) + + $SetAclSplat = @{ + Path = "$($PSDrive.Name):\$($Parent.DistinguishedName)" + AclObject = $ACL + ErrorAction = 'Continue' + } + Set-Acl @SetAclSplat + } + } + } + + If ([boolean]$PSDrive.Name -eq 'ADDS') { + $RemovePSDriveSplat = @{ + Name = 'ADDS' + Force = $True + Confirm = $False + } + Remove-PSDrive @RemovePSDriveSplat | Out-Null + } +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.yml b/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.yml new file mode 100644 index 0000000..b964119 --- /dev/null +++ b/scripts/ADDS/payload/scripts/12.Restrict OU Permissions.yml @@ -0,0 +1,4 @@ +WhiteListedOUs: [] # Entries will be concatenated with ',DC=,DC=' automatically +#- OU=User accounts +LimitedSecurityPrincipals: [] +#- Servicedesk employees diff --git a/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.ps1 b/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.ps1 new file mode 100644 index 0000000..e2e3f98 --- /dev/null +++ b/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.ps1 @@ -0,0 +1,34 @@ +#Requires -Modules 'ActiveDirectory' +Param( + [Parameter(Mandatory)] + [hashtable]$Parameter +) + +# Only executed on primary Domain Controller +If ((Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole -eq 5) { + $GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml") + Raw = $True + } + $RawContent = Get-Content @GetContentSplat + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $Policy = ConvertFrom-Yaml @ConvertFromYamlSplat + + $SetADDefaultDomainPasswordPolicySplat = @{ + Identity = $Parameter['addsconfig.domainname'] + ComplexityEnabled = [Convert]::ToBoolean($Policy.Password.RequireComplexity) + LockoutThreshold = [uint32]$Policy.Account.Lockout.Threshold + # LockoutDuration = [timespan]$Policy.Account.Lockout.Duration + # LockoutObservationWindow = [timespan]$Policy.Account.Lockout.ObservationWindow + MaxPasswordAge = [timespan]$Policy.Password.Age.Maximum + MinPasswordAge = [timespan]$Policy.Password.Age.Minimum + MinPasswordLength = [uint32]$Policy.Password.Length.Minimum + PasswordHistoryCount = [uint32]$Policy.Password.History + ReversibleEncryptionEnabled = [Convert]::ToBoolean($Policy.Password.ReversibleEncryption) + Confirm = $False + } + Set-ADDefaultDomainPasswordPolicy @SetADDefaultDomainPasswordPolicySplat +} \ No newline at end of file diff --git a/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.yml b/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.yml new file mode 100644 index 0000000..a77fe3b --- /dev/null +++ b/scripts/ADDS/payload/scripts/13.Default Domain Password Policy.yml @@ -0,0 +1,14 @@ +Account: + Lockout: + Threshold: 0 + # Duration: '00:15:00.00' + # ObservationWindow: '00:05:00.00' +Password: + RequireComplexity: True + Age: + Minimum: 0 + Maximum: 0 + Length: + Minimum: 10 + History: 0 + ReversibleEncryption: False \ No newline at end of file diff --git a/scripts/Remove-Resources.ps1 b/scripts/Remove-Resources.ps1 new file mode 100644 index 0000000..34a37f0 --- /dev/null +++ b/scripts/Remove-Resources.ps1 @@ -0,0 +1,39 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [string]$VMName, + [Parameter(Mandatory)] + [string]$VSphereFQDN, + [Parameter(Mandatory)] + [string]$VSphereUsername, + [Parameter(Mandatory)] + [string]$VSpherePassword +) + +$PowerCliConfigurationSplat = @{ + Scope = 'User' + ParticipateInCEIP = $False + Confirm = $False + InvalidCertificateAction = 'Ignore' +} +Set-PowerCLIConfiguration @PowerCliConfigurationSplat + +$ConnectVIServerSplat = @{ + Server = $VSphereFQDN + User = "$VSphereUsername" + Password = "$VSpherePassword" + WarningAction = 'SilentlyContinue' +} +Connect-VIServer @ConnectVIServerSplat | Out-Null + +$RemoveVMSplat = @{ + VM = "$($VMName)*" + DeletePermanently = $True + Confirm = $False + ErrorAction = 'SilentlyContinue' +} +Remove-VM @RemoveVMSplat + +# Also delete ISO/floppy? + +Disconnect-VIServer * -Confirm:$False \ No newline at end of file diff --git a/scripts/Update-Manifest.ps1 b/scripts/Update-Manifest.ps1 new file mode 100644 index 0000000..03e3ff8 --- /dev/null +++ b/scripts/Update-Manifest.ps1 @@ -0,0 +1,55 @@ +#Requires -Modules 'powershell-yaml' +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [ValidateScript({ + If (Test-Path($_)) { + $True + } Else { + Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting." + } + })] + [string]$ManifestFileName +) + +$GetItemSplat = @{ + Path = $ManifestFileName +} +$ManifestFile = Get-Item @GetItemSplat + +$SetLocationSplat = @{ + Path = $ManifestFile.DirectoryName +} +Set-Location @SetLocationSplat + +$GetContentSplat = @{ + Path = $ManifestFile.FullName +} +$Manifest = Get-Content @GetContentSplat + +$UpdatedManifest = ForEach ($Line in $Manifest) { + Write-Host "Processing '$($Line)' ..." + If ($Line -match '^SHA256\((.+)\)= ([0-9a-fA-F]{64})$') { + If (Test-Path $Matches[1]) { + $GetFileHashSplat = @{ + Path = $Matches[1] + Algorithm = 'SHA256' + } + Write-Host "Updating checksum..." + "SHA256($($Matches[1]))= $((Get-FileHash @GetFileHashSplat).Hash)" + } + } +} + +If ($UpdatedManifest -ne $Null) { + $SetContentSplat = @{ + Path = $ManifestFile.FullName + Value = $UpdatedManifest + Force = $True + Confirm = $False + } + Set-Content @SetContentSplat +} Else { + Write-Host "Failed updating manifest." + Exit 1 +} diff --git a/scripts/Update-OvfConfiguration.ps1 b/scripts/Update-OvfConfiguration.ps1 new file mode 100644 index 0000000..fd731de --- /dev/null +++ b/scripts/Update-OvfConfiguration.ps1 @@ -0,0 +1,219 @@ +#Requires -Modules 'powershell-yaml' +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [ValidateScript({ + If (Test-Path($_)) { + $True + } Else { + Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting." + } + })] + [string]$OVFFile +) + +$GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml") + Raw = $True +} +$RawContent = Get-Content @GetContentSplat +$ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True +} +$OVFConfig = ConvertFrom-Yaml @ConvertFromYamlSplat + +$SourceFile = Get-Item -Path $OVFFile +$GetContentSplat = @{ + Path = $SourceFile.FullName +} +$XML = [xml](Get-Content @GetContentSplat) +$NS = [System.Xml.XmlNamespaceManager]$XML.NameTable +[void]$NS.AddNamespace('Any', $XML.DocumentElement.xmlns) + +If ($OVFConfig.DeploymentConfigurations.Count -gt 0) { + $XMLSection = $XML.CreateElement('DeploymentOptionSection', $XML.DocumentElement.xmlns) + $XMLSectionInfo = $XML.CreateElement('Info', $XML.DocumentElement.xmlns) + $XMLSectionInfo.InnerText = 'Deployment Type' + [void]$XMLSection.AppendChild($XMLSectionInfo) + + ForEach ($Configuration in $OVFConfig.DeploymentConfigurations) { + $XMLConfig = $XML.CreateElement('Configuration', $XML.DocumentElement.xmlns) + + $XMLConfigAttrId = $XML.CreateAttribute('id', $XML.DocumentElement.ovf) + $XMLConfigAttrId.Value = $Configuration.Id + + $XMLConfigLabel = $XML.CreateElement('Label', $XML.DocumentElement.xmlns) + $XMLConfigLabel.InnerText = $Configuration.Label + + $XMLConfigDescription = $XML.CreateElement('Description', $XML.DocumentElement.xmlns) + $XMLConfigDescription.InnerText = $Configuration.Description + + [void]$XMLConfig.Attributes.Append($XMLConfigAttrId) + [void]$XMLConfig.AppendChild($XMLConfigLabel) + [void]$XMLConfig.AppendChild($XMLConfigDescription) + + [void]$XMLSection.AppendChild($XMLConfig) + } + [void]$XML.SelectSingleNode('//Any:Envelope', $NS).InsertAfter($XMLSection, $XML.SelectSingleNode('//Any:NetworkSection', $NS)) + Write-Host "Inserted 'DeploymentOptionSection' with $($Configuration.Count) nodes" +} + +$XMLAttrTransport = $XML.CreateAttribute('transport', $XML.DocumentElement.ovf) +$XMLAttrTransport.Value = 'com.vmware.guestInfo' +[void]$XML.SelectSingleNode('//Any:VirtualHardwareSection', $NS).Attributes.Append($XMLAttrTransport) + +$XMLProductSection = $XML.SelectSingleNode('//Any:ProductSection', $NS) +If ($XMLProductSection -eq $Null) { + $XMLProductSection = $XML.CreateElement('ProductSection', $XML.DocumentElement.xmlns) + [void]$XML.SelectSingleNode('//Any:VirtualSystem', $NS).AppendChild($XMLProductSection) + Write-Host "Inserted 'ProductSection'" +} Else { + ForEach ($Child in $XMLProductSection.SelectNodes('//Any:ProductSection/child::*', $NS)) { + [void]$Child.ParentNode.RemoveChild($Child) + } + Write-Host "Destroyed pre-existing children in 'ProductSection'" +} +$XMLProductSectionInfo = $XML.CreateElement('Info', $XML.DocumentElement.xmlns) +$XMLProductSectionInfo.InnerText = 'Information about the installed software' +[void]$XMLProductSection.AppendChild($XMLProductSectionInfo) +Write-Host "Inserted new 'Info' into 'ProductSection'" + +ForEach ($Category in $OVFConfig.PropertyCategories) { + If ($Category.Name -ne '') { + $XMLCategory = $XML.CreateElement('Category', $XML.DocumentElement.xmlns) + $XMLCategory.InnerText = $Category.Name + [void]$XMLProductSection.AppendChild($XMLCategory) + Write-Host "Inserted new 'Category' into 'ProductSection'" + } + + ForEach ($Property in $Category.ProductProperties) { + $XMLProperty = $XML.CreateElement('Property', $XML.DocumentElement.xmlns) + + $XMLPropertyAttrKey = $XML.CreateAttribute('key', $XML.DocumentElement.ovf) + $XMLPropertyAttrKey.Value = $Property.Key + $XMLPropertyAttrType = $XML.CreateAttribute('type', $XML.DocumentElement.ovf) + Switch -regex ($Property.Type) { + 'boolean' { + $XMLPropertyAttrType.Value = 'boolean' + } + 'int' { + $XMLPropertyAttrType.Value = 'uint8' + $Qualifiers = @() + If ($Property.Type -match 'int\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinValue($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxValue($($Matches[2]))" + } + $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) + $XMLPropertyAttrQualifiers.Value = $Qualifiers -join ' ' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) + } + } + 'ip' { + $XMLPropertyAttrType.Value = 'string' + $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.vmw) + $XMLPropertyAttrQualifiers.Value = 'Ip' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) + } + 'password' { + $XMLPropertyAttrType.Value = 'string' + $XMLPropertyAttrPassword = $XML.CreateAttribute('password', $XML.DocumentElement.ovf) + $XMLPropertyAttrPassword.Value = 'true' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrPassword) + + $Qualifiers = @() + If ($Property.Type -match 'password\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinLen($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxLen($($Matches[2]))" + } + $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) + $XMLPropertyAttrQualifiers.Value = $Qualifiers -join ' ' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) + } + } + 'string' { + $XMLPropertyAttrType.Value = 'string' + $Qualifiers = @() + If ($Property.Type -match 'string\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinLen($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxLen($($Matches[2]))" + } + $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) + $XMLPropertyAttrQualifiers.Value = $Qualifiers -join ' ' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) + } ElseIf ($Property.Type -match 'string\[(.*)\]') { + $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) + $XMLPropertyAttrQualifiers.Value = "ValueMap{$($Matches[1] -replace '","', '", "')}" + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) + } + } + } + $XMLPropertyAttrUserConfigurable = $XML.CreateAttribute('userConfigurable', $XML.DocumentElement.ovf) + $XMLPropertyAttrUserConfigurable.Value = "$([boolean]$Property.UserConfigurable)".ToLower() + $XMLPropertyAttrValue = $XML.CreateAttribute('value', $XML.DocumentElement.ovf) + If ($Property.Type -eq 'boolean') { + $XMLPropertyAttrValue.Value = "$([boolean]$Property.DefaultValue)".ToLower() + } Else { + $XMLPropertyAttrValue.Value = $Property.DefaultValue + } + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrKey) + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrType) + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrUserConfigurable) + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrValue) + + If ($Property.Label) { + $XMLPropertyLabel = $XML.CreateElement('Label', $XML.DocumentElement.xmlns) + $XMLPropertyLabel.InnerText = $Property.Label + [void]$XMLProperty.AppendChild($XMLPropertyLabel) + } + If ($Property.Description) { + $XMLPropertyDescription = $XML.CreateElement('Description', $XML.DocumentElement.xmlns) + $XMLPropertyDescription.InnerText = $Property.Description + [void]$XMLProperty.AppendChild($XMLPropertyDescription) + } + + If (($Property.Configurations.Count -eq 1) -and ($Property.Configurations -eq '*')) { + $XMLPropertyAttrConfiguration = $XML.CreateAttribute('configuration', $XML.DocumentElement.ovf) + $XMLPropertyAttrConfiguration.Value = $OVFConfig.DeploymentConfigurations.Id -join ' ' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrConfiguration) + } ElseIf ($Property.Configurations.Count -gt 0) { + $XMLPropertyAttrConfiguration = $XML.CreateAttribute('configuration', $XML.DocumentElement.ovf) + $XMLPropertyAttrConfiguration.Value = $Property.Configurations -join ' ' + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrConfiguration) + } + + If ($Property.Value.Count -eq 1) { + $XMLPropertyAttrValue = $XML.CreateAttribute('value', $XML.DocumentElement.ovf) + $XMLPropertyAttrValue.Value = $Property.Value + [void]$XMLProperty.Attributes.Append($XMLPropertyAttrValue) + } ElseIf ($Property.Value.Count -gt 1) { + ForEach ($Value in $Property.Value) { + $XMLValue = $XML.CreateElement('Value', $XML.DocumentElement.xmlns) + + $XMLValueAttrValue = $XML.CreateAttribute('value', $XML.DocumentElement.ovf) + $XMLValueAttrValue.Value = $Value + $XMLValueAttrConfiguration = $XML.CreateAttribute('configuration', $XML.DocumentElement.ovf) + $XMLValueAttrConfiguration.Value = $Value + + [void]$XMLValue.Attributes.Append($XMLValueAttrValue) + [void]$XMLValue.Attributes.Append($XMLValueAttrConfiguration) + + [void]$XMLProperty.AppendChild($XMLValue) + } + } + + [void]$XMLProductSection.AppendChild($XMLProperty) + } + Write-Host "Inserted $($Category.ProductProperties.Count) new node(s) into 'ProductSection'" +} + +$XML.Save($SourceFile.FullName) diff --git a/scripts/Update-OvfConfiguration.yml b/scripts/Update-OvfConfiguration.yml new file mode 100644 index 0000000..58fb6f6 --- /dev/null +++ b/scripts/Update-OvfConfiguration.yml @@ -0,0 +1,147 @@ +DeploymentConfigurations: +- Id: primary + Label: Primary + Description: Initial Domain Controller with 'PDC Emulator'-role (redundant deployment) +- Id: secondary + Label: Secondary + Description: Additional Domain Controller (redundant deployment) +- Id: standalone + Label: Stand-alone + Description: Single Domain Controller (non-redundant deployment) +PropertyCategories: +- Name: '' + ProductProperties: + - Key: deployment.type + Type: string + Value: + - primary + - secondary + - standalone + UserConfigurable: false +- Name: 1) Operating System + ProductProperties: + - Key: guestinfo.hostname + Type: string(1..15) + Label: Hostname* + Description: '(max length: 15 characters)' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true +- Name: 2) Networking + ProductProperties: + - Key: guestinfo.ipaddress + Type: ip + Label: IP Address* + Description: '' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.prefixlength + Type: int(8..32) + Label: Subnet prefix length* + Description: '' + DefaultValue: '24' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.dnsserver + Type: ip + Label: DNS server* + Description: Specify IP address of existing primary Domain Controller + DefaultValue: '127.0.0.1' + Configurations: + - secondary + - standalone + UserConfigurable: true + - Key: guestinfo.gateway + Type: ip + Label: Gateway* + Description: '' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true +- Name: 3) Active Directory Domain Services + ProductProperties: + - Key: addsconfig.domainname + Type: string(5..) + Label: Domain name* + Description: 'Must be a valid FQDN' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: addsconfig.netbiosname + Type: string(1..15) + Label: Domain short name (NetBIOS)* + Description: '(max length: 15 characters)' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: addsconfig.administratorpw + Type: password(7..) + Label: Domain Administrator password* + Description: Must meet password complexity rules + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: addsconfig.safemodepw + Type: password(7..) + Label: Safe-mode password* + Description: Must meet password complexity rules + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: addsconfig.ntpserver + Type: string(1..) + Label: Time server* + Description: A comma-separated list of upstream timeservers + DefaultValue: 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org + Configurations: + - primary + - standalone + UserConfigurable: true +- Name: 4) DHCP default scope + ProductProperties: + - Key: dhcpconfig.startip + Type: ip + Label: Start IP address + Description: '' + DefaultValue: '0.0.0.0' + Configurations: + - secondary + - standalone + UserConfigurable: true + - Key: dhcpconfig.endip + Type: ip + Label: End IP address + Description: '' + DefaultValue: '0.0.0.0' + Configurations: + - secondary + - standalone + UserConfigurable: true + - Key: dhcpconfig.subnetmask + Type: ip + Label: Subnet mask + Description: '' + DefaultValue: '255.255.255.0' + Configurations: + - secondary + - standalone + UserConfigurable: true + - Key: dhcpconfig.gateway + Type: ip + Label: Gateway IP address + Description: '' + DefaultValue: '0.0.0.0' + Configurations: + - secondary + - standalone + UserConfigurable: true + - Key: dhcpconfig.leaseduration + Type: string(1..) + Label: Lease duration + Description: 'Enter as timestamp format (DD.HH:MM:SS.FFFF), or as a number of seconds' + DefaultValue: '01:00:00.00' + Configurations: + - secondary + - standalone + UserConfigurable: true \ No newline at end of file