From 1d9be076989902e72c8b644b8bb9180ea5c9aa52 Mon Sep 17 00:00:00 2001 From: Anand Gaitonde Date: Thu, 19 Nov 2020 12:29:18 -0800 Subject: [PATCH] add bosh-psmodules to stembuild - did not include files related to 2012R2 and 1803 Source: https://github.com/cloudfoundry-attic/bosh-psmodules/ [#175550580](https://www.pivotaltracker.com/story/show/175550580) Where should bosh-psmodules live? --- modules/BOSH.Account/BOSH.Account.Tests.ps1 | 47 ++ modules/BOSH.Account/BOSH.Account.psd1 | 21 + modules/BOSH.Account/BOSH.Account.psm1 | 45 ++ modules/BOSH.Agent/BOSH.Agent.Tests.ps1 | 287 ++++++++ modules/BOSH.Agent/BOSH.Agent.psd1 | 21 + modules/BOSH.Agent/BOSH.Agent.psm1 | 243 +++++++ modules/BOSH.Agent/README.md | 7 + modules/BOSH.Agent/fixtures/bosh-agent.zip | Bin 0 -> 1682 bytes modules/BOSH.Autologon/BOSH.Autologon.psd1 | 21 + modules/BOSH.Autologon/BOSH.Autologon.psm1 | 26 + modules/BOSH.CFCell/BOSH.CFCell.Tests.ps1 | 178 +++++ modules/BOSH.CFCell/BOSH.CFCell.psd1 | 26 + modules/BOSH.CFCell/BOSH.CFCell.psm1 | 376 +++++++++++ modules/BOSH.Disk/BOSH.Disk.psd1 | 21 + modules/BOSH.Disk/BOSH.Disk.psm1 | 75 +++ modules/BOSH.Registry/BOSH.Registry.Tests.ps1 | 82 +++ modules/BOSH.Registry/BOSH.Registry.psd1 | 23 + modules/BOSH.Registry/BOSH.Registry.psm1 | 70 ++ .../BOSH.Registry/data/IE-Policies/.gitignore | 2 + .../data/IE-Policies/machine.txt | Bin 0 -> 29532 bytes .../BOSH.Registry/data/IE-Policies/user.txt | Bin 0 -> 1124 bytes modules/BOSH.SSH/BOSH.SSH.Tests.ps1 | 316 +++++++++ modules/BOSH.SSH/BOSH.SSH.psd1 | 22 + modules/BOSH.SSH/BOSH.SSH.psm1 | 143 ++++ modules/BOSH.Sysprep/BOSH.Sysprep.Tests.ps1 | 619 ++++++++++++++++++ modules/BOSH.Sysprep/BOSH.Sysprep.psd1 | 21 + modules/BOSH.Sysprep/BOSH.Sysprep.psm1 | 474 ++++++++++++++ .../microsoft/windows nt/Audit/audit.csv | 66 ++ .../microsoft/windows nt/SecEdit/GptTmpl.inf | Bin 0 -> 228 bytes .../DomainSysvol/GPO/Machine/reg.pol | Bin 0 -> 180 bytes .../DomainSysvol/GPO/Machine/registry.txt | Bin 0 -> 188 bytes .../DomainSysvol/GPO/User/registry.txt | Bin 0 -> 468 bytes .../print_conflicted_from_audit_csv.rb | 77 +++ .../print_conflicted_keys_from_inf_txt.rb | 108 +++ ...t_conflicted_registry_keys_from_pol_txt.rb | 42 ++ modules/BOSH.Utils/AbsolutePathChroot.psd1 | 14 + modules/BOSH.Utils/AbsolutePathChroot.psm1 | 18 + modules/BOSH.Utils/BOSH.Utils.Tests.ps1 | 527 +++++++++++++++ modules/BOSH.Utils/BOSH.Utils.psd1 | 41 ++ modules/BOSH.Utils/BOSH.Utils.psm1 | 376 +++++++++++ modules/BOSH.Utils/example.zip | Bin 0 -> 171 bytes modules/BOSH.WinRM/BOSH.WinRM.psd1 | 21 + modules/BOSH.WinRM/BOSH.WinRM.psm1 | 73 +++ .../BOSH.WindowsUpdates.Tests.ps1 | 215 ++++++ .../BOSH.WindowsUpdates.psd1 | 21 + .../BOSH.WindowsUpdates.psm1 | 385 +++++++++++ modules/README.md | 30 + 47 files changed, 5180 insertions(+) create mode 100755 modules/BOSH.Account/BOSH.Account.Tests.ps1 create mode 100644 modules/BOSH.Account/BOSH.Account.psd1 create mode 100644 modules/BOSH.Account/BOSH.Account.psm1 create mode 100644 modules/BOSH.Agent/BOSH.Agent.Tests.ps1 create mode 100644 modules/BOSH.Agent/BOSH.Agent.psd1 create mode 100644 modules/BOSH.Agent/BOSH.Agent.psm1 create mode 100644 modules/BOSH.Agent/README.md create mode 100644 modules/BOSH.Agent/fixtures/bosh-agent.zip create mode 100644 modules/BOSH.Autologon/BOSH.Autologon.psd1 create mode 100644 modules/BOSH.Autologon/BOSH.Autologon.psm1 create mode 100644 modules/BOSH.CFCell/BOSH.CFCell.Tests.ps1 create mode 100644 modules/BOSH.CFCell/BOSH.CFCell.psd1 create mode 100644 modules/BOSH.CFCell/BOSH.CFCell.psm1 create mode 100644 modules/BOSH.Disk/BOSH.Disk.psd1 create mode 100644 modules/BOSH.Disk/BOSH.Disk.psm1 create mode 100644 modules/BOSH.Registry/BOSH.Registry.Tests.ps1 create mode 100644 modules/BOSH.Registry/BOSH.Registry.psd1 create mode 100644 modules/BOSH.Registry/BOSH.Registry.psm1 create mode 100644 modules/BOSH.Registry/data/IE-Policies/.gitignore create mode 100755 modules/BOSH.Registry/data/IE-Policies/machine.txt create mode 100755 modules/BOSH.Registry/data/IE-Policies/user.txt create mode 100644 modules/BOSH.SSH/BOSH.SSH.Tests.ps1 create mode 100644 modules/BOSH.SSH/BOSH.SSH.psd1 create mode 100644 modules/BOSH.SSH/BOSH.SSH.psm1 create mode 100644 modules/BOSH.Sysprep/BOSH.Sysprep.Tests.ps1 create mode 100644 modules/BOSH.Sysprep/BOSH.Sysprep.psd1 create mode 100644 modules/BOSH.Sysprep/BOSH.Sysprep.psm1 create mode 100755 modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/Audit/audit.csv create mode 100755 modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf create mode 100755 modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/reg.pol create mode 100755 modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/registry.txt create mode 100755 modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/User/registry.txt create mode 100755 modules/BOSH.Sysprep/print_conflicted_from_audit_csv.rb create mode 100755 modules/BOSH.Sysprep/print_conflicted_keys_from_inf_txt.rb create mode 100755 modules/BOSH.Sysprep/print_conflicted_registry_keys_from_pol_txt.rb create mode 100644 modules/BOSH.Utils/AbsolutePathChroot.psd1 create mode 100644 modules/BOSH.Utils/AbsolutePathChroot.psm1 create mode 100644 modules/BOSH.Utils/BOSH.Utils.Tests.ps1 create mode 100644 modules/BOSH.Utils/BOSH.Utils.psd1 create mode 100644 modules/BOSH.Utils/BOSH.Utils.psm1 create mode 100644 modules/BOSH.Utils/example.zip create mode 100644 modules/BOSH.WinRM/BOSH.WinRM.psd1 create mode 100644 modules/BOSH.WinRM/BOSH.WinRM.psm1 create mode 100644 modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.Tests.ps1 create mode 100644 modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psd1 create mode 100644 modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psm1 create mode 100644 modules/README.md diff --git a/modules/BOSH.Account/BOSH.Account.Tests.ps1 b/modules/BOSH.Account/BOSH.Account.Tests.ps1 new file mode 100755 index 00000000..88bfdb75 --- /dev/null +++ b/modules/BOSH.Account/BOSH.Account.Tests.ps1 @@ -0,0 +1,47 @@ +Remove-Module -Name BOSH.Account -ErrorAction Ignore +Import-Module ./BOSH.Account.psm1 + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ../BOSH.Utils/BOSH.Utils.psm1 + +Describe "Account" { + + Context "when username is not provided" { + It "throws" { + { Add-Account } | Should Throw "Provide a user name" + } + } + + Context "when password is not provided" { + It "throws" { + { Add-Account -User hello } | Should Throw "Provide a password" + } + } + + Context "when the username and password are valid" { + $timestamp=(get-date -UFormat "%s" -Millisecond 0) + $user = "TestUser_$timestamp" + $password = "Password123!" + + BeforeEach { + $userExists = !!(Get-LocalUser | Where {$_.Name -eq $user}) + if($userExists) { + Remove-LocalUser -Name $user + } + } + + It "Adds and removes a new user account" { + Add-Account -User $user -Password $password + mkdir "C:\Users\$user" -ErrorAction Ignore + $adsi = [ADSI]"WinNT://$env:COMPUTERNAME" + $existing = $adsi.Children | where {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user } + $existing | Should Not Be $null + Remove-Account -User $user + $existing = $adsi.Children | where {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user } + $existing | Should Be $null + } + } +} + +Remove-Module -Name BOSH.Account -ErrorAction Ignore +Remove-Module -Name BOSH.Utils -ErrorAction Ignore diff --git a/modules/BOSH.Account/BOSH.Account.psd1 b/modules/BOSH.Account/BOSH.Account.psd1 new file mode 100644 index 00000000..8694d27c --- /dev/null +++ b/modules/BOSH.Account/BOSH.Account.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.Account' +ModuleVersion = '0.1' +GUID = 'ebdf7a79-39df-46ce-a046-42dd10de82ce' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Commands for creating a new Windows user' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils') +FunctionsToExport = @('Add-Account','Remove-Account') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('BOSH') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.Account/BOSH.Account.psm1 b/modules/BOSH.Account/BOSH.Account.psm1 new file mode 100644 index 00000000..5907feb2 --- /dev/null +++ b/modules/BOSH.Account/BOSH.Account.psm1 @@ -0,0 +1,45 @@ +<# +.Synopsis + Add Windows user +.Description + This cmdlet adds a Windows user +#> +function Add-Account { + Param( + [string]$User = $(Throw "Provide a user name"), + [string]$Password = $(Throw "Provide a password") + ) + Write-Log "Add-Account" + + Write-Log "Creating new local user $User." + & NET USER $User $Password /add /y /expires:never + + $Group = "Administrators" + + Write-Log "Adding local user $User to $Group." + $adsi = [ADSI]"WinNT://$env:COMPUTERNAME" + Write-Log $adsi + $AdminGroup = $adsi.Children | where {$_.SchemaClassName -eq 'group' -and $_.Name -eq $Group } + Write-Log $AdminGroup + $UserObject = $adsi.Children | where {$_.SchemaClassName -eq 'user' -and $_.Name -eq $User } + Write-Log $UserObject + $AdminGroup.Add($UserObject.Path) + Write-Log "Completed adding $User to $Group" +} + +<# +.Synopsis +Remove Windows user +.Description +This cmdlet removes a Windows user +#> +function Remove-Account { + Param( + [string]$User = $(Throw "Provide a user name") + ) + Write-Log "Remove-Account" + Write-Log "Removing local user $User." + $adsi = [ADSI]"WinNT://$env:COMPUTERNAME" + $adsi.Delete('User', $User) + Move-Item -Path "C:\Users\$User" -Destination "$env:windir\Temp\$User" -Force -ErrorAction Ignore +} diff --git a/modules/BOSH.Agent/BOSH.Agent.Tests.ps1 b/modules/BOSH.Agent/BOSH.Agent.Tests.ps1 new file mode 100644 index 00000000..bae54686 --- /dev/null +++ b/modules/BOSH.Agent/BOSH.Agent.Tests.ps1 @@ -0,0 +1,287 @@ +Remove-Module -Name BOSH.Agent -ErrorAction Ignore +Import-Module ./BOSH.Agent.psm1 + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ../BOSH.Utils/BOSH.Utils.psm1 + +function New-TempDir { + $parent = [System.IO.Path]::GetTempPath() + [string] $name = [System.Guid]::NewGuid() + (New-Item -ItemType Directory -Path (Join-Path $parent $name)).FullName +} + +Describe "Copy-Agent" { + BeforeEach { + $installDir=(New-TempDir) + $boshDir = (Join-Path $installDir "bosh") + $vcapDir = (Join-Path $installDir (Join-Path "var" (Join-Path "vcap" "bosh"))) + $agentZipPath = (Join-Path $PSScriptRoot (Join-Path "fixtures" "bosh-agent.zip")) + } + + AfterEach { + Remove-Item -Recurse -Force $installDir + } + + Context "when installDir is not provided" { + It "throws" { + { Copy-Agent -agentZipPath $agentZipPath } | Should Throw "Provide a directory to install the BOSH agent" + } + } + + Context "when agentZipPath is not provided" { + It "throws" { + { Copy-Agent -installDir $installDir } | Should Throw "Provide the path to the BOSH agent zipfile" + } + } + + It "creates required directories" { + { Copy-Agent -installDir $installDir -agentZipPath $agentZipPath } | Should Not Throw + Test-Path $boshDir -PathType Container | Should Be $True + Test-Path $vcapDir -PathType Container | Should Be $True + Test-Path (Join-Path $vcapDir "bin") -PathType Container | Should Be $True + Test-Path (Join-Path $vcapDir "log") -PathType Container | Should Be $True + } + + It "populates the created directories with the BOSH agent executable(s)" { + { Copy-Agent -installDir $installDir -agentZipPath $agentZipPath } | Should Not Throw + Test-Path (Join-Path $boshDir "bosh-agent.exe") | Should Be $True + Test-Path (Join-Path $boshDir "service_wrapper.exe") | Should Be $True + Test-Path (Join-Path $boshDir "service_wrapper.xml") | Should Be $True + + $depsDir = (Join-Path $vcapDir "bin") + Test-Path (Join-Path $depsDir "job-service-wrapper.exe") | Should Be $True + Test-Path (Join-Path $depsDir "pipe.exe") | Should Be $True + Test-Path (Join-Path $depsDir "tar.exe") | Should Be $True + Test-Path (Join-Path $depsDir "bosh-blobstore-dav.exe") | Should Be $True + Test-Path (Join-Path $depsDir "bosh-blobstore-s3.exe") | Should Be $True + Test-Path (Join-Path $depsDir "bosh-blobstore-gcs.exe") | Should Be $True + } +} + + +Describe "Write-AgentConfig" { + BeforeEach { + $boshDir=(New-TempDir) + } + + AfterEach { + Remove-Item -Recurse -Force $boshDir + } + + Context "when IaaS is not provided" { + It "throws" { + { Write-AgentConfig -BoshDir $boshDir } | Should Throw "Provide an IaaS for configuration" + } + } + + Context "when IaaS is not supported" { + It "throws" { + { Write-AgentConfig -BoshDir $boshDir -IaaS idontexist } | Should Throw "IaaS idontexist is not supported" + } + } + + Context "when boshDir is not provided" { + It "throws" { + { Write-AgentConfig -IaaS aws } | Should Throw "Provide a directory to install the BOSH agent config" + } + } + + Context "when provided a nonexistent directory" { + It "throws" { + { Write-AgentConfig -BoshDir "nonexistent-dir" -IaaS aws } | Should Throw "Error: nonexistent-dir does not exist" + } + } + + Context "when IaaS is 'aws'" { + It "writes the agent config for aws" { + { Write-AgentConfig -BoshDir $boshDir -IaaS aws } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + } + + It "enables ephemeral disk mounting by default" { + { Write-AgentConfig -BoshDir $boshDir -IaaS aws } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch 'EnableEphemeralDiskMounting' + } + + It "enables ephemeral disk mounting when the flag is true" { + { Write-AgentConfig -BoshDir $boshDir -IaaS aws -EnableEphemeralDiskMounting $true } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch ([regex]::New('"EnableEphemeralDiskMounting":\s*true')) + } + } + + Context "when IaaS is 'openstack'" { + It "writes the agent config for openstack" { + { Write-AgentConfig -BoshDir $boshDir -IaaS openstack } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch ([regex]::Escape('"SSHKeysPath": "/latest/meta-data/public-keys/0/openssh-key/"')) + ($configPath) | Should -FileContentMatch ([regex]::Escape('"UseServerName": true')) + } + } + + Context "when IaaS is 'azure'" { + It "writes the agent config for azure" { + { Write-AgentConfig -BoshDir $boshDir -IaaS azure } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + $configContent = $(Get-Content $configPath | ConvertFrom-JSON) + + $configContent.Infrastructure.Settings.Sources[0].SettingsPath | Should Be "C:/AzureData/CustomData.bin" + $configContent.Infrastructure.Settings.Sources[0].MetaDataPath | Should Be "C:/AzureData/CustomData.bin" + $configContent.Infrastructure.Settings.UseServerName | Should Be "false" + } + + It "enables ephemeral disk mounting when the flag is true" { + { Write-AgentConfig -BoshDir $boshDir -IaaS azure -EnableEphemeralDiskMounting $true } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + $configContent = $(Get-Content $configPath | ConvertFrom-JSON) + + $configContent.Platform.Windows.EnableEphemeralDiskMounting | Should Be $true + } + + } + + Context "when IaaS is 'gcp'" { + It "writes the agent config for gcp" { + { Write-AgentConfig -BoshDir $boshDir -IaaS gcp } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch ([regex]::New('"Metadata-Flavor":\s*"Google"')) + } + + It "enables ephemeral disk mounting by default" { + { Write-AgentConfig -BoshDir $boshDir -IaaS gcp } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch 'EnableEphemeralDiskMounting' + } + + It "enables ephemeral disk mounting when the flag is true" { + { Write-AgentConfig -BoshDir $boshDir -IaaS gcp -EnableEphemeralDiskMounting $true } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch ([regex]::New('"EnableEphemeralDiskMounting":\s*true')) + } + } + + Context "when IaaS is 'vsphere'" { + It "writes the agent config for vsphere" { + { Write-AgentConfig -BoshDir $boshDir -IaaS vsphere } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + $configContent = $(Get-Content $configPath | ConvertFrom-JSON) + $configContent.Infrastructure.Settings.Sources[0].Type | Should Be "CDROM" + } + + It "enables ephemeral disk mounting by default" { + { Write-AgentConfig -BoshDir $boshDir -IaaS vsphere } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch 'EnableEphemeralDiskMounting' + } + + It "enables ephemeral disk mounting when the flag is true" { + { Write-AgentConfig -BoshDir $boshDir -IaaS vsphere -EnableEphemeralDiskMounting $true } | Should Not Throw + $configPath = (Join-Path $boshDir "agent.json") + Test-Path $configPath | Should Be $True + ($configPath) | Should -FileContentMatch ([regex]::New('"EnableEphemeralDiskMounting":\s*true')) + } + } +} + +Describe "Set-Path" { + BeforeEach { + $oldPath=(Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path + $tempDir=(New-TempDir) + } + + AfterEach { + Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $oldPath + Remove-Item -Recurse -Force $tempDir + } + + It "sets the system path" { + { Set-Path -Path $tempDir} | Should Not Throw + $path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path + $path | Should Match ([regex]::Escape($tempDir)) + } + + Context "when not provided a path to add" { + It "throws" { + { Set-Path } | Should Throw "Error: Provide a directory to add to the path" + } + } +} + +Describe "Install-Agent" { + It "calls service_wrapper.exe" { + Mock -Verifiable -ModuleName BOSH.Agent Start-Process {} -ParameterFilter { $FilePath -eq "C:\bosh\service_wrapper.exe" -and $ArgumentList -eq "install" -and $NoNewWindow -and $Wait } + Install-AgentService + Assert-VerifiableMock + } +} + +Describe "Install-Agent" { + Context "when IaaS is not provided" { + It "throws" { + { Install-Agent -agentZipPath "some-agent-zip-path" } | Should Throw "Provide the IaaS of your VM" + } + } + Context "when agent.zip is not provided" { + It "throws" { + { Install-Agent -IaaS "some-Iaas" } | Should Throw "Provide the path of your agent.zip" + } + } + + Context "windows 2012R2" { + It "calls helper functions with default arguments" { + Mock Get-OSVersion { "windows2012R2" } -ModuleName BOSH.Agent + Mock Test-Path { $true } -ModuleName BOSH.Agent + + Mock -Verifiable -ModuleName BOSH.Agent Copy-Agent {} -ParameterFilter { $InstallDir -eq "C:\" -and $agentZipPath -eq "some-agent-zip-path" } + + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $path -eq "C:\bosh" } + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $path -eq "C:\var" } + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $path -eq "C:\Windows\Panther" -and $disableInheritance -eq $false } + + Mock -Verifiable -ModuleName BOSH.Agent Write-AgentConfig {} -ParameterFilter { $IaaS -eq "aws" -and $BoshDir -eq "C:\bosh" -and $EnableEphemeralDiskMounting -eq $false } + Mock -Verifiable -ModuleName BOSH.Agent Set-Path {} -ParameterFilter { $Path -eq "C:\var\vcap\bosh\bin" } + Mock -Verifiable -ModuleName BOSH.Agent Install-AgentService {} + + Install-Agent -IaaS aws -agentZipPath "some-agent-zip-path" + + Assert-VerifiableMock + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Agent + } + } + + Context "windows 2016" { + It "calls helper functions with default arguments" { + Mock Get-OSVersion { "window2016" } -ModuleName BOSH.Agent + Mock Test-Path { $true } -ModuleName BOSH.Agent + + Mock -Verifiable -ModuleName BOSH.Agent Copy-Agent {} -ParameterFilter { $InstallDir -eq "C:\" -and $agentZipPath -eq "some-agent-zip-path" } + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $Path -eq "C:\bosh" } + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $Path -eq "C:\var" } + Mock -Verifiable -ModuleName BOSH.Agent Protect-Dir {} -ParameterFilter { $Path -eq "C:\Windows\Panther" -and $disableInheritance -eq $false } + + Mock -Verifiable -ModuleName BOSH.Agent Write-AgentConfig {} -ParameterFilter { $IaaS -eq "aws" -and $BoshDir -eq "C:\bosh" -and $EnableEphemeralDiskMounting -eq $true } + Mock -Verifiable -ModuleName BOSH.Agent Set-Path {} -ParameterFilter { $Path -eq "C:\var\vcap\bosh\bin" } + Mock -Verifiable -ModuleName BOSH.Agent Install-AgentService {} + + Install-Agent -IaaS aws -agentZipPath "some-agent-zip-path" + + Assert-VerifiableMock + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Agent + } + } +} + +Remove-Module -Name BOSH.Agent -ErrorAction Ignore +Remove-Module -Name BOSH.Utils -ErrorAction Ignore diff --git a/modules/BOSH.Agent/BOSH.Agent.psd1 b/modules/BOSH.Agent/BOSH.Agent.psd1 new file mode 100644 index 00000000..4c9dee25 --- /dev/null +++ b/modules/BOSH.Agent/BOSH.Agent.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.Agent' +ModuleVersion = '0.1' +GUID = 'f46b71ee-4312-4f80-34c7-665d64250ae8' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Install BOSH-Agent on a BOSH deployed vm' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils') +FunctionsToExport = @('Install-Agent') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('BOSH') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.Agent/BOSH.Agent.psm1 b/modules/BOSH.Agent/BOSH.Agent.psm1 new file mode 100644 index 00000000..83b8981f --- /dev/null +++ b/modules/BOSH.Agent/BOSH.Agent.psm1 @@ -0,0 +1,243 @@ +<# +.Synopsis + Install BOSH Agent +.Description + This cmdlet installs BOSH Agent +#> +$ErrorActionPreference = "Stop"; +trap { $host.SetShouldExit(1) } + +function Install-Agent { + Param( + [string]$IaaS = $(Throw "Provide the IaaS of your VM"), + [string]$agentZipPath = $(Throw "Provide the path of your agent.zip"), + [switch]$EnableEphemeralDiskMounting = $true + ) + + $OsVersion = Get-OSVersion + + if ($OSVersion -eq "windows2012R2") { $EnableEphemeralDiskMounting = $false } + + Write-Log "Install-Agent: Started" + + Copy-Agent -InstallDir "C:\" -agentZipPath $agentZipPath + Protect-Dir -Path "C:\bosh" + Protect-Dir -Path "C:\var" + Write-AgentConfig -BoshDir "C:\bosh" -IaaS $IaaS -EnableEphemeralDiskMounting $EnableEphemeralDiskMounting + Set-Path "C:\var\vcap\bosh\bin" + Install-AgentService + Protect-Dir -Path "C:\Windows\Panther" -disableInheritance $False + Write-Log "Install-Agent: Finished" +} + +function Copy-Agent { + Param( + [string]$installDir = $(Throw "Provide a directory to install the BOSH agent"), + [string]$agentZipPath = $(Throw "Provide the path to the BOSH agent zipfile") + ) + + Write-Log "Copy-Agent InstallDir=${installDir} Zip=${agentZipPath}" + + $boshDir = (Join-Path $installDir "bosh") + if (Test-Path $boshDir) { + Write-Log "Copy-Agent removing existing BOSH dir: ${boshDir}" + Remove-Item -Path $boshDir -Recurse -Force + } + New-Item -Path $boshDir -ItemType Directory -Force + + $varDir = (Join-Path $installDir "var") + if (Test-Path $varDir) { + Write-Log "Copy-Agent removing existing VAR dir: ${varDir}" + Remove-Item -Path $varDir -Recurse -Force + } + $vcapDir = (Join-Path $installDir (Join-Path "var" (Join-Path "vcap" "bosh"))) + New-Item -Path (Join-Path $vcapDir "log") -ItemType Directory -Force + + $depsDir = (Join-Path $vcapDir "bin") + if (Test-Path $depsDir) { + Write-Log "Copy-Agent removing existing Deps dir: ${depsDir}" + Remove-Item -Path $depsDir -Recurse -Force + } + New-Item -Path $depsDir -ItemType Directory -Force + + Open-Zip $agentZipPath $boshDir + Move-Item (Join-Path $boshDir (Join-Path "deps" "*")) $depsDir + Remove-Item -Path (Join-Path $boshDir "deps") -Force +} + + +function Write-AgentConfig { + Param( + [string]$boshDir = $(Throw "Provide a directory to install the BOSH agent config"), + [string]$IaaS = $(Throw "Provide an IaaS for configuration"), + [bool]$EnableEphemeralDiskMounting = $true + ) + + if (-Not (Test-Path $boshDir -PathType Container)) { + Throw "Error: $($boshDir) does not exist" + } + + $awsConfig = @{ + "Platform" = @{ + "Linux" = @{ + "DevicePathResolutionType" = "virtio" + } + } + "Infrastructure" = @{ + "Settings" = @{ + # Sources is an array of objects, hence the weird PS syntax + "Sources" = (,@{ + "Type" = "HTTP" + "URI" = "http://169.254.169.254" + "UserDataPath" = "/latest/user-data/" + "InstanceIDPath" = "/latest/meta-data/instance-id/" + }) + "UseRegistry" = $true + } + } + } + if ($EnableEphemeralDiskMounting) { $awsConfig.Platform.Windows = @{ EnableEphemeralDiskMounting = $true } } + $awsConfig = $awsConfig | ConvertTo-JSON -Depth 100 + + $azureConfig = @{ + "Platform" = @{ + "Linux" = @{ + "CreatePartitionIfNoEphemeralDisk" = $true + "DevicePathResolutionType" = "scsi" + } + } + "Infrastructure" = @{ + "Settings" = @{ + "Sources" = (,@{ + "Type" = "File" + "MetaDataPath" = "C:/AzureData/CustomData.bin" + "UserDataPath" = "C:/AzureData/CustomData.bin" + "SettingsPath" = "C:/AzureData/CustomData.bin" + }) + "UseServerName" = $false + "UseRegistry" = $true + } + } + } + if ($EnableEphemeralDiskMounting) { $azureConfig.Platform.Windows = @{ EnableEphemeralDiskMounting = $true } } + $azureConfig = $azureConfig | ConvertTo-JSON -Depth 100 + + $gcpConfig = @{ + "Platform" = @{ + "Linux" = @{ + "CreatePartitionIfNoEphemeralDisk" = $true + "DevicePathResolutionType" = "virtio" + "VirtioDevicePrefix" = "google" + } + } + "Infrastructure" = @{ + "Settings" = @{ + # Sources is an array of objects, hence the weird PS syntax + "Sources" = (,@{ + "Type" = "InstanceMetadata" + "URI" = "http://169.254.169.254" + "SettingsPath" = "/computeMetadata/v1/instance/attributes/bosh_settings" + "Headers" = @{ + "Metadata-Flavor" = "Google" + } + }) + "UseServerName" = $true + "UseRegistry" = $false + } + } + } + if ($EnableEphemeralDiskMounting) { $gcpConfig.Platform.Windows = @{ EnableEphemeralDiskMounting = $true } } + $gcpConfig = $gcpConfig | ConvertTo-JSON -Depth 100 + + $openstackConfig = @" +{ + "Platform": { + "Linux": { + "DevicePathResolutionType": "virtio" + } + }, + "Infrastructure": { + "Settings": { + "Sources": [ + { + "Type": "HTTP", + "URI": "http://169.254.169.254", + "UserDataPath": "/latest/user-data/", + "InstanceIDPath": "/latest/meta-data/instance-id/", + "SSHKeysPath": "/latest/meta-data/public-keys/0/openssh-key/" + } + ], + "UseRegistry": true, + "UseServerName": true + } + } +} +"@ + + $vsphereConfig = @{ + "Platform" = @{ + "Linux" = @{ + "DevicePathResolutionType" = "scsi" + } + } + "Infrastructure" = @{ + "Settings" = @{ + "Sources" = (,@{ + "Type" = "CDROM" + "FileName" = "ENV" + }) + } + } + } + + if ($EnableEphemeralDiskMounting) { $vsphereConfig.Platform.Windows = @{ EnableEphemeralDiskMounting = $true } } + $vsphereConfig = $vsphereConfig | ConvertTo-JSON -Depth 100 + + if ($IaaS -eq 'aws') { + Write-Log "Agent Config: ${awsConfig}" + New-Item -ItemType file -path (Join-Path $boshDir "agent.json") -Value $awsConfig + } elseif ($IaaS -eq 'azure') { + Write-Log "Agent Config: ${azureConfig}" + New-Item -ItemType file -path (Join-Path $boshDir "agent.json") -Value $azureConfig + } elseif ($IaaS -eq 'vsphere') { + Write-Log "Agent Config: ${vsphereConfig}" + New-Item -ItemType file -path (Join-Path $boshDir "agent.json") -Value $vsphereConfig + } elseif ($IaaS -eq 'gcp') { + Write-Log "Agent Config: ${gcpConfig}" + New-Item -ItemType file -path (Join-Path $boshDir "agent.json") -Value $gcpConfig + } elseif ($IaaS -eq 'openstack') { + Write-Log "Agent Config: ${openstackConfig}" + New-Item -ItemType file -path (Join-Path $boshDir "agent.json") -Value $openstackConfig + } else { + Throw "IaaS $($IaaS) is not supported" + } + +} + +function Set-Path { + Param( + [string]$Path= $(Throw "Error: Provide a directory to add to the path") + ) + Write-Log "Set-Path: ${Path} to path" + Setx PATH "${env:PATH};${Path}" /m +} + +function Install-AgentService { + Write-Log "Updating services timeout from 30s to 60s" + $parentRegistryPath = "HKLM:\SYSTEM\CurrentControlSet" + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control" + $name = "ServicesPipeTimeout" + $value = 60000 + + If (-NOT (Test-Path $parentRegistryPath)) { + New-Item $parentRegistryPath | Out-Null + } + + If (-NOT (Test-Path $registryPath)) { + New-Item $registryPath | Out-Null + } + + New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null + Write-Log "Install-AgentService: Installing BOSH Agent" + Start-Process -FilePath "C:\bosh\service_wrapper.exe" -ArgumentList "install" -NoNewWindow -Wait +} diff --git a/modules/BOSH.Agent/README.md b/modules/BOSH.Agent/README.md new file mode 100644 index 00000000..5f3cf9b2 --- /dev/null +++ b/modules/BOSH.Agent/README.md @@ -0,0 +1,7 @@ +# BOSH.Agent + +Powershell module to install bosh-agent for a given IAAS + +``` +Install-Agent -IaaS $IaaS -AgentZipPath (Join-Path $PSScriptRoot 'agent.zip') +``` diff --git a/modules/BOSH.Agent/fixtures/bosh-agent.zip b/modules/BOSH.Agent/fixtures/bosh-agent.zip new file mode 100644 index 0000000000000000000000000000000000000000..754ac3d230f8130c9387c62eef94a4cebc75efaf GIT binary patch literal 1682 zcma)+Jxjw-6oyZmMzDS$)In_VA4COzfw&X}DWV{LAW3^|E3FAhZ9~DKle!8n;@Hhe zP;hf_a_Hiy{sl*O&yBga;oj!PhK4NXIdAScPf8nFCJXep*4Uu+6u`2*nqgMS zX={=AWM_LEwDYUS-S>~@gEM~uQkkC=KxP&zN}Ti7yWWg)bmUZs<1Gc{w^(}fsVF}d zV&qwQki}})me;afVpPmF&-8+sl+c<0osWTfE1db7#5@$3Q;g}<9l|jWNtmX9DKN~5 zZ5bYM+x0RrIvNW>GDBJX+#_i$b&yLjm-A7Yr3=3O1moC>qF~L>qj?G+@>spIW2f%g#jk%lI(~&SGc~BB?1__HII~I*x!v-Ub kLohW?kzDpI3Vs>^4(vlPwNC=;W4Ka~#jC3Ur11~%3v4!3V*mgE literal 0 HcmV?d00001 diff --git a/modules/BOSH.Autologon/BOSH.Autologon.psd1 b/modules/BOSH.Autologon/BOSH.Autologon.psd1 new file mode 100644 index 00000000..93e32595 --- /dev/null +++ b/modules/BOSH.Autologon/BOSH.Autologon.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.Autologon' +ModuleVersion = '0.1' +GUID = 'eee3e65d-b18e-4277-abc8-12c60a8f1f52' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Commands to enable/disable Autologon on a BOSH deployed vm' +RequiredModules = @('BOSH.Utils') +PowerShellVersion = '4.0' +FunctionsToExport = @('Enable-Autologon','Disable-Autologon') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('Autologon') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.Autologon/BOSH.Autologon.psm1 b/modules/BOSH.Autologon/BOSH.Autologon.psm1 new file mode 100644 index 00000000..5c5a6f67 --- /dev/null +++ b/modules/BOSH.Autologon/BOSH.Autologon.psm1 @@ -0,0 +1,26 @@ +<# +.Synopsis + Configure Autologon +.Description + This cmdlet enables/disables the Autologon +#> + +$RegistryKey="HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" + +function Enable-Autologon { + Param ( + [Parameter(Mandatory=$true)][string]$Password, + [string]$User="Provisioner" + ) + + Write-Log "Enable Autologon" + Set-ItemProperty -Path $RegistryKey -Name AutoAdminLogon -Value 1 -Force + Set-ItemProperty -Path $RegistryKey -Name AutoLogonCount -Value 50 -Force + Set-ItemProperty -Path $RegistryKey -Name DefaultUserName -Value $User -Force + Set-ItemProperty -Path $RegistryKey -Name DefaultPassword -Value $Password -Force +} + +function Disable-Autologon { + Write-Log "Disable Autologon" + Set-ItemProperty $RegistryKey -name AutoAdminLogon -value 0 +} diff --git a/modules/BOSH.CFCell/BOSH.CFCell.Tests.ps1 b/modules/BOSH.CFCell/BOSH.CFCell.Tests.ps1 new file mode 100644 index 00000000..68d82050 --- /dev/null +++ b/modules/BOSH.CFCell/BOSH.CFCell.Tests.ps1 @@ -0,0 +1,178 @@ +Remove-Module -Name BOSH.CFCell -ErrorAction Ignore +Import-Module ./BOSH.CFCell.psm1 + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ../BOSH.Utils/BOSH.Utils.psm1 + +#this function does not exist on VMs without Windows Defender installed +function Set-MpPreference() { + param( + [bool]$DisableBehaviorMonitoring, + [bool]$OtherThing + ) +} + +Describe "Protect-CFCell" { + BeforeEach { + $oldWinRMStatus = (Get-Service winrm).Status + $oldWinRMStartMode = ( Get-Service winrm ).StartType + + { Set-Service -Name "winrm" -StartupType "Manual" } | Should Not Throw + + Start-Service winrm + + Mock Get-Command { [hashtable]@{ParameterSets = [hashtable]@{Parameters = @()}} } -ModuleName BOSH.CFCell + Mock Write-Log {} -ModuleName BOSH.CFCell + + } + + AfterEach { + if ($oldWinRMStatus -eq "Stopped") { + { Stop-Service winrm } | Should Not Throw + } else { + { Set-Service -Name "winrm" -Status $oldWinRMStatus } | Should Not Throw + } + { Set-Service -Name "winrm" -StartupType $oldWinRMStartMode } | Should Not Throw + } + + It "disables the RDP service and firewall rule" { + Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0 + Get-NetFirewallRule -DisplayName "Remote Desktop*" | Set-NetFirewallRule -enabled true + Get-Service "Termservice" | Set-Service -StartupType "Automatic" + netstat /p tcp /a | findstr ":3389 " | Should Not BeNullOrEmpty + + Protect-CFCell + + Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" | select -exp fDenyTSConnections | Should Be 1 + netstat /p tcp /a | findstr ":3389 " | Should BeNullOrEmpty + Get-NetFirewallRule -DisplayName "Remote Desktop*" | ForEach { $_.enabled | Should be "False" } + Get-Service "Termservice" | Select -exp starttype | Should Be "Disabled" + } + + It "disables the services" { + Get-Service | Where-Object {$_.Name -eq "WinRM" } | Set-Service -StartupType Automatic + Get-Service | Where-Object {$_.Name -eq "W3Svc" } | Set-Service -StartupType Automatic + Protect-CFCell + (Get-Service | Where-Object {$_.Name -eq "WinRM" } ).StartType| Should be "Disabled" + $w3svcStartType = (Get-Service | Where-Object {$_.Name -eq "W3Svc" } ).StartType + "Disabled", $null -contains $w3svcStartType | Should Be $true + } + + It "sets firewall rules" { + Set-NetFirewallProfile -all -DefaultInboundAction Allow -DefaultOutboundAction Allow -AllowUnicastResponseToMulticast False -Enabled True + get-firewall "public" | Should be "public,Allow,Allow" + get-firewall "private" | Should be "private,Allow,Allow" + get-firewall "domain" | Should be "domain,Allow,Allow" + Protect-CFCell + get-firewall "public" | Should be "public,Block,Allow" + get-firewall "private" | Should be "private,Block,Allow" + get-firewall "domain" | Should be "domain,Block,Allow" + } + + It "sets all Windows Defender `disable` settings to true" { + Mock Get-Command { + [hashtable]@{ + ParameterSets = [hashtable]@{ + Parameters = @( + @{Name = "DisableBehaviorMonitoring"}, + @{Name = "OtherThing"} + ) + } + } + } -ModuleName BOSH.CFCell + Mock Set-MpPreference { } -ModuleName BOSH.CFCell + + Protect-CFCell + + Assert-MockCalled Write-Log -Exactly 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $Message -eq "Disabling Windows Defender Features" } + + Assert-MockCalled Set-MpPreference -Exactly 1 -Scope It -ParameterFilter { $DisableBehaviorMonitoring -eq $true } -ModuleName BOSH.CFCell + Assert-MockCalled Set-MpPreference -Exactly 0 -Scope It -ParameterFilter { $OtherThing -eq $true } -ModuleName BOSH.CFCell + + Assert-MockCalled Write-Log -Exactly 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $Message -eq "Setting Defender preference DisableBehaviorMonitoring to True" } + } + + It "does not attempt to change Windows Defender settings if Windows Defender is not installed" { + Mock Get-Command { $false } -ModuleName BOSH.CFCell + Mock Set-MpPreference { } -ModuleName BOSH.CFCell + + Protect-CFCell + + Assert-MockCalled Write-Log -Exactly 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $Message -eq "Set-MpPreference command not found, assuming Windows Defender is not installed" } + Assert-MockCalled Set-MpPreference -Scope It -Exactly 0 -ModuleName BOSH.CFCell + } + +} + +Describe "Install-CFFeatures" { + It "restarts computer on Microsoft server 2016 and later" { + Mock Install-CFFeatures2012 { } -ModuleName BOSH.CFCell + Mock Install-CFFeatures2016 { } -ModuleName BOSH.CFCell + Mock Write-Error { } -ModuleName BOSH.CFCell + Mock Get-WmiObject { New-Object PSObject -Property @{Version = "10.0.1803"} } -ModuleName BOSH.CFCell + + { Install-CFFeatures } | Should -Not -Throw + + Assert-MockCalled Install-CFFeatures2016 -Times 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $ForceReboot } + Assert-MockCalled Install-CFFeatures2012 -Times 0 -Scope It -ModuleName BOSH.CFCell + } +} + +Describe "Install-CFFeatures2016" { + BeforeEach { + Mock Write-Log { } -ModuleName BOSH.CFCell + Mock Get-WinRMConfig { "Some config" } -ModuleName BOSH.CFCell + Mock WindowsFeatureInstall { } -ModuleName BOSH.CFCell + Mock Remove-WindowsFeature { } -ModuleName BOSH.CFCell + Mock Set-Service { } -ModuleName BOSH.CFCell + Mock Restart-Computer { } -ModuleName BOSH.CFCell + } + + It "triggers a machine restart when the -ForceReboot flag is set" { + { Install-CFFeatures2016 -ForceReboot } | Should -Not -Throw + + Assert-MockCalled Restart-Computer -Times 1 -Scope It -ModuleName BOSH.CFCell + } + + It "doesn't trigger a machine restart if -ForceReboot flag not set" { + { Install-CFFeatures2016 } | Should -Not -Throw + + Assert-MockCalled Restart-Computer -Times 0 -Scope It -ModuleName BOSH.CFCell + } + + It "logs Installing CloudFoundry Cell Windows Features" { + { Install-CFFeatures2016 } | Should -Not -Throw + + Assert-MockCalled Write-Log -Times 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $Message -eq "Installing CloudFoundry Cell Windows Features" } + } + + It "logs Installed CloudFoundry Cell Windows Features after installation" { + { Install-CFFeatures2016 } | Should -Not -Throw + + Assert-MockCalled Write-Log -Times 1 -Scope It -ModuleName BOSH.CFCell -ParameterFilter { $Message -eq "Installed CloudFoundry Cell Windows Features" } + } + +} + +Describe "Remove-DockerPackage" { + It "Is impossible to test this" { + # Pest has issues mocking functions that use validateSet See: https://github.com/pester/Pester/issues/734 +# Mock Uninstall-Package { } -ModuleName BOSH.CFCell +# Mock Write-Log { } -ModuleName BOSH.CFCell +# Mock Uninstall-Module { } -ParameterFilter { $Name -eq "DockerMsftProvider" -and $ErrorAction -eq "Ignore" } -ModuleName BOSH.CFCell +# Mock Get-HNSNetwork { "test-network" } -ModuleName BOSH.CFCell +# Mock Remove-HNSNetwork { } -ModuleName BOSH.CFCell +# Mock remove-DockerProgramData { } -ModuleName BOSH.CFCell +# +# { Remove-DockerPackage } | Should -Not -Throw +# +# Assert-MockCalled Uninstall-Package -Times 1 +# Assert-MockCalled Uninstall-Module -Times 1 -Scope It -ParameterFilter { $Name -eq "DockerMsftProvider" -and $ErrorAction -eq "Ignore" } -ModuleName BOSH.CFCell +# Assert-MockCalled Get-HNSNetwork -Times 1 -Scope It -ModuleName BOSH.CFCell +# Assert-MockCalled Remove-HNSNetwork -Times 1 -Scope It -ParameterFilter { $ArgumentList -eq "test-network" } -ModuleName BOSH.CFCell +# Assert-MockCalled remove-DockerProgramData-Times 1 -Scope It -ModuleName BOSH.CFCell + } +} + +Remove-Module -Name BOSH.CFCell -ErrorAction Ignore +Remove-Module -Name BOSH.Utils -ErrorAction Ignore diff --git a/modules/BOSH.CFCell/BOSH.CFCell.psd1 b/modules/BOSH.CFCell/BOSH.CFCell.psd1 new file mode 100644 index 00000000..b4a78900 --- /dev/null +++ b/modules/BOSH.CFCell/BOSH.CFCell.psd1 @@ -0,0 +1,26 @@ +@{ +RootModule = 'BOSH.CFCell' +ModuleVersion = '0.1' +GUID = '43f3e65d-b18e-2134-abc8-12c60a8f1f52' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Commands for CloudFoundry Cell on a BOSH deployed vm' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils') +FunctionsToExport = @('disable-service', +'Install-CFFeatures', +'Install-CFFeatures2012', +'Install-CFFeatures2016', +'Remove-DockerPackage', +'Protect-CFCell') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('CloudFoundry') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.CFCell/BOSH.CFCell.psm1 b/modules/BOSH.CFCell/BOSH.CFCell.psm1 new file mode 100644 index 00000000..0b28d7ae --- /dev/null +++ b/modules/BOSH.CFCell/BOSH.CFCell.psm1 @@ -0,0 +1,376 @@ +<# +.Synopsis + Install CloudFoundry Cell components for either 2012R2 or 2016 +.Description + This cmdlet installs the minimum set of features for a CloudFoundry Cell on Windows 2012R2 or Windows 2016 +#> +function Install-CFFeatures { + $OS = Get-WmiObject Win32_OperatingSystem + switch -Wildcard ($OS.Version) { + "6.3.*" { + Install-CFFeatures2012 + } + "10.0.*" { + Install-CFFeatures2016 -ForceReboot + } + default { + Write-Error "Unsupported Windows version $($OS.Version)" + } + } +} + +<# +.Synopsis + Install Windows 2012 CloudFoundry Cell components +.Description + This cmdlet installs the minimum set of features for a CloudFoundry Cell on Windows 2012R2 +#> +function Install-CFFeatures2012 { + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + + Write-Log "Installing CloudFoundry Cell Windows 2012 Features" + $ErrorActionPreference = "Stop"; + trap { $host.SetShouldExit(1) } + + WindowsFeatureInstall("Web-Webserver") + WindowsFeatureInstall("Web-WebSockets") + WindowsFeatureInstall("AS-Web-Support") + WindowsFeatureInstall("AS-NET-Framework") + WindowsFeatureInstall("Web-WHC") + WindowsFeatureInstall("Web-ASP") + + Write-Log "Installed CloudFoundry Cell Windows 2012 Features" +} + +<# +.Synopsis + Install Windows 2016 CloudFoundry Cell components +.Description + This cmdlet installs the minimum set of features for a CloudFoundry Cell on Windows 2016 +#> +function Install-CFFeatures2016 { + param([switch]$ForceReboot) + + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + + Write-Log "Installing CloudFoundry Cell Windows Features" + $ErrorActionPreference = "Stop"; + trap { $host.SetShouldExit(1) } + + WindowsFeatureInstall("FS-Resource-Manager") + WindowsFeatureInstall("Containers") + + Write-Log "Installed CloudFoundry Cell Windows Features" + + Write-Log "Setting WinRM startup type to automatic" + Get-Service | Where-Object {$_.Name -eq "WinRM" } | Set-Service -StartupType Automatic + + if ($ForceReboot -eq $true) { + Restart-Computer + } +} + +function Wait-ForNewIfaces() { + param([string]$ifaces) + $max = 20 + $try = 0 + + while($try -le $max) { + # Get a list of network interfaces created by installing Docker. + $newIfaces=(Get-NetIPInterface -AddressFamily IPv4 | where { + -Not ($_.InterfaceAlias -in $ifaces) -and $_.NlMtu -eq 1500 + }).InterfaceAlias + + if($newIfaces.Count -gt 0) { + Write-Host "Docker added interfaces: $newIfaces" + return $newIfaces + } + Start-Sleep -s 5 + $try++ + } + + Write-Error "Time-out waiting for docker to add Network Interface on GCP" + Throw "Should not get here" +} + +function Protect-CFCell { + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + disable-service("WinRM") + disable-service("W3Svc") + disable-rdp + set-firewall + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + + Write-Log "Disabling NetBIOS over TCP" + Disable-NetBIOS + + Disable-WindowsDefenderFeatures +} + +function Disable-WindowsDefenderFeatures { + if (Get-Command -Name Set-MpPreference -ErrorAction SilentlyContinue) + { + Write-Log "Disabling Windows Defender Features" + (Get-Command -Name Set-MpPreference).ParameterSets.Parameters | + Where-Object { + $_.Name -Like "Disable*" + } | + ForEach-Object { + Write-Log "Setting Defender preference $( $_.Name ) to True" + iex "Set-MpPreference -$( $_.Name ) `$true" + } + } + else { + Write-Log "Set-MpPreference command not found, assuming Windows Defender is not installed" + } +} + +function WindowsFeatureInstall { + param ([string]$feature) + + Write-Log "Installing $feature" + If (!(Get-WindowsFeature $feature).Installed) { + Install-WindowsFeature $feature + If (!(Get-WindowsFeature $feature).Installed) { + Throw "Failed to install $feature" + } + } +} + +function disable-rdp { + Write-Log "Starting to disable RDP" + Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 1 + Get-NetFirewallRule -DisplayName "Remote Desktop*" | Set-NetFirewallRule -enabled false + disable-service "Termservice" + Write-Log "Disabled RDP" +} + +function disable-service { + Param([string]$Service) + + Write-Log "Starting to disable $Service" + Get-Service | Where-Object {$_.Name -eq $Service } | Set-Service -StartupType Disabled + Write-Log "Disabled $Service" +} + +function set-firewall { + Write-Log "Starting to set firewall rules" + Set-NetFirewallProfile -all -DefaultInboundAction Block -DefaultOutboundAction Allow -AllowUnicastResponseToMulticast False -Enabled True + check-firewall "public" + check-firewall "private" + check-firewall "domain" + Write-Log "Finished setting firewall rules" + + $MetadataServerAllowRules = Get-NetFirewallRule -Enabled True -Direction Outbound | Get-NetFirewallAddressFilter | Where-Object -FilterScript { $_.RemoteAddress -Eq '169.254.169.254' } + If ($MetadataServerAllowRules -Ne $null) { + Write-Log "Removing firewall rule that allows access to metadata server" + $MetadataServerAllowRules | Remove-NetFirewallRule + New-NetFirewallRule ` + -Name "Allow-GCEAgent-Metadata-Server" ` + -DisplayName "Allow GCEAgent to reach the GCP metadata server" ` + -Direction Outbound ` + -Action Allow ` + -RemoteAddress "169.254.169.254" ` + -Service "GCEAgent" + New-NetFirewallRule ` + -Name "Allow-BOSH-Agent-Metadata-Server" ` + -DisplayName "Allow BOSH Agent to reach the GCP metadata server" ` + -Direction Outbound ` + -Action Allow ` + -RemoteAddress "169.254.169.254" ` + -Service "bosh-agent" + } +} + +function get-firewall { + param([string] $profile) + + $firewall = (Get-NetFirewallProfile -Name $profile) + $result = "{0},{1},{2}" -f $profile,$firewall.DefaultInboundAction,$firewall.DefaultOutboundAction + return $result +} + +function check-firewall { + param([string] $profile) + + $firewall = (get-firewall $profile) + Write-Log $firewall + if ($firewall -ne "$profile,Block,Allow") { + Write-Log $firewall + Throw "Unable to set $profile Profile" + } +} + +<# +.Synopsis + Disables NetBIOS over TCP +.Description + This cmdlet disables NetBIOS over TCP by configuring the network interfaces + and by disabling all associated firewall rules. Additionally, the ports + used by NetBIOS over TCP are explicitly blocked. +#> +function Disable-NetBIOS { + + # Disable NetBIOS over TCP at the network interface level + + $NoInstances = $false + try { + WMIC.exe NICCONFIG WHERE '(TcpipNetbiosOptions=0 OR TcpipNetbiosOptions=1)' GET Caption,Index,TcpipNetbiosOptions *>&1 + } + catch { + $NoInstances = $true + } + + if ($NoInstances) { + Write-Log "NetBIOS over TCP is not enabled on any network interfaces" + } else { + # List Interfaces that will be changed + Write-Log "NetBIOS over TCP will be disabled on the following network interfaces:" + WMIC.exe NICCONFIG WHERE '(TcpipNetbiosOptions=0 OR TcpipNetbiosOptions=1)' GET Caption,Index,TcpipNetbiosOptions + + # Disable NetBIOS over TCP + WMIC.exe NICCONFIG WHERE '(TcpipNetbiosOptions=0 OR TcpipNetbiosOptions=1)' CALL SetTcpipNetbios 2 + } + + # Disable NetBIOS firewall rules + + $BuiltinNetBIOSRules=@( + "NETDIS-NB_Name-In-UDP", + "NETDIS-NB_Name-Out-UDP", + "NETDIS-NB_Datagram-In-UDP", + "NETDIS-NB_Datagram-Out-UDP", + "FPS-NB_Session-In-TCP", + "FPS-NB_Session-Out-TCP", + "FPS-NB_Name-In-UDP", + "FPS-NB_Name-Out-UDP", + "FPS-NB_Datagram-In-UDP", + "FPS-NB_Datagram-Out-UDP" + ) + foreach ($name in $BuiltinNetBIOSRules) { + Write-Log "Disabling firewall rule: $name" + Disable-NetFirewallRule -Name $name + } + + # Explicitly block NetBIOS Over TCP/IP: + # + # This blocks access to the below ports: + # + # - UDP port 137 (name services) + # - UDP port 138 (datagram services) + # - TCP port 139 (session services) + # + # source: https://technet.microsoft.com/en-us/library/cc940063.aspx + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Name-Disable-In-UDP")) { + Write-Log "Creating firewall rule: NB_Name-Disable-In-UDP" + New-NetFirewallRule ` + -Name "NB_Name-Disable-In-UDP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-In)" ` + -Direction Inbound ` + -Action Block ` + -Protocol UDP ` + -LocalPort 137 + } + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Name-Disable-Out-UDP")) { + Write-Log "Creating firewall rule: NB_Name-Disable-Out-UDP" + New-NetFirewallRule ` + -Name "NB_Name-Disable-Out-UDP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-Out)" ` + -Direction Outbound ` + -Action Block ` + -Protocol UDP ` + -RemotePort 137 + } + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Datagram-Disable-In-UDP")) { + Write-Log "Creating firewall rule: NB_Datagram-Disable-In-UDP" + New-NetFirewallRule ` + -Name "NB_Datagram-Disable-In-UDP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-In)" ` + -Direction Inbound ` + -Action Block ` + -Protocol UDP ` + -LocalPort 138 + } + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Datagram-Disable-Out-UDP")) { + Write-Log "Creating firewall rule: NB_Datagram-Disable-Out-UDP" + New-NetFirewallRule ` + -Name "NB_Datagram-Disable-Out-UDP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-Out)" ` + -Direction Outbound ` + -Action Block ` + -Protocol UDP ` + -RemotePort 138 + } + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Session-Disable-In-TCP")) { + Write-Log "Creating firewall rule: NB_Session-Disable-In-TCP" + New-NetFirewallRule ` + -Name "NB_Session-Disable-In-TCP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-In)" ` + -Direction Inbound ` + -Action Block ` + -Protocol TCP ` + -LocalPort 139 + } + + if (-Not ((Get-NetFirewallRule).Name -contains "NB_Session-Disable-Out-TCP")) { + Write-Log "Creating firewall rule: NB_Session-Disable-Out-TCP" + New-NetFirewallRule ` + -Name "NB_Session-Disable-Out-TCP" ` + -DisplayName "Disable File and Printer Sharing (NB-Session-Out)" ` + -Direction Outbound ` + -Action Block ` + -Protocol TCP ` + -RemotePort 139 + } + + $ExplicitBlockNetBIOSRules=@( + "NB_Name-Disable-In-UDP", + "NB_Name-Disable-Out-UDP", + "NB_Datagram-Disable-In-UDP", + "NB_Datagram-Disable-Out-UDP", + "NB_Session-Disable-In-TCP", + "NB_Session-Disable-Out-TCP" + ) + foreach ($name in $ExplicitBlockNetBIOSRules) { + Write-Log "Enabling firewall rule: $name" + Enable-NetFirewallRule -Name $name + } + + Write-Log "Disable-NetBIOS: Complete" +} + +function Remove-DockerPackage { + $dockerPackage = Get-Package -Name DockerMsftProvider -ErrorAction ignore + + if ($dockerPackage -eq $null) { + Write-Log "Docker is not installed. No need to remove." + return + } + + Write-Log "Uninstalling Docker: Starting" + Uninstall-Package -Name docker -ProviderName DockerMsftProvider + Uninstall-Module -Name DockerMsftProvider + + Write-Log "Uninstalling Docker: HNSNetworks" + Get-HNSNetwork | Remove-HNSNetwork + + Write-Log "Uninstalling Docker: ProgramData" + cmd.exe /c rmdir /s /q "C:\ProgramData\Docker" + + Write-Log "Uninstalling Docker: Complete" +} diff --git a/modules/BOSH.Disk/BOSH.Disk.psd1 b/modules/BOSH.Disk/BOSH.Disk.psd1 new file mode 100644 index 00000000..4ba773fe --- /dev/null +++ b/modules/BOSH.Disk/BOSH.Disk.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.Disk' +ModuleVersion = '0.1' +GUID = '55e568fc-6388-45de-99b7-273b75be75d0' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Commands for Disk Utilities on a BOSH deployed vm' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils') +FunctionsToExport = @('Compress-Disk','Optimize-Disk') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('BOSH') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.Disk/BOSH.Disk.psm1 b/modules/BOSH.Disk/BOSH.Disk.psm1 new file mode 100644 index 00000000..916d17a1 --- /dev/null +++ b/modules/BOSH.Disk/BOSH.Disk.psm1 @@ -0,0 +1,75 @@ +<# +.Synopsis + Install BOSH Disk Utilities +.Description + This cmdlet installs the Disk Utilities for BOSH deployed vm +#> + +function Compress-Disk { + Write-Log "Starting to compress disk" + DefragDisk + ZeroDisk + DefragDisk # Just for good measure + Write-Log "Finished compressing disk" +} + +function Optimize-Disk { + Write-Log "Starting to clean disk" + + Get-WindowsFeature | + ? { $_.InstallState -eq 'Available' } | + Uninstall-WindowsFeature -Remove + + # Cleanup WinSxS folder: https://technet.microsoft.com/en-us/library/dn251565.aspx + Write-Log "Running Dism" + Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase + if ($LASTEXITCODE -ne 0) { + Write-Log "Error: Running Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase" + Throw "Dism.exe failed" + } + Dism.exe /online /Cleanup-Image /SPSuperseded + if ($LASTEXITCODE -ne 0) { + Write-Log "Error: Running Dism.exe /online /Cleanup-Image /SPSuperseded" + Throw "Dism.exe failed" + } + Write-Log "Finished clean disk" +} + +function DefragDisk { + # First - get the volumes via WMI + $volumes = gwmi win32_volume + + # Now get the C:\ volume + $v1 = $volumes | where {$_.name -eq "C:\"} + + # Perform a defrag analysis + $v1.defraganalysis().defraganalysis + + Write-Log "DefragDisk: Volume: ${v1}" + $v1.defrag($true) + + Write-Log "DefragDisk: Redo Defrag analysis: ${v1}" + $v1.defraganalysis().defraganalysis +} + +function ZeroDisk { + $Success = $TRUE + $FilePath = "C:\zero.tmp" + $Volume = Get-WmiObject win32_logicaldisk -filter "DeviceID='C:'" + $ArraySize = 64kb + $SpaceToLeave = $Volume.Size * 0.005 + $FileSize = $Volume.FreeSpace - $SpacetoLeave + $ZeroArray = New-Object byte[]($ArraySize) + + Write-Log "Zeroing volume: $Volume" + $Stream = [io.File]::OpenWrite($FilePath) + $CurFileSize = 0 + while ($CurFileSize -lt $FileSize) { + $Stream.Write($ZeroArray, 0, $ZeroArray.Length) + $CurFileSize +=$ZeroArray.Length + } + if ($Stream) { + $Stream.Close() + } + Remove-Item -Path $FilePath -Force +} diff --git a/modules/BOSH.Registry/BOSH.Registry.Tests.ps1 b/modules/BOSH.Registry/BOSH.Registry.Tests.ps1 new file mode 100644 index 00000000..b4948476 --- /dev/null +++ b/modules/BOSH.Registry/BOSH.Registry.Tests.ps1 @@ -0,0 +1,82 @@ +Remove-Module -Name BOSH.Registry -ErrorAction Ignore +Import-Module ./BOSH.Registry.psm1 + +Describe "BOSH.Registry" { + BeforeEach { + $newItemReturn = [pscustomobject]@{"NewPath" = "HKCU:/Path/created";} + Mock New-Item { $newItemReturn } -ModuleName BOSH.Registry + # reset for our -parameterfilter mock + Mock New-Item { $newItemReturn } -ModuleName BOSH.Registry -ParameterFilter { $PSBoundParameters['ErrorAction'] -eq "Stop" } + } + + It "Set-InternetExplorerRegistries applies internet explorer settings when valid policy files are generated" { + Mock Invoke-LGPO-Build-Pol-From-Text { 0 } -ModuleName BOSH.Registry + Mock Invoke-LGPO-Apply-Policies { 0 } -ModuleName BOSH.Registry + + Set-InternetExplorerRegistries + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 2 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 1 -Scope It -ModuleName BOSH.Registry + } + It "Set-InternetExplorerRegistries errors out when policy application fails" { + Mock Invoke-LGPO-Build-Pol-From-Text { 0 } -ModuleName BOSH.Registry + Mock Invoke-LGPO-Apply-Policies { 1 } -ModuleName BOSH.Registry + + { Set-InternetExplorerRegistries } | Should -Throw "Error Applying IE policy:" + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 2 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 1 -Scope It -ModuleName BOSH.Registry + } + + It "Set-InternetExplorerRegistries errors out when User policy generation fails and does not attempt policy application" { + Mock Invoke-LGPO-Build-Pol-From-Text { 0 } -ModuleName BOSH.Registry -ParameterFilter { + $LGPOTextReadPath -like "*machine.txt" + } + Mock Invoke-LGPO-Build-Pol-From-Text { 1 } -ModuleName BOSH.Registry -ParameterFilter { + $LGPOTextReadPath -like "*user.txt" + } + + { Set-InternetExplorerRegistries } | Should -Throw "Generating IE policy: User" + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 2 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 0 -Scope It -ModuleName BOSH.Registry + } + + It "Set-InternetExplorerRegistries errors out when Machine policy generation fails and does not attempt policy application" { + Mock Invoke-LGPO-Build-Pol-From-Text { 1 } -ModuleName BOSH.Registry -ParameterFilter { + $LGPOTextReadPath -like "*machine.txt" + } + + { Set-InternetExplorerRegistries } | Should -Throw "Generating IE policy: Machine" + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 1 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 0 -Scope It -ModuleName BOSH.Registry + } + + It "Set-InternetExplorerRegistries doesn't call Invoke-LGPO-Build-Pol-From-Text if New-Item call for Machine Directory fails" { + # ErrorAction Parameterfilter is present to ensure we only throw an error on a New-Item call that is configured to throw errors + Mock New-Item { Throw 'some error' } -ModuleName BOSH.Registry -ParameterFilter { + $Path -like "*Machine" -and + $PSBoundParameters['ErrorAction'] -eq "Stop" + } + + { Set-InternetExplorerRegistries } | Should -Throw + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 0 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 0 -Scope It -ModuleName BOSH.Registry + } + + + It "Set-InternetExplorerRegistries doesn't call Invoke-LGPO-Build-Pol-From-Text if New-Item call for User Directory fails" { + # ErrorAction Parameterfilter is present to ensure we only throw an error on a New-Item call that is configured to throw errors + Mock New-Item { Throw 'some error' } -ModuleName BOSH.Registry -ParameterFilter { + $Path -like "*User" -and + $PSBoundParameters['ErrorAction'] -eq "Stop" + } + + { Set-InternetExplorerRegistries } | Should -Throw + + Assert-MockCalled Invoke-LGPO-Build-Pol-From-Text -Exactly 1 -Scope It -ModuleName BOSH.Registry + Assert-MockCalled Invoke-LGPO-Apply-Policies -Exactly 0 -Scope It -ModuleName BOSH.Registry + } +} diff --git a/modules/BOSH.Registry/BOSH.Registry.psd1 b/modules/BOSH.Registry/BOSH.Registry.psd1 new file mode 100644 index 00000000..b3434744 --- /dev/null +++ b/modules/BOSH.Registry/BOSH.Registry.psd1 @@ -0,0 +1,23 @@ +@{ + RootModule = 'BOSH.Registry' + ModuleVersion = '0.1' + GUID = '5b414c84-d454-4752-9e59-1532f78836e5' + Author = 'BOSH' + Copyright = '(c) 2019 BOSH' + Description = 'Install Microsoft SSHD' + PowerShellVersion = '4.0' + FunctionsToExport = @( + 'Set-RegistryProperty', + 'Set-InternetExplorerRegistries' + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @('BOSH', 'Registry') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } + } +} diff --git a/modules/BOSH.Registry/BOSH.Registry.psm1 b/modules/BOSH.Registry/BOSH.Registry.psm1 new file mode 100644 index 00000000..04aebc81 --- /dev/null +++ b/modules/BOSH.Registry/BOSH.Registry.psm1 @@ -0,0 +1,70 @@ +function Invoke-LGPO-Build-Pol-From-Text { + param( + [Parameter(Mandatory=$True)] + [String] + $LGPOTextReadPath, + + [Parameter(Mandatory=$True)] + [String] + $RegistryPolWritePath + ) + process { + LGPO.exe /r $LGPOTextReadPath /w $RegistryPolWritePath + return $LASTEXITCODE + } +} + +function Invoke-LGPO-Apply-Policies { + param( + [Parameter(Mandatory=$True)] + [String] + $RegistryPolPath + ) + process { + LGPO.exe /g $RegistryPolPath + return $LASTEXITCODE + } +} + +function Set-InternetExplorerRegistries { + <# + .SYNOPSIS + Apply BOSH Windows Stemcell registry settings related to internet explorer + .DESCRIPTION + Apply Internet Explorer registry settings taken from Microsoft's baseline security analysis tool + .INPUTS + None. You can't pipe anything in to this command + .OUTPUTS + Set-InternetExplorerRegistries will return any failure output + #> + + [CmdletBinding()] + + param() + + process { + Write-Log "Starting Internet Explorer Registry Changes" + $IePolicyPath = Join-Path $PSScriptRoot "data\IE-Policies" + + $MachineDir="$IePolicyPath\DomainSysvol\GPO\Machine" + + New-Item -ItemType Directory -Path $MachineDir -Force -ErrorAction "Stop" + $machinePolicyExitCode = Invoke-LGPO-Build-Pol-From-Text -LGPOTextReadPath "$IePolicyPath\machine.txt" -RegistryPolWritePath "$MachineDir\registry.pol" + if ($machinePolicyExitCode -ne 0) { + Throw "Generating IE policy: Machine" + } + + $UserDir="$IePolicyPath\DomainSysvol\GPO\User" + New-Item -ItemType Directory -Path $UserDir -Force -ErrorAction "Stop" + $userPolicyExitCode = Invoke-LGPO-Build-Pol-From-Text -LGPOTextReadPath "$IePolicyPath\user.txt" -RegistryPolWritePath "$UserDir\registry.pol" + if ($userPolicyExitCode -ne 0) { + Throw "Generating IE policy: User" + } + + # Apply policies + $policyApplicationExitCode = Invoke-LGPO-Apply-Policies -RegistryPolPath $IePolicyPath + if ($policyApplicationExitCode -ne 0) { + Throw "Error Applying IE policy: $IePolicyPath" + } + } +} \ No newline at end of file diff --git a/modules/BOSH.Registry/data/IE-Policies/.gitignore b/modules/BOSH.Registry/data/IE-Policies/.gitignore new file mode 100644 index 00000000..452e8e97 --- /dev/null +++ b/modules/BOSH.Registry/data/IE-Policies/.gitignore @@ -0,0 +1,2 @@ + +DomainSysvol/* \ No newline at end of file diff --git a/modules/BOSH.Registry/data/IE-Policies/machine.txt b/modules/BOSH.Registry/data/IE-Policies/machine.txt new file mode 100755 index 0000000000000000000000000000000000000000..50a4b359b2a81b92f4223369c600b1372b92c402 GIT binary patch literal 29532 zcmeHQVQ(5o5S`DJ`XBh%euxDmc9QlJKH~<}Hps+w5+NZB#6fYf1;mc?N$de8F`Bi=q_NgHiIhLl>63$3!lNKvd8?*;^H-;c#rnCcoy&HQ`w6Vu8*hhwd71cqlfcb?OUSA*`CNw z@jAlw0p1_t33)8UJJCKI1QxF&;yF{<)4|PuuO`GwZO*9!9jdQ3z(y zCP321czwJFLupA2`7xYlmn^diIc6D;ZZW=OTL-M9)y)dL`v%v@yB?S@0DEYMiCUK| zWZNIH#ClPr$Nn>{@I3@gc?TBVp@kuO_*__B|M&6feT;G#Q_j-Bc(T8%_8Ct{o^wBI zv$P$49-F8+R3wgJ4H&2|WQ@)I9eF0ZIJUXIc|G1k-_qmWfZ7FBB#JEGH+7uZPh&L^ zRPoCz{0(Tr+GQ=X8oxST2R^if4F9Gb><8Bdt|OON6ZX-|3iSR2+EbM_RHX(46|m(% z&kyBqTy5apI$G6%gDWn3{~NAx=5md>(K+J z!z*#4KLqPJU)%Bu+&{uBpjSz+lT3enh&?iVv5mc{**COJ@VEw@-+wfWo`={sqvs*^ z#%TH{P52~BW{-I9X5?Ug z3}HMtX7kvm@>2C>Gi+d46lP?Y-wzo(%SRr8F$v!3AkXpvoMDv$v)ZB6=2)Pq{bJn( zvy6As@l9v-vMqQ%Q6xj;x5vO4GWJ}rhW$QyK$kfdCgV{TdcOAXEgxyXwqgT=Wv?dgfl`xx! zV{x4e5t>R{T~7hN^DNhhIBVOsd{O%`r`d)DJJ9Vm?&~R|vz%N{elu&Y_29e$vY;Tu z>Jj5B5uCItw>IkY=+SURY)w&= zz}MpRIPJg0VZr+_+N$mwV_9dPZ;VeBV;Ra*6tk&6u0DG-7-Q#ZP*K)oUaQqk5Gy8# ztHv6QG4}7M&gLs!W6{-s(a%beM^u*Q(33>ALD^9$Ccb15sK1Jd2KeATrN2pP7zb8O%TRVk53c zc^kzTr3as38;J7rC}u?Tf()G(*3oD1G>^)rvkDx2AgS-s=V^rUxE%nH;6 zUlCskk;FXpj(Gt3xoRjD_2AJIfwE7z82 zKpd$rr zhN9(jq00uPr+bCAPj~BQthQRE#m2oxx1+b6?)IFW1djjJ3q&g8zXoj_xFBCT}GFb1$JE!TTKeQa!+0 q;KHhG9c%7~(PohTopP6X- "$dir\OpenSSH-Win64\install-sshd.ps1" + echo "fake sshd" > "$dir\OpenSSH-Win64\sshd.exe" + echo "fake config" > "$dir\OpenSSH-Win64\sshd_config_default" + + Compress-Archive -Force -Path "$dir\OpenSSH-Win64" -DestinationPath $fakeZipPath +} + +Describe "Enable-SSHD" { + BeforeEach { + Mock Set-Service { } -ModuleName BOSH.SSH + Mock Run-LGPO { } -ModuleName BOSH.SSH + + $guid = $( New-Guid ).Guid + $TMP_DIR = "$env:TEMP\BOSH.SSH.Tests-$guid" + + $FAKE_ZIP = "$TMP_DIR\OpenSSH-TestFake.zip" + $INSTALL_SCRIPT_SPY_STATUS = "$TMP_DIR\install-script-status" + + CreateFakeOpenSSHZip -dir $TMP_DIR -installScriptSpyStatus $INSTALL_SCRIPT_SPY_STATUS -fakeZipPath $FAKE_ZIP + + mkdir -p "$TMP_DIR\Windows\Temp" + echo "fake LGPO" > "$TMP_DIR\Windows\LGPO.exe" + + $ORIGINAL_WINDIR = $env:WINDIR + $env:WINDIR = "$TMP_DIR\Windows" + + $ORIGINAL_PROGRAMDATA = $env:ProgramData + $env:PROGRAMDATA = "$TMP_DIR\ProgramData" + } + + AfterEach { + rmdir $TMP_DIR -Recurse -ErrorAction Ignore + $env:WINDIR = $ORIGINAL_WINDIR + $env:PROGRAMDATA = $ORIGINAL_PROGRAMDATA + } + + It "sets the startup type of sshd to automatic" { + Mock Set-Service { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $Name -eq "sshd" -and $StartupType -eq "Automatic" } + + Enable-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "sets the startup type of ssh-agent to automatic" { + Mock Set-Service { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $Name -eq "ssh-agent" -and $StartupType -eq "Automatic" } + + Enable-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "sets up firewall when ssh not already set up" { + Mock Get-NetFirewallRule { + return [ordered]@{ + "Name" = "{3c06039b-ece1-4da3-8ece-255894975894}" + "DisplayName" = "NTP" + "Description" = "" + "DisplayGroup" = "" + "Group" = "" + "Enabled" = "True" + "Profile" = "Any" + "Platform" = "{}" + "Direction" = "Outbound" + "Action" = "Allow" + "EdgeTraversalPolicy" = "Block" + "LooseSourceMapping" = "False" + "LocalOnlyMapping" = "False" + "Owner" = "" + "PrimaryStatus" = "OK" + "Status" = "The rule was parsed successfully from the store. (65536)" + "EnforcementStatus" = "NotApplicable" + "PolicyStoreSource" = "PersistentStore" + "PolicyStoreSourceType" = "Local" + } + } -ModuleName BOSH.SSH + + Mock New-NetFirewallRule { } -ModuleName BOSH.SSH + Enable-SSHD -SSHZipFile $FAKE_ZIP + Assert-MockCalled New-NetFirewallRule -Times 1 -ModuleName BOSH.SSH -Scope It + } + + It "doesn't set up firewall when ssh is already set up " { + Mock Get-NetFirewallRule { + return [ordered]@{ + "Name" = "{ E02857AB-8EA8-4358-8119-ED7D20DA7712 }" + "DisplayName" = "SSH" + "Description" = "" + "DisplayGroup" = "" + "Group" = "" + "Enabled" = "True" + "Profile" = "Any" + "Platform" = "{ }" + "Direction" = "Inbound" + "Action" = "Allow" + "EdgeTraversalPolicy" = "Block" + "LooseSourceMapping" = "False" + "LocalOnlyMapping" = "False" + "Owner" = "" + "PrimaryStatus" = "OK" + "Status" = "The rule was parsed successfully from the store. (65536)" + "EnforcementStatus" = "NotApplicable" + "PolicyStoreSource" = "PersistentStore" + "PolicyStoreSourceType" = "Local" + } + } -ModuleName BOSH.SSH + + Mock New-NetFirewallRule { } -ModuleName BOSH.SSH + Enable-SSHD -SSHZipFile $FAKE_ZIP + Assert-MockCalled New-NetFirewallRule -Times 0 -ModuleName BOSH.SSH -Scope It + } + + It "Generates inf and invokes LGPO if LGPO exists" { + Mock Run-LGPO -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $LGPOPath -eq "$TMP_DIR\Windows\LGPO.exe" -and $InfFilePath -eq "$TMP_DIR\Windows\Temp\enable-ssh.inf" } + + Enable-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "Skips LGPO if LGPO.exe not found" { + rm "$TMP_DIR\Windows\LGPO.exe" + + Enable-SSHD -SSHZipFile $FAKE_ZIP + + Assert-MockCalled Run-LGPO -Times 0 -ModuleName BOSH.SSH -Scope It + } + + Context "When LGPO executable fails" { + It "Throws an appropriate error" { + Mock Run-LGPO { throw "some error" } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $LGPOPath -eq "$TMP_DIR\Windows\LGPO.exe" -and $InfFilePath -eq "$TMP_DIR\Windows\Temp\enable-ssh.inf" } + { Enable-SSHD -SSHZipFile $FAKE_ZIP } | Should -Throw "LGPO.exe failed with: some error" + } + } + + It "removes existing SSH keys" { + New-Item -ItemType Directory -Path "$TMP_DIR\ProgramData\ssh" -ErrorAction Ignore + echo "delete" > "$TMP_DIR\ProgramData\ssh\ssh_host_1" + echo "delete" > "$TMP_DIR\ProgramData\ssh\ssh_host_2" + echo "delete" > "$TMP_DIR\ProgramData\ssh\ssh_host_3" + echo "ignore" > "$TMP_DIR\ProgramData\ssh\not_ssh_host_4" + + Enable-SSHD -SSHZipFile $FAKE_ZIP + + $numHosts = (Get-ChildItem "$TMP_DIR\ProgramData\ssh\").count + $numHosts | Should -eq 1 + } + + It "creates empty ssh program dir if it doesn't exist" { + Enable-SSHD -SSHZipFile $FAKE_ZIP + { Test-Path "$TMP_DIR\ProgramData\ssh" } | Should -eq $True + } +} + +Describe "Install-SSHD" { + BeforeEach { + Mock Set-Service { } -ModuleName BOSH.SSH + Mock Protect-Dir { } -ModuleName BOSH.SSH + Mock Invoke-CACL { } -ModuleName BOSH.SSH + Mock Write-Log { } -ModuleName BOSH.Utils + + $guid = $( New-Guid ).Guid + $TMP_DIR = "$env:TEMP\BOSH.SSH.Tests-$guid" + + mkdir -p "$TMP_DIR\Windows\Temp" + mkdir -p "$TMP_DIR\ProgramData" + + $FAKE_ZIP = "$TMP_DIR\OpenSSH-TestFake.zip" + $INSTALL_SCRIPT_SPY_STATUS = "$TMP_DIR\install-script-status" + + CreateFakeOpenSSHZip -dir $TMP_DIR -installScriptSpyStatus $INSTALL_SCRIPT_SPY_STATUS -fakeZipPath $FAKE_ZIP + + $ORIGINAL_PROGRAMFILES = $env:PROGRAMFILES + $env:PROGRAMFILES = "$TMP_DIR\ProgramFiles" + } + + AfterEach { + rmdir $TMP_DIR -Recurse -ErrorAction Ignore + $env:PROGRAMFILES = $ORIGINAL_PROGRAMFILES + } + + It "extracts OpenSSH to Program Files" { + Install-SSHD -SSHZipFile $FAKE_ZIP + + Get-Item $env:PROGRAMFILES\OpenSSH | Should -Exist + Get-Item $env:PROGRAMFILES\OpenSSH\sshd.exe | Should -Exist + } + + It "runs the install-sshd script" { + Install-SSHD -SSHZipFile $FAKE_ZIP + + "$INSTALL_SCRIPT_SPY_STATUS" | Should -FileContentMatchExactly 'installed' + } + + It "calls Protect-Dir to lock down permissions" { + Mock Protect-Dir { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $path -eq "$env:PROGRAMFILES\OpenSSH" } + + Install-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "calls Invoke-CACL with expected files" { + Mock Invoke-CACL { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { + @( + "libcrypto.dll", + "scp.exe", + "sftp-server.exe", + "sftp.exe", + "ssh-add.exe", + "ssh-agent.exe", + "ssh-keygen.exe", + "ssh-keyscan.exe", + "ssh-shellhost.exe", + "ssh.exe", + "sshd.exe" + ) + } + + Install-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "sets the startup type of sshd to disabled" { + Mock Set-Service { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $Name -eq "sshd" -and $StartupType -eq "Disabled" } + + Install-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "sets the startup type of ssh-agent to disabled" { + Mock Set-Service { } -Verifiable -ModuleName BOSH.SSH -ParameterFilter { $Name -eq "ssh-agent" -and $StartupType -eq "Disabled" } + + Install-SSHD -SSHZipFile $FAKE_ZIP + + Assert-VerifiableMock + } + + It "modifies the openssh configuration to remove default admin key location while maintaining UTF-8 encoding" { + Mock Get-Content { @" +Match Group administrators +AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys +"@ } -ModuleName BOSH.SSH -ParameterFilter { $Path -like "*sshd_config_default" } + + Install-SSHD -SSHZipFile $FAKE_ZIP + Get-Content $env:PROGRAMFILES\OpenSSH\sshd_config_default | Out-String | Should -BeLike "#*#*" + Get-FileEncoding $env:PROGRAMFILES\OpenSSH\sshd_config_default | Should -BeLike "System.Text.UTF8Encoding" + } + +} + +Describe "Modify-DefaultOpenSSHConfig"{ + It "Comments out default configuration for where administrator keys are stored" { + + Mock Get-Content { +@" +Match Group administrators +AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys +"@ + } -ModuleName BOSH.SSH + + $result = Modify-DefaultOpenSSHConfig -ConfigPath "some/path/sshd_config_default" + + Assert-MockCalled Get-Content -Times 1 -ModuleName BOSH.SSH -Scope It -ParameterFilter { $Path -like "*sshd_config_default" } + $result | Should -BeLike "#*#*" + } + +} diff --git a/modules/BOSH.SSH/BOSH.SSH.psd1 b/modules/BOSH.SSH/BOSH.SSH.psd1 new file mode 100644 index 00000000..1fe1a24f --- /dev/null +++ b/modules/BOSH.SSH/BOSH.SSH.psd1 @@ -0,0 +1,22 @@ +@{ +RootModule = 'BOSH.SSH' +ModuleVersion = '0.1' +GUID = '50c1c4b1-e154-4b07-92bc-718a3efba6b3' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Install Microsoft SSHD' +PowerShellVersion = '4.0' +FunctionsToExport = @('Install-SSHD', +'Enable-SSHD', +'Remove-SSHKeys') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('BOSH', 'SSHD') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.SSH/BOSH.SSH.psm1 b/modules/BOSH.SSH/BOSH.SSH.psm1 new file mode 100644 index 00000000..157a9ad5 --- /dev/null +++ b/modules/BOSH.SSH/BOSH.SSH.psm1 @@ -0,0 +1,143 @@ +function Install-SSHD +{ + param ( + [string]$SSHZipFile = $( Throw "Provide an SSHD zipfile" ) + ) + + New-Item "$env:PROGRAMFILES\SSHTemp" -Type Directory -Force + Open-Zip -ZipFile $SSHZipFile -OutPath "$env:PROGRAMFILES\SSHTemp" + + $ConfigPath = "$env:PROGRAMFILES\SSHTemp\OpenSSH-Win64\sshd_config_default" + $ModifiedConfigContents = Modify-DefaultOpenSSHConfig -ConfigPath $ConfigPath + Remove-Item -Force $ConfigPath + Out-File -FilePath $ConfigPath -InputObject $ModifiedConfigContents -Encoding UTF8 + + Move-Item -Force "$env:PROGRAMFILES\SSHTemp\OpenSSH-Win64" "$env:PROGRAMFILES\OpenSSH" + Remove-Item -Force "$env:PROGRAMFILES\SSHTemp" + + # Remove users from 'OpenSSH' before installing. The install process + # will add back permissions for the NT AUTHORITY\Authenticated Users for some files + Protect-Dir -path "$env:PROGRAMFILES\OpenSSH" + + Push-Location "$env:PROGRAMFILES\OpenSSH" + powershell -ExecutionPolicy Bypass -File install-sshd.ps1 + Pop-Location + + # # Grant NT AUTHORITY\Authenticated Users access to .EXEs and the .DLL in OpenSSH + $FileNames = @( + "libcrypto.dll", + "scp.exe", + "sftp-server.exe", + "sftp.exe", + "ssh-add.exe", + "ssh-agent.exe", + "ssh-keygen.exe", + "ssh-keyscan.exe", + "ssh-shellhost.exe", + "ssh.exe", + "sshd.exe" + ) + Invoke-CACL -FileNames $FileNames + + Set-Service -Name sshd -StartupType Disabled + # ssh-agent is not the same as ssh-agent in *nix openssh + Set-Service -Name ssh-agent -StartupType Disabled +} + +function Enable-SSHD +{ + if ((Get-NetFirewallRule | where { $_.DisplayName -ieq 'SSH' }) -eq $null) + { + "Creating firewall rule for SSH" + New-NetFirewallRule -Protocol TCP -LocalPort 22 -Direction Inbound -Action Allow -DisplayName SSH + } + else + { + "Firewall rule for SSH already exists" + } + + $InfFilePath = "$env:WINDIR\Temp\enable-ssh.inf" + + $InfFileContents = @' +[Unicode] +Unicode=yes +[Version] +signature=$CHICAGO$ +Revision=1 +[Registry Values] +[System Access] +[Privilege Rights] +SeDenyNetworkLogonRight=*S-1-5-32-546 +SeAssignPrimaryTokenPrivilege=*S-1-5-19,*S-1-5-20,*S-1-5-80-3847866527-469524349-687026318-516638107-1125189541 +'@ + $LGPOPath = "$env:WINDIR\LGPO.exe" + if (Test-Path $LGPOPath) + { + Out-File -FilePath $InfFilePath -Encoding unicode -InputObject $InfFileContents -Force + Try + { + Run-LGPO -LGPOPath $LGPOPath -InfFilePath $InfFilePath + } + Catch + { + throw "LGPO.exe failed with: $_.Exception.Message" + } + } + else + { + "Did not find $LGPOPath. Assuming existing security policies are sufficient to support ssh." + } + + Set-Service -Name sshd -StartupType Automatic + # ssh-agent is not the same as ssh-agent in *nix openssh + Set-Service -Name ssh-agent -StartupType Automatic + + Remove-SSHKeys +} + +function Remove-SSHKeys +{ + $SSHDir = "C:\Program Files\OpenSSH" + + Push-Location $SSHDir + New-Item -ItemType Directory -Path "$env:ProgramData\ssh" -ErrorAction Ignore + + "Removing any existing host keys" + Remove-Item -Path "$env:ProgramData\ssh\ssh_host_*" -ErrorAction Ignore + Pop-Location +} + +function Invoke-CACL +{ + param ( + [string[]] $FileNames = $( Throw "Files not provided" ) + ) + + foreach ($name in $FileNames) + { + $path = Join-Path "$env:PROGRAMFILES\OpenSSH" $name + cacls.exe $Path /E /P "NT AUTHORITY\Authenticated Users:R" + } +} + +function Run-LGPO +{ + param ( + [string]$LGPOPath = $( Throw "Provide LGPO path" ), + [string]$InfFilePath = $( Throw "Provide Inf file path" ) + ) + & $LGPOPath /s $InfFilePath +} + +function Modify-DefaultOpenSSHConfig +{ + param ( + [string]$ConfigPath = $( Throw "Provide openssh default config path" ) + ) + + $ModifiedConfig = Get-Content $ConfigPath ` + | %{$_ -replace ".*Match Group administrators.*", "#$&"} ` + | %{$_ -replace ".*AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys.*", "#$&" } + + return $ModifiedConfig +} diff --git a/modules/BOSH.Sysprep/BOSH.Sysprep.Tests.ps1 b/modules/BOSH.Sysprep/BOSH.Sysprep.Tests.ps1 new file mode 100644 index 00000000..b60deb82 --- /dev/null +++ b/modules/BOSH.Sysprep/BOSH.Sysprep.Tests.ps1 @@ -0,0 +1,619 @@ +Remove-Module -Name BOSH.Sysprep -ErrorAction Ignore +Import-Module ./BOSH.Sysprep.psm1 + +#We remove WinRM as it imports BOSH.Utils +Remove-Module -Name BOSH.WinRM -ErrorAction Ignore + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ../BOSH.Utils/BOSH.Utils.psm1 + + +function New-TempDir +{ + $parent = [System.IO.Path]::GetTempPath() + [string]$name = [System.Guid]::NewGuid() + (New-Item -ItemType Directory -Path (Join-Path $parent $name)).FullName +} + +Describe "Remove-WasPassProcessed" { + Context "when no answer file path is provided" { + It "throws" { + { Remove-WasPassProcessed } | Should -Throw "Cannot bind argument to parameter 'Path' because it is an empty string." + } + } + + Context "when provided a nonexistent answer file" { + It "throws" { + { Remove-WasPassProcessed -AnswerFilePath "C:\IDoNotExist.xml" } | Should -Throw "Answer file C:\IDoNotExist.xml does not exist" + } + } + + Context "when provided an answer file with invalid XML" { + BeforeEach { + $BadAnswerXmlDirectory = (New-TempDir) + $BadAnswerXmlPath = Join-Path $BadAnswerXmlDirectory "bad.xml" + + "bad xml" | Out-File $BadAnswerXmlPath + } + + AfterEach { + Remove-Item -Recurse -Force $BadAnswerXmlDirectory + } + + It "throws" { + { Remove-WasPassProcessed -AnswerFilePath $BadAnswerXmlPath } | Should -Throw "Cannot convert value `"bad xml`" to type `"System.Xml.XmlDocument`". Error: `"The specified node cannot be inserted as the valid child of this node, because the specified node is the wrong type.`"" + } + } + + Context "when provided an answer file which contains valid XML and a 'specialize' block containing 'Microsoft-Windows-Deployment' which has the attribute 'wasPassProcessed'" { + BeforeEach { + $answerFileDirectory = (New-TempDir) + $answerFilePath = Join-Path $answerFileDirectory "validanswer.xml" + + " + + + + + + + + + + + + + + + + + " | Out-File $answerFilePath + } + + AfterEach { + Remove-Item -Recurse -Force $answerFileDirectory + } + + It "removes the attribute regardless of its value" { + Remove-WasPassProcessed $answerFilePath + $content = [xml](Get-Content $answerFilePath) + + foreach ($specializeBlock in $content.unattend.settings) + { + $specializeBlock.HasAttribute("wasPassProcessed") | Should Be False + } + } + } + + Context "when processing several 'specialize' blocks which have the attribute 'wasProcessed = true'" { + BeforeEach { + $GoodAnswerFileDirectory = (New-TempDir) + $GoodAnswerFilePath = Join-Path $GoodAnswerFileDirectory "validanswer.xml" + + " + + + + + " | Out-File $GoodAnswerFilePath + } + + AfterEach { + Remove-Item -Recurse -Force $GoodAnswerFileDirectory + } + + It "does nothing" { + Remove-WasPassProcessed $GoodAnswerFilePath + $content = [xml](Get-Content $GoodAnswerFilePath) + $mwdBlock = ((($content.unattend.settings|where { $_.pass -eq 'specialize' }).component|where { $_.name -eq "Microsoft-Windows-Deployment" })) + $specializeBlock = $mwdBlock.ParentNode + $specializeBlock.hasAttribute("wasPassProcessed") | Should Be False + } + } +} + +Describe "Remove-UserAccounts" { + Context "when no answer file path is provided" { + It "throws" { + { Remove-UserAccounts } | Should -Throw "Cannot bind argument to parameter 'Path' because it is an empty string." + } + } + + Context "when provided a nonexistent answer file" { + It "throws" { + { Remove-UserAccounts -AnswerFilePath "C:\IDoNotExist.xml" } | Should -Throw "Answer file C:\IDoNotExist.xml does not exist" + } + } + + Context "when provided an answer file with invalid XML" { + BeforeEach { + $BadAnswerXmlDirectory = (New-TempDir) + $BadAnswerXmlPath = Join-Path $BadAnswerXmlDirectory "bad.xml" + + "bad xml" | Out-File $BadAnswerXmlPath + } + + AfterEach { + Remove-Item -Recurse -Force $BadAnswerXmlDirectory + } + + It "throws" { + { Remove-UserAccounts -AnswerFilePath $BadAnswerXmlPath } | Should -Throw "Cannot convert value `"bad xml`" to type `"System.Xml.XmlDocument`". Error: `"The specified node cannot be inserted as the valid child of this node, because the specified node is the wrong type.`"" + } + } + + Context "when provided an answer file containing XML but without an 'oobeSystem' block containing 'Microsoft-Windows-Shell-Setup'" { + BeforeEach { + $noOOBEXmlDirectory = (New-TempDir) + $noOOBEXmlPath = Join-Path $noOOBEXmlDirectory "invalidanswer.xml" + + " + + + + + " | Out-File $noOOBEXmlPath + } + + AfterEach { + Remove-Item -Recurse -Force $noOOBEXmlDirectory + } + + It "does nothing" { + { Remove-UserAccounts -AnswerFilePath $noOOBEXmlPath } | Should -Throw "Could not locate oobeSystem XML block. You may not be running this function on an answer file." + } + } + + Context "when provided a valid XML answer file containing an 'oobeSystem' block which contains a 'Microsoft-Windows-Shell-Setup' block which DOES NOT contain a 'UserAccounts' block" { + BeforeEach { + $GoodAnswerFileDirectory = (New-TempDir) + $GoodAnswerFilePath = Join-Path $GoodAnswerFileDirectory "validanswer.xml" + + " + + + + + " | Out-File $GoodAnswerFilePath + } + + AfterEach { + Remove-Item -Recurse -Force $GoodAnswerFileDirectory + } + + It "does nothing" { + Remove-UserAccounts -AnswerFilePath $GoodAnswerFilePath + $content = [xml](Get-Content $GoodAnswerFilePath) + $userAccountsBlock = (($content.unattend.settings|where { $_.pass -eq 'oobeSystem' }).component|where { $_.name -eq "Microsoft-Windows-Shell-Setup" }).UserAccounts + $userAccountsBlock | Should Be $Null + } + } + + Context "when provided a valid XML answer file containing an 'oobeSystem' block which contains a 'Microsoft-Windows-Shell-Setup' block which contains a 'UserAccounts' block" { + BeforeEach { + $GoodAnswerFileDirectory = (New-TempDir) + $GoodAnswerFilePath = Join-Path $GoodAnswerFileDirectory "validanswer.xml" + + " + + + + + foo + + + + + " | Out-File $GoodAnswerFilePath + } + + AfterEach { + Remove-Item -Recurse -Force $GoodAnswerFileDirectory + } + + It "Removes the UserAccounts xml block" { + Remove-UserAccounts -AnswerFilePath $GoodAnswerFilePath + $content = [xml](Get-Content $GoodAnswerFilePath) + $userAccountsBlock = (($content.unattend.settings|where { $_.pass -eq 'oobeSystem' }).component|where { $_.name -eq "Microsoft-Windows-Shell-Setup" }).UserAccounts + $userAccountsBlock | Should Be $Null + } + } +} + +Describe "Invoke-Sysprep" { + Context "when not provided an IaaS" { + It "throws" { + { Invoke-Sysprep -OsVersion "windows2012R2" } | Should -Throw "Provide the IaaS this stemcell will be used for" + } + } + + Context "when provided an invalid Iaas" { + It "throws" { + { Invoke-Sysprep -IaaS "OpenShift" -SkipLGPO -OsVersion "windows2012R2" } | Should -Throw "Invalid IaaS 'OpenShift' supported platforms are: AWS, Azure, GCP and Vsphere" + } + } + + Context "handles OS version differences" { + BeforeEach { + Mock Get-ItemProperty { } -ModuleName BOSH.Sysprep + Mock Stop-Computer { } -ModuleName BOSH.Sysprep + Mock Start-Process { } -ModuleName BOSH.Sysprep + Mock Test-Path { $True } -ParameterFilter { $Path -cmatch "C:\\Windows\\LGPO.exe" } -ModuleName BOSH.Sysprep + + Mock Write-Log { } -ModuleName BOSH.Sysprep + + Mock Allow-NTPSync { } -ModuleName BOSH.Sysprep + Mock Enable-LocalSecurityPolicy { } -ModuleName BOSH.Sysprep + Mock Update-AWS2012R2Config { } -ModuleName BOSH.Sysprep + Mock Update-AWS2016Config { } -ModuleName BOSH.Sysprep + Mock Enable-AWS2016Sysprep { } -ModuleName BOSH.Sysprep + + Mock Create-Unattend { } -ModuleName BOSH.Sysprep + Mock Create-Unattend-GCP { } -ModuleName BOSH.Sysprep + + function GCESysprep {} + Mock GCESysprep {} -ModuleName BOSH.Sysprep + + Mock Invoke-Expression { } -ModuleName BOSH.Sysprep + } + + Context "for AWS" { + It "handles Windows 2012R2" { + Mock Get-OSVersion { "windows2012R2" } -ModuleName BOSH.SysPrep + + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Update-AWS2012R2Config -Times 1 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Start-Process -Times 1 -Scope It -ParameterFilter { $FilePath -eq "C:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -and $ArgumentList -eq "-sysprep" } -ModuleName BOSH.Sysprep + + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Update-AWS2016Config -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Enable-AWS2016Sysprep -Times 0 -Scope It -ModuleName BOSH.Sysprep + } + + It "handles Windows 1709" { + Mock Get-OSVersion { "windows2016" } -ModuleName BOSH.Sysprep + + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Update-AWS2016Config -Times 1 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Enable-AWS2016Sysprep -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Update-AWS2012R2Config -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Start-Process -Times 0 -Scope It -ParameterFilter { $FilePath -eq "C:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -and $ArgumentList -eq "-sysprep" } -ModuleName BOSH.Sysprep + } + + It "handles Windows 1803" { + Mock Get-OSVersion { "windows1803" } -ModuleName Bosh.Sysprep + + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Update-AWS2016Config -Times 1 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Enable-AWS2016Sysprep -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Update-AWS2012R2Config -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Start-Process -Times 0 -Scope It -ParameterFilter { $FilePath -eq "C:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -and $ArgumentList -eq "-sysprep" } -ModuleName BOSH.Sysprep + } + + It "handles other OS'" { + Mock Get-OSVersion { Throw "invalid OS detected" } -ModuleName Bosh.Sysprep + + { Invoke-Sysprep -Iaas "aws" } | Should -Throw "invalid OS detected" + + Assert-MockCalled Get-OSVersion -Times 1 -Scope It -ModuleName BOSH.Sysprep + + Assert-MockCalled Update-AWS2016Config -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Enable-AWS2016Sysprep -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Update-AWS2012R2Config -Times 0 -Scope It -ModuleName BOSH.Sysprep + Assert-MockCalled Start-Process -Times 0 -Scope It -ParameterFilter { $FilePath -eq "C:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -and $ArgumentList -eq "-sysprep" } -ModuleName BOSH.Sysprep + } + } + Context "for vSphere" { + It "creates an unattend file" { + Invoke-Sysprep -IaaS vsphere + + Assert-MockCalled Create-Unattend -ModuleName BOSH.Sysprep -ParameterFilter { + $NewPassword -ne $null ` + -and $ProductKey -ne $null ` + -and $Organization -ne $null ` + -and $Owner -ne $null + } + } + + It "calls windows sysprep and shuts down" { + + Invoke-Sysprep -IaaS vsphere + + Assert-MockCalled Invoke-Expression -Scope It -ModuleName BOSH.Sysprep -ParameterFilter { + $Command -like '*sysprep.exe*/shutdown*' } + } + It "calls windows sysprep with unattend file" { + + Invoke-Sysprep -IaaS vsphere + + Assert-MockCalled Invoke-Expression -Scope It -ModuleName BOSH.Sysprep -ParameterFilter { + $Command -like '*sysprep.exe*/unattend*unattend.xml*' } + } + It "calls windows sysprep with correct parameters to generalize the image, and next boot with oobe" { + + Invoke-Sysprep -IaaS vsphere + + Assert-MockCalled Invoke-Expression -Scope It -ModuleName BOSH.Sysprep -ParameterFilter { + $Command -like '*sysprep.exe*/generalize*/oobe*' } + } + } + + Context "for GCP" { + It "creates an unattend file" { + Invoke-Sysprep -IaaS gcp + + Assert-MockCalled Create-Unattend-GCP -ModuleName BOSH.Sysprep + } + } + + Context "for LGPO" { + # We use AWS as the IaaS as it is the only IaaS that is fully mocked right now + # We don't want to trigger Sysprep during our test + It "handles Windows 2012R2" { + Mock Get-OSVersion { "windows2012R2" } -ModuleName Bosh.Sysprep + $ExpectedPath = Join-Path $PSScriptRoot "cis-merge-2012R2" + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Enable-LocalSecurityPolicy -ParameterFilter { $PolicySource -eq $ExpectedPath } -Times 1 -Scope It -ModuleName BOSH.Sysprep + } + + It "handles Windows 1709" { + Mock Get-OSVersion { "windows2016" } -ModuleName Bosh.Sysprep + + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Enable-LocalSecurityPolicy -Times 0 -Scope It -ModuleName BOSH.Sysprep + } + + It "handles Windows 1803" { + Mock Get-OSVersion { "windows1803" } -ModuleName Bosh.Sysprep + $ExpectedPath = Join-Path $PSScriptRoot "cis-merge-1803" + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Enable-LocalSecurityPolicy -ParameterFilter { $PolicySource -eq $ExpectedPath } -Times 1 -Scope It -ModuleName BOSH.Sysprep + } + + It "handles Windows 2019" { + Mock Get-OSVersion { "windows2019" } -ModuleName Bosh.Sysprep + $ExpectedPath = Join-Path $PSScriptRoot "cis-merge-2019" + { Invoke-Sysprep -Iaas "aws" } | Should -Not -Throw + + Assert-MockCalled Enable-LocalSecurityPolicy -ParameterFilter { $PolicySource -eq $ExpectedPath } -Times 1 -Scope It -ModuleName BOSH.Sysprep + } + + It "skips local policy update if -SkipLGPO is set" { + Mock Get-OSVersion { "windows2012R2" } -ModuleName Bosh.Sysprep + + { Invoke-Sysprep -Iaas "aws" -SkipLGPO } | Should -Not -Throw + + Assert-MockCalled Enable-LocalSecurityPolicy -Times 0 -Scope It -ModuleName BOSH.Sysprep + } + + It "handles all other OS'" { + Mock Get-OSVersion { Throw "invalid OS detected" } -ModuleName Bosh.Sysprep + + { Invoke-Sysprep -Iaas "aws" } | Should -Throw "invalid OS detected" + + Assert-MockCalled Enable-LocalSecurityPolicy -Times 0 -Scope It -ModuleName BOSH.Sysprep + } + } + } +} + +Describe "ModifyInfFile" { + BeforeEach { + $InfFileDirectory = (New-TempDir) + $InfFilePath = Join-Path $InfFileDirectory "infFile.inf" + + "something=something`nkey=blah`nx=x" | Out-File $InfFilePath + } + + AfterEach { + Remove-Item -Recurse -Force $InfFileDirectory + } + + It "modifies the inf key" { + ModifyInfFile -InfFilePath $InfFilePath -KeyName 'key' -KeyValue 'value' + + $actual = (Get-Content $InfFilePath) -join "`n" + + $actual | Should Be "something=something`nkey=value`nx=x" + } +} + +Describe "Create-Unattend" { + BeforeEach { + $UnattendDestination = (New-TempDir) + $NewPassword = "NewPassword" + $ProductKey = "ProductKey" + $Organization = "Organization" + $Owner = "Owner" + } + + AfterEach { + Remove-Item -Recurse -Force $UnattendDestination + } + + It "places the generated Unattend file in the specified directory" { + { + Create-Unattend -UnattendDestination $UnattendDestination ` + -NewPassword $NewPassword ` + -ProductKey $ProductKey ` + -Organization $Organization ` + -Owner $Owner + } | Should -Not -Throw + Test-Path (Join-Path $UnattendDestination "unattend.xml") | Should Be $True + } + + It "handles special chars in passwords" { + $NewPassword = " + + + true + + + + + + + Greenwich Standard Time + + + + + + en-us + en-us + en-us + en-us + + + + + true + + Other + + true + 1 + true + true + + + false + Greenwich Standard Time + + + Google Cloud Platform + Google Compute Engine Virtual Machine + https://support.google.com/enterprisehelp/answer/142244?hl=en#cloud + C:\Program Files\Google Compute Engine\sysprep\gcp.bmp + + + + +'@ + + $UnattendPath = "C:\Program Files\Google\Compute Engine\sysprep\unattended.xml" + [xml]$Unattend = (Get-Content -Path $UnattendPath) + + if (-Not ($Unattend.xml.Equals($Expected.xml))) { + Write-Error "The unattend.xml shipped with GCP has changed." + } +} + +function Create-Unattend-GCP() { + Param ( + [string]$UnattendDestination = "C:\Program Files\Google\Compute Engine\sysprep" + ) + $UnattendXML = @' + + + + + + true + + + + + + + UTC + + + + + + en-us + en-us + en-us + en-us + + + + + true + + Other + + true + 3 + true + true + + + false + UTC + + + Google Cloud Platform + Google Compute Engine Virtual Machine + https://support.google.com/enterprisehelp/answer/142244?hl=en#cloud + C:\Program Files\Google Compute Engine\sysprep\gcp.bmp + + + + +'@ + + $UnattendPath = Join-Path $UnattendDestination "unattended.xml" + + Out-File -FilePath $UnattendPath -InputObject $UnattendXML -Encoding utf8 -Force +} + +function Remove-WasPassProcessed { + Param ( + [string]$AnswerFilePath + ) + + If (!$(Test-Path $AnswerFilePath)) { + Throw "Answer file $AnswerFilePath does not exist" + } + + Write-Log "Removing wasPassProcessed" + + $content = [xml](Get-Content $AnswerFilePath) + + foreach ($specializeBlock in $content.unattend.settings) { + $specializeBlock.RemoveAttribute("wasPassProcessed") + } + + $content.Save($AnswerFilePath) +} + +function Remove-UserAccounts { + Param ( + [string]$AnswerFilePath + ) + + If (!$(Test-Path $AnswerFilePath)) { + Throw "Answer file $AnswerFilePath does not exist" + } + + Write-Log "Removing UserAccounts block from Answer File" + + $content = [xml](Get-Content $AnswerFilePath) + $mswShellSetup = (($content.unattend.settings|where {$_.pass -eq 'oobeSystem'}).component|where {$_.name -eq "Microsoft-Windows-Shell-Setup"}) + + if ($mswShellSetup -eq $Null) { + Throw "Could not locate oobeSystem XML block. You may not be running this function on an answer file." + } + + $userAccountsBlock = $mswShellSetup.UserAccounts + + if ($userAccountsBlock.Count -eq 0) { + Return + } + + $mswShellSetup.RemoveChild($userAccountsBlock) + + $content.Save($AnswerFilePath) +} + +function Update-AWS2012R2Config { + $ec2config = [xml] (get-content 'C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml') + + # Enable password generation and retrieval + ($ec2config.ec2configurationsettings.plugins.plugin | where { $_.Name -eq "Ec2SetPassword" }).State = 'Enabled' + + # Disable SetDnsSuffixList setting + $ec2config.ec2configurationsettings.GlobalSettings.SetDnsSuffixList = "false" + + $ec2config.Save("C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml") + + # Enable sysprep + $ec2settings = [xml] (get-content 'C:\Program Files\Amazon\Ec2ConfigService\Settings\BundleConfig.xml') + ($ec2settings.BundleConfig.Property | where { $_.Name -eq "AutoSysprep" }).Value = 'Yes' + + # Don't shutdown when running sysprep, let packer do it + # ($ec2settings.BundleConfig.GeneralSettings.Sysprep | where { $_.AnswerFilePath -eq "sysprep2008.xml" }).Switches = "/oobe /quit /generalize" + + $ec2settings.Save('C:\Program Files\Amazon\Ec2ConfigService\Settings\BundleConfig.xml') +} + +function Update-AWS2016Config +{ + $LaunchConfigJson = 'C:\ProgramData\Amazon\EC2-Windows\Launch\Config\LaunchConfig.json' + $LaunchConfig = Get-Content $LaunchConfigJson -raw | ConvertFrom-Json + $LaunchConfig.addDnsSuffixList = $False + $LaunchConfig.extendBootVolumeSize = $False + $LaunchConfig | ConvertTo-Json | Set-Content $LaunchConfigJson +} + +function Enable-AWS2016Sysprep { + # Enable sysprep + cd 'C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts' + ./InitializeInstance.ps1 -Schedule + ./SysprepInstance.ps1 +} + +<# +.Synopsis + Sysprep Utilities +.Description + This cmdlet runs Sysprep and generalizes a VM so it can be a BOSH stemcell +#> +function Invoke-Sysprep() +{ + Param ( + [string]$IaaS = $( Throw "Provide the IaaS this stemcell will be used for" ), + [string]$NewPassword, + [string]$ProductKey = "", + [string]$Organization = "", + [string]$Owner = "", + [switch]$SkipLGPO, + [switch]$EnableRDP + ) + + Write-Log "Invoking Sysprep for IaaS: ${IaaS}" + + $OsVersion = Get-OSVersion + + # WARN WARN: this should be removed when Microsoft fixes this bug + # See tracker story https://www.pivotaltracker.com/story/show/150238324 + # Skip sysprep if using Windows Server 2016 insider build with UALSVC bug + $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" + If ((Get-ItemProperty -Path $RegPath).CurrentBuildNumber -Eq '16278') + { + Stop-Computer + } + + Allow-NTPSync + + if (-Not $SkipLGPO) + { + if (-Not (Test-Path "C:\Windows\LGPO.exe")) { + Throw "Error: LGPO.exe is expected to be installed to C:\Windows\LGPO.exe" + } + + switch ($OsVersion) + { + "windows2012R2" { + Enable-LocalSecurityPolicy (Join-Path $PSScriptRoot "cis-merge-2012R2") + } + + "windows1803" { + Enable-LocalSecurityPolicy (Join-Path $PSScriptRoot "cis-merge-1803") + } + + "windows2019" { + Enable-LocalSecurityPolicy (Join-Path $PSScriptRoot "cis-merge-2019") + } + } + } + + switch ($IaaS) { + "aws" { + switch ($OsVersion) { + "windows2012R2" { + Update-AWS2012R2Config + Start-Process "C:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -ArgumentList "-sysprep" -Wait + } + {($_ -eq"windows2016") -or ($_ -eq"windows1803") -or ($_ -eq"windows2019")} { + Update-AWS2016Config + Enable-AWS2016Sysprep + } + } + } + "gcp" { + Create-Unattend-GCP + GCESysprep + } + "azure" { + C:\Windows\System32\Sysprep\sysprep.exe /generalize /quiet /oobe /quit + } + "vsphere" { + Create-Unattend -NewPassword $NewPassword -ProductKey $ProductKey ` + -Organization $Organization -Owner $Owner + + Invoke-Expression -Command 'C:/windows/system32/sysprep/sysprep.exe /generalize /oobe /unattend:"C:/Windows/Panther/Unattend/unattend.xml" /quiet /shutdown' + } + Default { Throw "Invalid IaaS '${IaaS}' supported platforms are: AWS, Azure, GCP and Vsphere" } + } +} + +function ModifyInfFile() { + Param( + [string]$InfFilePath = $(Throw "inf file path missing"), + [string]$KeyName = $(Throw "keyname missing"), + [string]$KeyValue = $(Throw "keyvalue missing") + ) + + $Regex = "^$KeyName" + $TempFile = $InfFilePath + ".tmp" + + Get-Content $InfFilePath | ForEach-Object { + $ValueToWrite=$_ + if($_ -match $Regex) { + $ValueToWrite="$KeyName=$KeyValue" + } + $ValueToWrite | Out-File -Append $TempFile + } + + Move-Item -Path $TempFile -Destination $InfFilePath -Force +} + +function Allow-NTPSync() { + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" -Name 'MaxNegPhaseCorrection' -Value 0xFFFFFFFF -Type dword + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" -Name 'MaxPosPhaseCorrection' -Value 0xFFFFFFFF -Type dword +} diff --git a/modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/Audit/audit.csv b/modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/Audit/audit.csv new file mode 100755 index 00000000..98351ee4 --- /dev/null +++ b/modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/Audit/audit.csv @@ -0,0 +1,66 @@ +Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value +,System,IPsec Driver,{0CCE9213-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,System Integrity,{0CCE9212-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Security System Extension,{0CCE9211-69AE-11D9-BED3-505054503030},Success,,1 +,System,Security State Change,{0CCE9210-69AE-11D9-BED3-505054503030},Success,,1 +,System,Other System Events,{0CCE9214-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Group Membership,{0CCE9249-69AE-11D9-BED3-505054503030},Success,,1 +,System,User / Device Claims,{0CCE9247-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Network Policy Server,{0CCE9243-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Other Logon/Logoff Events,{0CCE921C-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Special Logon,{0CCE921B-69AE-11D9-BED3-505054503030},Success,,1 +,System,IPsec Extended Mode,{0CCE921A-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,IPsec Quick Mode,{0CCE9219-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,IPsec Main Mode,{0CCE9218-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Account Lockout,{0CCE9217-69AE-11D9-BED3-505054503030},Failure,,2 +,System,Logoff,{0CCE9216-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Logon,{0CCE9215-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Handle Manipulation,{0CCE9223-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Central Policy Staging,{0CCE9246-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Removable Storage,{0CCE9245-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Detailed File Share,{0CCE9244-69AE-11D9-BED3-505054503030},Failure,,2 +,System,Other Object Access Events,{0CCE9227-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Filtering Platform Connection,{0CCE9226-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Filtering Platform Packet Drop,{0CCE9225-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,File Share,{0CCE9224-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Application Generated,{0CCE9222-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Certification Services,{0CCE9221-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,SAM,{0CCE9220-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Kernel Object,{0CCE921F-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Registry,{0CCE921E-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,File System,{0CCE921D-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Other Privilege Use Events,{0CCE922A-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Non Sensitive Privilege Use,{0CCE9229-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Sensitive Privilege Use,{0CCE9228-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,RPC Events,{0CCE922E-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Token Right Adjusted Events,{0CCE924A-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Process Creation,{0CCE922B-69AE-11D9-BED3-505054503030},Success,,1 +,System,Process Termination,{0CCE922C-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Plug and Play Events,{0CCE9248-69AE-11D9-BED3-505054503030},Success,,1 +,System,DPAPI Activity,{0CCE922D-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Other Policy Change Events,{0CCE9234-69AE-11D9-BED3-505054503030},Failure,,2 +,System,Authentication Policy Change,{0CCE9230-69AE-11D9-BED3-505054503030},Success,,1 +,System,Audit Policy Change,{0CCE922F-69AE-11D9-BED3-505054503030},Success,,1 +,System,Filtering Platform Policy Change,{0CCE9233-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Authorization Policy Change,{0CCE9231-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,MPSSVC Rule-Level Policy Change,{0CCE9232-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Other Account Management Events,{0CCE923A-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Application Group Management,{0CCE9239-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Distribution Group Management,{0CCE9238-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Security Group Management,{0CCE9237-69AE-11D9-BED3-505054503030},Success,,1 +,System,Computer Account Management,{0CCE9236-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,User Account Management,{0CCE9235-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Directory Service Replication,{0CCE923D-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Directory Service Access,{0CCE923B-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Detailed Directory Service Replication,{0CCE923E-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Directory Service Changes,{0CCE923C-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Other Account Logon Events,{0CCE9241-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Kerberos Service Ticket Operations,{0CCE9240-69AE-11D9-BED3-505054503030},No Auditing,,0 +,System,Credential Validation,{0CCE923F-69AE-11D9-BED3-505054503030},Success and Failure,,3 +,System,Kerberos Authentication Service,{0CCE9242-69AE-11D9-BED3-505054503030},No Auditing,,0 +,,Option:CrashOnAuditFail,,Disabled,,0 +,,Option:FullPrivilegeAuditing,,Disabled,,0 +,,Option:AuditBaseObjects,,Disabled,,0 +,,Option:AuditBaseDirectories,,Disabled,,0 +,,FileGlobalSacl,,,, +,,RegistryGlobalSacl,,,, diff --git a/modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf b/modules/BOSH.Sysprep/cis-merge-2019/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf new file mode 100755 index 0000000000000000000000000000000000000000..5aa28126f7ec5cc2b07f7e26e2457ef40110fa21 GIT binary patch literal 228 zcmZ9FI|{-;6h%)hxQ7tjfMPpEuuv;S(I!}g7-R|)$oL^HUcK|2%3=oY@4U~uLOKXeOKCOlQUr<=30Njl8tn3jGgj_-TE;cQ}e&}_dDQJWk5pVA_D;LSjpEvxJ)o_~uXKMCrIMA@7 zVol4Ik&eidm*+~7HL^hO{E@a4yfq?&G;uPce6$a?rFqX=Mw`j6`I^b;;jc?}8b!8) TtjkT$QD@pT8H>Mr?)$1brk-p7myH0GBe4L<#)m0 z9w<&YY@RZFatb2u+|&p3ycwAJP&4j~lvsE`iB%P1%|f5z7+$n=s?Mqn9+i-qM$<`K fM!uR2QgplC?)bl)lE_cYm4`Bv%5snPxy{!K&0#Z} literal 0 HcmV?d00001 diff --git a/modules/BOSH.Sysprep/print_conflicted_from_audit_csv.rb b/modules/BOSH.Sysprep/print_conflicted_from_audit_csv.rb new file mode 100755 index 00000000..b5dfd482 --- /dev/null +++ b/modules/BOSH.Sysprep/print_conflicted_from_audit_csv.rb @@ -0,0 +1,77 @@ +#!/usr/bin/env ruby + +class AuditFile + attr_accessor :header + attr_accessor :contents + + def initialize(header, contents) + @header = header + @contents = contents + end + + def self.from_string(s) + lines = s.split "\r\n" + header = lines[0] + contents = lines[1..-1] + + AuditFile.new(header, contents) + end + + def print() + ([header] + contents).join("\r\n") + end + + def uniq() + uniqed = contents.uniq do |x| + values = x.split(',') + keys = values[0..5].join('').downcase + setting_value = values[6] + keys+setting_value + end + + AuditFile.new(header, uniqed) + end + + def size() + contents.size + end +end + +def merge(a, b) + AuditFile.new(a.header, a.contents + b.contents) +end + +a_file = File.read 'audit-cis.csv' +b_file = File.read 'audit-ms.csv' + +a_audit = AuditFile.from_string(a_file) +b_audit = AuditFile.from_string(b_file) + +puts "a has #{a_audit.size} lines" +puts "b has #{b_audit.size} lines" + +merged = merge(a_audit, b_audit) +puts "after merge: #{merged.size} lines" + +uniqued = merged.uniq +puts "after uniq: #{uniqued.size} lines" + +grouped = uniqued.contents.group_by do |line| + fields = line.split ',' + policy_target = fields[1].downcase + subcategory = fields[2].downcase + policy_target + ',' + subcategory +end + +dups = grouped.keys.select {|k| grouped[k].size > 1} + +if(dups.nil?) + puts "found 0 conflicts" +else + puts "found #{dups.size} conflicts" + dups.each {|x| puts x} +end + +merged_file = "audit-merged.csv" +File.write merged_file, uniqued.print +puts "wrote merged stuff to #{merged_file}" diff --git a/modules/BOSH.Sysprep/print_conflicted_keys_from_inf_txt.rb b/modules/BOSH.Sysprep/print_conflicted_keys_from_inf_txt.rb new file mode 100755 index 00000000..54715ba6 --- /dev/null +++ b/modules/BOSH.Sysprep/print_conflicted_keys_from_inf_txt.rb @@ -0,0 +1,108 @@ +#!/usr/bin/env ruby + +class Infs + def self.from_string(s) + Inf.from_string_array(s.split(/(?=\[.+\])/)) + end +end + +class Inf + attr_accessor :name + attr_accessor :contents + + def initialize(name, contents) + @name = name + @contents = contents + end + + def self.from_string_array(sections) + sections.map do |x| + contents = x.split("\r\n") + section_name = contents.first + section_contents = contents[1..-1] + + Inf.new(section_name, section_contents) + end + end + + def uniq() + trim_contents = contents.map { |x| x.strip } + Inf.new(name, trim_contents.uniq {|text| text.split('=')[0].downcase + text.split('=')[1..-1].join('')}) + end + + def sort() + Inf.new(name, contents.sort) + end + + def size() + @contents.size + end + + def summary() + "section: #{@name} size: #{size}" + end +end + +def print_section_summary(sections) + sections.each do |x| + puts x.summary + end +end + +def convert_to_utf8(name) + `iconv -f UTF-16LE -t UTF-8 #{name} > /tmp/registry-tmp` + File.read '/tmp/registry-tmp', encoding: 'bom|utf-8' +end + +def merge(a, b) + a_no_comment = a.split("\r\n").reject {|x| x.strip.gsub(/^;.*/,'').empty?}.join("\r\n").strip + b_no_comment = b.split("\r\n").reject {|x| x.strip.gsub(/^;.*/,'').empty?}.join("\r\n").strip + + a_normalize_equals = a_no_comment.gsub(' = ', '=').gsub(" =\r\n", "=\r\n") + b_normalize_equals = b_no_comment.gsub(' = ', '=').gsub(" =\r\n", "=\r\n") + + a_infs = Infs.from_string(a_normalize_equals) + b_infs = Infs.from_string(b_normalize_equals) + + (a_infs + b_infs).group_by {|x| x.name}.map {|section_name,infs| Inf.new(section_name, infs.map{|x| x.contents}.flatten)} +end + +a_contents = convert_to_utf8('GptTmpl-ms-baseline.inf') +b_contents = convert_to_utf8('GptTmpl-cis-baseline.inf') + +puts "merged" +merged = merge(a_contents, b_contents) + +print_section_summary(merge(a_contents, b_contents)) + +puts "removing dups" + +unique = merged.map do |section| + section.uniq +end + +sorted = unique.map do |section| + section.sort +end + +print_section_summary(sorted) + +puts "listing conflicts" +sorted.each do |section| + elements = section.contents + title = section.name + + grouped = elements.group_by {|x| x.split("=")[0]} + dups = grouped.keys.select {|k| grouped[k].size > 1} + + if(dups.nil?) + puts "found 0 conflicts in section #{title}" + else + puts "found #{dups.size} conflicts in section #{title}" + dups.each {|x| puts x} + end +end + +new_file = 'GptTmpl-merged.inf' +File.write new_file, sorted.map{|section| section.name + "\n" + section.contents.join("\n")}.join("\n") +puts "merged files with uniques removed outputted to #{new_file}" diff --git a/modules/BOSH.Sysprep/print_conflicted_registry_keys_from_pol_txt.rb b/modules/BOSH.Sysprep/print_conflicted_registry_keys_from_pol_txt.rb new file mode 100755 index 00000000..810bd379 --- /dev/null +++ b/modules/BOSH.Sysprep/print_conflicted_registry_keys_from_pol_txt.rb @@ -0,0 +1,42 @@ +#!/usr/bin/env ruby + +def convert_to_utf8(name) + `iconv -f UTF-16LE -t UTF-8 #{name} > /tmp/registry-tmp` + File.read '/tmp/registry-tmp', encoding: 'bom|utf-8' +end + +def merge(a, b) + a_no_comment = a.split("\r\n").reject {|x| x[0] == ';' }.join("\r\n").strip + b_no_comment = b.split("\r\n").reject {|x| x[0] == ';' }.join("\r\n").strip + a_no_comment.strip + "\r\n\r\n" + b_no_comment.strip +end + +registry_a_name = 'registry-cis-machine.txt' +registry_b_name = 'registry-ms-baseline-machine.txt' + +registry_a_contents = convert_to_utf8(registry_a_name) +registry_b_contents = convert_to_utf8(registry_b_name) + +before_uniq = merge(registry_a_contents, registry_b_contents).split("\r\n\r\n").sort + +puts "before uniq: #{before_uniq.size}" +after_uniq = before_uniq.uniq do |entry| + lines = entry.split "\r\n" + reg_key = (lines[1] + lines[2]).downcase + lines[0] + reg_key + lines[3] +end +puts "after uniq: #{after_uniq.size}" + +library = after_uniq.group_by {|x| x.split("\r\n")[1..2].join('\\')} +dups = library.keys.select {|k| library[k].size > 1} + +if(dups.nil?) + puts "found 0 conflicts" +else + puts "found #{dups.size} conflicts" + dups.each {|x| puts x} +end + +merged_file = "registry-merged.txt" +File.write merged_file, after_uniq.join("\r\n\r\n") +puts "wrote merged registry files with duplicates removed to #{merged_file}" diff --git a/modules/BOSH.Utils/AbsolutePathChroot.psd1 b/modules/BOSH.Utils/AbsolutePathChroot.psd1 new file mode 100644 index 00000000..2d3f1840 --- /dev/null +++ b/modules/BOSH.Utils/AbsolutePathChroot.psd1 @@ -0,0 +1,14 @@ +@{ +RootModule = 'AbsolutePathChroot' +ModuleVersion = '0.1' +Author = 'BOSH' +Copyright = '(c) 2019 BOSH' +Description = 'Common Utils on a BOSH deployed vm' +PowerShellVersion = '4.0' +FunctionsToExport = @( + 'AbsolutePathChroot-New-Item' +) +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +} diff --git a/modules/BOSH.Utils/AbsolutePathChroot.psm1 b/modules/BOSH.Utils/AbsolutePathChroot.psm1 new file mode 100644 index 00000000..0a91e10f --- /dev/null +++ b/modules/BOSH.Utils/AbsolutePathChroot.psm1 @@ -0,0 +1,18 @@ +<# +.Synopsis + AbsolutePathChroot +.Description + This cmdlet is for use in tests to mock filesystem operations, so absolute paths become relative +#> + +function AbsolutePathChroot-New-Item { + param( + $Path + ) + $optionalDriveLetterRegex = "(.:)?(.+)" + $pathWithoutDriveName = $Path -replace $optionalDriveLetterRegex, '$2' + + $safePath=".\" + + New-Item -Path "$safePath$pathWithoutDriveName" @Args +} diff --git a/modules/BOSH.Utils/BOSH.Utils.Tests.ps1 b/modules/BOSH.Utils/BOSH.Utils.Tests.ps1 new file mode 100644 index 00000000..0c7e8cdf --- /dev/null +++ b/modules/BOSH.Utils/BOSH.Utils.Tests.ps1 @@ -0,0 +1,527 @@ +#We remove multiple module that import BOSH.Utils +Remove-Module -Name BOSH.WinRM -ErrorAction Ignore +Remove-Module -Name BOSH.CFCell -ErrorAction Ignore +Remove-Module -Name BOSH.AutoLogon -ErrorAction Ignore + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ./BOSH.Utils.psm1 +Remove-Module -Name AbsolutePathChroot -ErrorAction Ignore +Import-Module ./AbsolutePathChroot.psm1 + + +#As of now, this function only supports DWords and Strings. +function Restore-RegistryState { + param( + [bool]$KeyExists, + [String]$KeyPath, + [String]$ValueName, + [PSObject]$ValueData + ) + if ($KeyExists) { + if ($ValueData -eq $null) { + Remove-ItemProperty -path $KeyPath -Name $ValueName + } else { + Set-ItemProperty -path $KeyPath -Name $ValueName -Value $ValueData + } + } else { + Remove-Item -Path $KeyPath -ErrorAction SilentlyContinue + } +} + +Describe "Restore-RegistryState" { + BeforeEach { + Mock Remove-ItemProperty {} + Mock Set-ItemProperty {} + Mock Remove-Item {} + } + It "restores the registry by deleting a registry key created by the test" { + Restore-RegistryState -KeyExists $false -KeyPath "HKLM:\Some registry key" + + Assert-MockCalled Remove-Item -Times 1 -Scope It -ParameterFilter { $Path -eq "HKLM:\Some registry key" } + Assert-MockCalled Remove-ItemProperty -Times 0 -Scope It + Assert-MockCalled Set-ItemProperty -Times 0 -Scope It + } + + It "restores the registry by deleting a registry value created by the test" { + Restore-RegistryState -KeyExist $true -KeyPath "HKLM:\Some registry key" -ValueName "SomeValue" + + Assert-MockCalled Remove-Item -Times 0 -Scope It + Assert-MockCalled Remove-ItemProperty -Times 1 -Scope It -ParameterFilter { $Path -eq "HKLM:\Some registry key" -and $Name -eq "SomeValue"} + Assert-MockCalled Set-ItemProperty -Times 0 -Scope It + } + + It "restores the registry by restoring a registry data modified by the test" { + Restore-RegistryState -KeyExist $true -KeyPath "HKLM:\Some registry key" -ValueName "SomeValue" -ValueData "Some Data" + Restore-RegistryState -KeyExist $true -KeyPath "HKLM:\Some dword reg key" -ValueName "SomeDwordValye" -ValueData 85432 + + Assert-MockCalled Remove-Item -Times 0 -Scope It + Assert-MockCalled Remove-ItemProperty -Times 0 -Scope It + Assert-MockCalled Set-ItemProperty -Times 1 -Scope It -ParameterFilter { $Path -eq "HKLM:\Some registry key" -and $Name -eq "SomeValue" -and $Value -eq "Some Data" } + Assert-MockCalled Set-ItemProperty -Times 1 -Scope It -ParameterFilter { $Path -eq "HKLM:\Some dword reg key" -and $Name -eq "SomeDwordValye" -and $Value -eq 85432 } + } +} + +function New-TempDir { + $parent = [System.IO.Path]::GetTempPath() + [string] $name = [System.Guid]::NewGuid() + (New-Item -ItemType Directory -Path (Join-Path $parent $name)).FullName +} + +Describe "Open-Zip" { + BeforeEach { + $outPath=(New-TempDir) + } + + AfterEach { + Remove-Item -Recurse -Force $outPath + } + + Context "when zipFile is not provided" { + It "throws" { + { Open-Zip } | Should Throw "Provide a ZipFile to extract" + } + } + Context "when output file already exists" { + It "does not throw" { + New-Item -Path $outPath -Name "file.txt" -ItemType "file" -Value "Hello" + { Open-Zip -ZipFile "./example.zip" -OutPath $outPath } | Should Not Throw + Get-Content (Join-Path $outPath "file.txt") | Should Be "file" + } + } + Context "when OutPath is not provided" { + It "throws" { + { Open-Zip -ZipFile "./example.zip" } | Should Throw "Provide an OutPath for extract" + } + } + It "extracts Zip file" { + Open-Zip -ZipFile "./example.zip" -OutPath $outPath + $file = (Join-Path $outPath "file.txt") + Test-Path $file | Should be $True + } +} + +Describe "Get-Log" { + Context "when missing log file" { + It "throws" { + $dir = (New-TempDir) + $logFile = (Join-Path $dir "log.log") + { Get-Log -LogFile $logFile } | Should Throw "Missing log file: $logFile" + } + } +} + +Describe "Protect-Dir" { + BeforeEach { + $aclDir=(New-TempDir) + New-Item -Path $aclDir -ItemType Directory -Force + + cacls.exe $aclDir /T /E /P "BUILTIN\Users:F" + $LASTEXITCODE | Should Be 0 + cacls.exe $aclDir /T /E /P "BUILTIN\IIS_IUSRS:F" + $LASTEXITCODE | Should Be 0 + } + + AfterEach { + Remove-Item -Recurse -Force $aclDir + } + + Context "when not provided a directory" { + It "throws" { + { Protect-Dir } | Should Throw "Provide a directory to set ACL on" + } + } + + It "sets the correct ACLs on the provided directory" { + { Protect-Dir -path $aclDir } | Should Not Throw + + $acl = (Get-Acl $aclDir) + $acl.Owner | Should Be "BUILTIN\Administrators" + $acl.Access | where { $_.IdentityReference -eq "BUILTIN\Users" } | Should BeNullOrEmpty + $acl.Access | where { $_.IdentityReference -eq "BUILTIN\IIS_IUSRS" } | Should BeNullOrEmpty + $adminAccess = ($acl.Access | where { $_.IdentityReference -eq "BUILTIN\Administrators" }) + $adminAccess | Should Not BeNullOrEmpty + $adminAccess.FileSystemRights | Should Be "FullControl" + } + + Context "when inheritance is disabled" { + It "disables ACL inheritance on the provided directory " { + { Protect-Dir -path $aclDir -disableInheritance $True } | Should Not Throw + + (Get-Acl $aclDir).AreAccessRulesProtected | Should Be $True + } + } +} + +Describe "Disable-RC4" { + It "Disables the use of RC4 Cipher" { + $rc4_128Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 128/128" + $rc4_128PathExists = Test-Path -Path $rc4_128Path + $oldRC4_128Value = (Get-ItemProperty -path $rc4_128Path -ErrorAction SilentlyContinue).'Enabled' + + $rc4_40Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 40/128" + $rc4_40PathExists = Test-Path -Path $rc4_40Path + $oldRC4_40Value = (Get-ItemProperty -path $rc4_40Path -ErrorAction SilentlyContinue).'Enabled' + + $rc4_56Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 56/128" + $rc4_56PathExists = Test-Path -Path $rc4_56Path + $oldRC4_56Value = (Get-ItemProperty -path $rc4_56Path -ErrorAction SilentlyContinue).'Enabled' + + { Disable-RC4 } | Should Not Throw + + (Get-ItemProperty -Path $rc4_128Path).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $rc4_40Path).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $rc4_56Path).'Enabled' | Should Be "0" + + Restore-RegistryState -KeyExists $rc4_128PathExists -KeyPath $rc4_128Path -ValueName 'Enabled' -ValueData $oldRC4_128Value + Restore-RegistryState -KeyExists $rc4_40PathExists -KeyPath $rc4_40Path -ValueName 'Enabled' -ValueData $oldRC4_40Value + Restore-RegistryState -KeyExists $rc4_56PathExists -KeyPath $rc4_56Path -ValueName 'Enabled' -ValueData $oldRC4_56Value + } +} + +Describe "Disable-TLS1" { + It "Disables the use of TLS 1.0" { + $serverPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' + $serverPathExists = Test-Path -Path $serverPath + + $oldServerDisabledValue = (Get-ItemProperty -path $serverPath -ErrorAction SilentlyContinue).'DisabledByDefault' + $oldServerValue = (Get-ItemProperty -path $serverPath -ErrorAction SilentlyContinue).'Enabled' + + $clientPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' + $clientPathExists = Test-Path -Path $clientPath + + $oldClientEnabledValue = (Get-ItemProperty -path $clientPath -ErrorAction SilentlyContinue).'Enabled' + $oldClientDisabledValue = (Get-ItemProperty -path $clientPath -ErrorAction SilentlyContinue).'DisabledByDefault' + + { Disable-TLS1 } | Should Not Throw + + (Get-ItemProperty -Path $serverPath).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $serverPath).'DisabledByDefault' | Should Be "1" + + (Get-ItemProperty -Path $clientPath).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $clientPath).'DisabledByDefault' | Should Be "1" + + Restore-RegistryState -KeyExists $serverPathExists -KeyPath $serverPath -ValueName 'Enabled' -ValueData $oldServerValue + Restore-RegistryState -KeyExists $serverPathExists -KeyPath $serverPath -ValueName 'DisabledByDefault' -ValueData $oldServerDisabledValue + + Restore-RegistryState -KeyExists $clientPathExists -KeyPath $clientPath -ValueName 'Enabled' -ValueData $oldClientValue + Restore-RegistryState -KeyExists $clientPathExists -KeyPath $clientPath -ValueName 'DisabledByDefault' -ValueData $oldClientDisabledValue + } +} + +Describe "Disable-TLS11" { + It "Disables the use of TLS 1.0" { + $serverPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' + $serverPathExists = Test-Path -Path $serverPath + + $oldServerDisabledValue = (Get-ItemProperty -path $serverPath -ErrorAction SilentlyContinue).'DisabledByDefault' + $oldServerValue = (Get-ItemProperty -path $serverPath -ErrorAction SilentlyContinue).'Enabled' + + $clientPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' + $clientPathExists = Test-Path -Path $clientPath + + $oldClientEnabledValue = (Get-ItemProperty -path $clientPath -ErrorAction SilentlyContinue).'Enabled' + $oldClientDisabledValue = (Get-ItemProperty -path $clientPath -ErrorAction SilentlyContinue).'DisabledByDefault' + + { Disable-TLS11 } | Should Not Throw + + (Get-ItemProperty -Path $serverPath).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $serverPath).'DisabledByDefault' | Should Be "1" + + (Get-ItemProperty -Path $clientPath).'Enabled' | Should Be "0" + (Get-ItemProperty -Path $clientPath).'DisabledByDefault' | Should Be "1" + + Restore-RegistryState -KeyExists $serverPathExists -KeyPath $serverPath -ValueName 'Enabled' -ValueData $oldServerValue + Restore-RegistryState -KeyExists $serverPathExists -KeyPath $serverPath -ValueName 'DisabledByDefault' -ValueData $oldServerDisabledValue + + Restore-RegistryState -KeyExists $clientPathExists -KeyPath $clientPath -ValueName 'Enabled' -ValueData $oldClientValue + Restore-RegistryState -KeyExists $clientPathExists -KeyPath $clientPath -ValueName 'DisabledByDefault' -ValueData $oldClientDisabledValue + } +} + +Describe "Enable-TLS12" { + It "Disables the use of TLS 1.0" { + $serverPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' + $serverPathExists = Test-Path -Path $serverPath + + $oldServerValue = (Get-ItemProperty -path $serverPath -ErrorAction SilentlyContinue).'Enabled' + + $clientPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' + $clientPathExists = Test-Path -Path $clientPath + + $oldClientEnabledValue = (Get-ItemProperty -path $clientPath -ErrorAction SilentlyContinue).'Enabled' + + { Enable-TLS12 } | Should Not Throw + + (Get-ItemProperty -Path $serverPath).'Enabled' | Should Be "1" + + (Get-ItemProperty -Path $clientPath).'Enabled' | Should Be "1" + + Restore-RegistryState -KeyExists $serverPathExists -KeyPath $serverPath -ValueName 'Enabled' -ValueData $oldServerValue + + Restore-RegistryState -KeyExists $clientPathExists -KeyPath $clientPath -ValueName 'Enabled' -ValueData $oldClientValue + } +} + +Describe "Disable-3DES" { + It "Disables birthday attacks against 64 bit block TLS ciphers" { + $registryPath = 'hklm:\system\currentcontrolset\control\securityproviders\schannel\ciphers\triple des 168' + $tripleDESPathExists = Test-Path $registryPath + $oldDESValue = (Get-ItemProperty -path $registryPath -ErrorAction SilentlyContinue).'Enabled' + + { Disable-3DES } | Should Not Throw + + (Get-ItemProperty -path $registryPath).'Enabled' | Should Be "0" + + Restore-RegistryState -KeyExists $tripleDESPathExists -KeyPath $registryPath -ValueName 'Enabled' -ValueData $oldDESValue + } +} + +Describe "Disable-DCOM" -Tag 'Focused' { + It "Disables the use of DCOM" { + $DCOMPath = 'HKLM:\Software\Microsoft\OLE' + $oldDCOMValue = (Get-ItemProperty -Path $DCOMPath).'EnableDCOM' + + { Disable-DCOM } | Should Not Throw + + (Get-ItemProperty -Path $DCOMPath).'EnableDCOM' | Should Be "N" + Set-ItemProperty -Path $DCOMPath -Name 'EnableDCOM' -Value $oldDCOMValue + + Restore-RegistryState -KeyExists $true -KeyPath $DCOMPath -ValueName 'EnableDCOM' -ValueData $oldDCOMValue + } +} + +Describe "Get-OSVersion" { + BeforeEach { + Mock Write-Log { } -ModuleName BOSH.Utils + } + + It "Correctly detects Windows 2012R2" { + Mock Get-OSVersionString { "6.3.9600.68" } -ModuleName BOSH.Utils + $actualOSVersion = $null + + { Get-OSVersion | Set-Variable -Name "actualOSVersion" -Scope 1 } | Should -Not -Throw + $actualOsVersion | Should -eq "windows2012R2" + + Assert-MockCalled Write-Log -Times 1 -Scope It -ParameterFilter { $Message -eq "Found OS version: Windows 2012R2" } -ModuleName BOSH.Utils + Assert-MockCalled Get-OSVersionString -Times 1 -Scope It -ModuleName BOSH.Utils + } + + It "Correctly detects Windows 1709" { + Mock Get-OSVersionString { "10.0.16299.233" } -ModuleName BOSH.Utils + $actualOSVersion = $null + + { Get-OSVersion | Set-Variable -Name "actualOSVersion" -Scope 1 } | Should -Not -Throw + $actualOsVersion | Should -eq "windows2016" + + Assert-MockCalled Write-Log -Times 1 -Scope It -ParameterFilter { $Message -eq "Found OS version: Windows 1709" } -ModuleName BOSH.Utils + Assert-MockCalled Get-OSVersionString -Times 1 -Scope It -ModuleName BOSH.Utils + } + + It "Correctly detects Windows 1803" { + Mock Get-OSVersionString { "10.0.17134.420" } -ModuleName BOSH.Utils + $actualOSVersion = $null + + { Get-OSVersion | Set-Variable -Name "actualOSVersion" -Scope 1 } | Should -Not -Throw + $actualOsVersion | Should -eq "windows1803" + + Assert-MockCalled Write-Log -Times 1 -Scope It -ParameterFilter { $Message -eq "Found OS version: Windows 1803" } -ModuleName BOSH.Utils + Assert-MockCalled Get-OSVersionString -Times 1 -Scope It -ModuleName BOSH.Utils + } + + It "Correctly detects Windows 2019" { + Mock Get-OSVersionString { "10.0.17763.410" } -ModuleName BOSH.Utils + $actualOSVersion = $null + + { Get-OSVersion | Set-Variable -Name "actualOSVersion" -Scope 1 } | Should -Not -Throw + $actualOsVersion | Should -eq "windows2019" + + Assert-MockCalled Write-Log -Times 1 -Scope It -ParameterFilter { $Message -eq "Found OS version: Windows 2019" } -ModuleName BOSH.Utils + Assert-MockCalled Get-OSVersionString -Times 1 -Scope It -ModuleName BOSH.Utils + } + + It "Throws an exception if a valid OS is not detected" { + Mock Get-OSVersionString { "01.23.456.789" } -ModuleName BOSH.Utils + + { Get-OSVersion } | Should -Throw "invalid OS detected" + + Assert-MockCalled Write-Log -Times 1 -Scope It -ParameterFilter { $Message -eq "invalid OS detected" } -ModuleName BOSH.Utils + Assert-MockCalled Get-OSVersionString -Times 1 -Scope It -ModuleName BOSH.Utils + } +} + +Describe "Get-WinRMConfig" { + It "makes a request for winrm config, returns stdout" { + Mock Invoke-Expression { + "Lots of winrm config" + } -ModuleName BOSH.Utils + + $output = "" + { Get-WinRMConfig | Set-Variable -Name "output" -Scope 1 } | Should -Not -Throw + + $output | Should -eq "Lots of winrm config" + + Assert-MockCalled Invoke-Expression -Times 1 -Scope It ` + -ParameterFilter { $Command -and $Command -eq "winrm get winrm/config" } -ModuleName BOSH.Utils + } + + It "throws a descriptive failure when winrm config is unavailable" { + Mock Invoke-Expression { + Write-Error "Some error output" + } -ModuleName BOSH.Utils + + $output = "" + { Get-WinRMConfig | Set-Variable -Name "output" -Scope 1 } | ` + Should -Throw "Failed to get WinRM config: Some error output" + + $output | Should -eq "" + } +} + +Describe "Set-ProxySettings" { + It "sets the Internet Explorer proxy settings" { + function Compare-Array { + $($args[0] -join ",") -eq $($args[1] -join ",") + } + + Mock Set-ItemProperty { + "Property set" + } -ModuleName BOSH.Utils + + { Set-ProxySettings "http-proxy" "https-proxy" "bypass-list" } | Should Not Throw + + [string] $start = [System.Text.Encoding]::ASCII.GetString([byte[]](70, 0, 0, 0, 25, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0 ), 0, 16); + [string] $endproxy = [System.Text.Encoding]::ASCII.GetString([byte[]]( 233, 0, 0, 0 ), 0, 4); + [string] $end = [System.Text.Encoding]::ASCII.GetString([byte[]]( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 0, 36); + + [string] $text = "$($start)http=http-proxy;https=https-proxy$($endproxy)bypass-list$($end)"; + [byte[]] $data = [System.Text.Encoding]::ASCII.GetBytes($text); + + Assert-MockCalled Set-ItemProperty -Times 1 -ModuleName BOSH.Utils -Scope It ` + -ParameterFilter { + $Path -eq "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -and + $Name -eq "DefaultConnectionSettings" -and + (Compare-Array $Value $data) + } + } + + It "exits when the registry can't be set or there's an error because the arguments are wrong" { + Mock Set-ItemProperty { + Write-Error "Property not set" + } -ModuleName BOSH.Utils + + + { Set-ProxySettings "http-proxy" "https-proxy" "bypass-list" } | Should -Throw "Failed to set proxy settings: Property not set" + } +} + +Describe "Clear-ProxySettings" { + BeforeEach { + Mock Write-Log { } -ModuleName BOSH.Utils + } + + It "Should remove proxy settings if they were set" { + $regKeyConnections = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" + Set-ItemProperty -Path $regKeyConnections -Name "DefaultConnectionSettings" -Value "test-value" -ErrorVariable err 2>&1 | Out-Null + + $set_proxy = & cmd.exe /c "netsh winhttp set proxy proxy-server=`"127.0.0.1`"" + + Clear-ProxySettings + + $item=Get-Item $regKeyConnections + + #DefaultConnectionSettings is actually added to a "Property" object + $item.Property | Should Be $null + + #We need to pipe the netsh command through Out-String in order to convert its output into a proper string + $output= (netsh winhttp show proxy) | Out-String + $output | Should -BeLike "*Direct access (no proxy server)*" + Assert-MockCalled Write-Log -Times 1 -ModuleName BOSH.Utils -Scope It -ParameterFilter { $Message -eq "Cleared proxy settings: $output" } + } + + It "Should not error if no proxy settings are found" { + Clear-ProxySettings + + Assert-MockCalled Write-Log -Times 1 -ModuleName BOSH.Utils -Scope It -ParameterFilter { $Message -eq "No proxy settings set. There is nothing to clear." } + } +} + +Describe "Get-WUCerts" { + BeforeEach { + Mock SST-Path { "certfile" } -ModuleName BOSH.Utils + Mock Invoke-Import-Certificate { } -ModuleName BOSH.Utils + Mock Invoke-Certutil { } -ModuleName BOSH.Utils + Mock Invoke-Remove-Item { } -ModuleName BOSH.Utils + } + It "calls certutil and imports the certificates" { + + Get-WUCerts + + Assert-MockCalled SST-Path -Times 1 -Scope It -ModuleName BOSH.Utils + Assert-MockCalled Invoke-Certutil -Times 1 -Scope It -ModuleName BOSH.Utils -ParameterFilter { $generateSSTFromWU -eq "certfile" } + Assert-MockCalled Invoke-Import-Certificate -Times 1 -Scope It -ModuleName BOSH.Utils -ParameterFilter { $CertStoreLocation -eq "Cert:\LocalMachine\Root" -and $FilePath -eq "certfile" } + Assert-MockCalled Invoke-Remove-Item -Times 1 -Scope It -ModuleName BOSH.Utils -ParameterFilter { $path -eq "certfile" } + } + + It "throws if the certfile cannot be generated" { + Mock Invoke-Certutil { throw 'some error' } -ModuleName BOSH.Utils + { Get-WUCerts } | Should Throw "some error" + } + + It "throws if the certfile cannot be imported" { + Mock Invoke-Import-Certificate { throw 'some error' } -ModuleName BOSH.Utils + { Get-WUCerts } | Should Throw "some error" + } +} + +Describe "New-VersionFile" { + BeforeEach { + $versionFileDestination = "./var/vcap/bosh/etc/stemcell_version" + Mock New-Item -Module BOSH.Utils { + AbsolutePathChroot-New-Item @Args + } + } + + AfterEach { + Remove-Item -ErrorAction Ignore -Recurse "./var" + } + + It "creates a version file with OS major.minor -Version parameter value as content" { + $version = '1803.456.17-build.2' + $versionExpectation = '^1803.456$' + + New-VersionFile -Version $version + + $versionFileDestination | Should -Exist + $versionFileDestination | Should -FileContentMatchExactly $versionExpectation + } + + It "throws if the version parameter is not specified" { + { New-VersionFile } | Should Throw "-Version parameter must be specified as major.minor[.whatever]" + } +} + +function getWindowsOptionalFeatureState { + param([string] $featureName) + sleep -Milliseconds 500 + $obj = Get-WindowsOptionalFeature -Online -FeatureName $featureName + return $obj.State +} + +Describe "Enable-Hyper-V" { + + AfterEach { + Disable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -norestart + } + + It "Enables Hyper V" { + getWindowsOptionalFeatureState("Microsoft-Hyper-V") | Should -MatchExactly "Disabled" + + Enable-Hyper-V + + getWindowsOptionalFeatureState("Microsoft-Hyper-V") | Should -MatchExactly "Enabled" + } + +} + + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore diff --git a/modules/BOSH.Utils/BOSH.Utils.psd1 b/modules/BOSH.Utils/BOSH.Utils.psd1 new file mode 100644 index 00000000..a979ac8c --- /dev/null +++ b/modules/BOSH.Utils/BOSH.Utils.psd1 @@ -0,0 +1,41 @@ +@{ +RootModule = 'BOSH.Utils' +ModuleVersion = '0.1' +GUID = '1113e65d-b18e-4277-abc8-12c60a8f1f52' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Common Utils on a BOSH deployed vm' +PowerShellVersion = '4.0' +FunctionsToExport = @( + 'Write-Log', + 'Get-Log', + 'Open-Zip', + 'New-Provisioner', + 'Clear-Provisioner', + 'Protect-Dir', + 'Protect-Path', + 'Set-ProxySettings', + 'Clear-ProxySettings', + 'Disable-RC4', + 'Disable-TLS1', + 'Disable-TLS11', + 'Enable-TLS12', + 'Disable-3DES', + 'Disable-DCOM', + 'Get-OSVersion', + 'Get-WinRMConfig', + 'Get-WUCerts', + 'New-VersionFile', + 'Enable-Hyper-V' +) +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('Utils') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.Utils/BOSH.Utils.psm1 b/modules/BOSH.Utils/BOSH.Utils.psm1 new file mode 100644 index 00000000..8d3c38f6 --- /dev/null +++ b/modules/BOSH.Utils/BOSH.Utils.psm1 @@ -0,0 +1,376 @@ +<# +.Synopsis + Common Utils +.Description + This cmdlet enables common utils for BOSH +#> + +function Write-Log { + Param ( + [Parameter(Mandatory=$True,Position=1)][string]$Message, + [string]$LogFile="C:\provision\log.log" + ) + + New-Item -Path $(split-path $LogFile -parent) -ItemType Directory -Force | Out-Null + + $msg = "{0} {1}" -f (Get-Date -Format o), $Message + Add-Content -Path $LogFile -Value $msg -Encoding 'UTF8' + Write-Host $msg +} + +function Get-Log { + Param ( + [string]$LogFile="C:\\provision\\log.log" + ) + + if (Test-Path $LogFile) { + Get-Content -Path $LogFile + } else { + Throw "Missing log file: $LogFile" + } +} + +function Open-Zip { + param( + [string]$ZipFile= $(Throw "Provide a ZipFile to extract"), + [string]$OutPath= $(Throw "Provide an OutPath for extract"), + [bool]$Keep=$True) + + $ZipFile = (Resolve-Path $ZipFile).Path + $OutPath = (Resolve-Path $OutPath).Path + Remove-Item "$OutPath\*" -Force -Recurse + Write-Log "Unzipping: ${ZipFile} to ${OutPath}" + + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($ZipFile, $OutPath) + + if (!$Keep) { + Write-Log "Unzip: removing zipfile ${ZipFile}" + Remove-Item -Path $ZipFile -Force + } +} + +function New-Provisioner { + param( + [string]$Dir="C:\\provision" + ) + + if (Test-Path $Dir) { + Remove-Item -Path $Dir -Recurse -Force + } + New-Item -ItemType Directory -Path $Dir +} + +function Clear-Provisioner { + param( + [string]$Dir="C:\\provision" + ) + + if (Test-Path $Dir) { + Remove-Item -Path $Dir -Recurse -Force + if (Test-Path $Dir) { + Throw "Unable to clean provisioner: $Dir" + } + } +} + +function Protect-Dir { + Param( + [string]$path = $(Throw "Provide a directory to set ACL on"), + [bool]$disableInheritance=$True + ) + + Write-Log "Protect-Dir: Grant Administrator" + cmd.exe /c cacls.exe $path /T /E /P Administrators:F + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + Write-Log "Protect-Dir: Remove BUILTIN\Users" + cmd.exe /c cacls.exe $path /T /E /R "BUILTIN\Users" + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + Write-Log "Protect-Dir: Remove BUILTIN\IIS_IUSRS" + cmd.exe /c cacls.exe $path /T /E /R "BUILTIN\IIS_IUSRS" + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + if ($disableInheritance) { + Write-Log "Protect-Dir: Disable Inheritance" + $acl = Get-ACL -LiteralPath $path + $acl.SetAccessRuleProtection($True, $True) + Set-Acl -LiteralPath $path -AclObject $acl + } +} + +function Protect-Path { + Param( + [string]$path = $(Throw "Provide a directory to set ACL on"), + [bool]$disableInheritance=$True + ) + + Write-Log "Protect-Path: Grant Administrator" + cmd.exe /c cacls.exe $path /E /P Administrators:F + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + Write-Log "Protect-Path: Remove BUILTIN\Users" + cmd.exe /c cacls.exe $path /E /R "BUILTIN\Users" + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + Write-Log "Protect-Path: Remove BUILTIN\IIS_IUSRS" + cmd.exe /c cacls.exe $path /E /R "BUILTIN\IIS_IUSRS" + if ($LASTEXITCODE -ne 0) { + Throw "Error setting ACL for $path exited with $LASTEXITCODE" + } + + if ($disableInheritance) { + Write-Log "Protect-Path: Disable Inheritance" + $acl = Get-ACL -LiteralPath $path + $acl.SetAccessRuleProtection($True, $True) + Set-Acl -LiteralPath $path -AclObject $acl + } +} + + + +function Set-ProxySettings { + Param([string]$HTTPProxy,[string]$HTTPSProxy,[string]$BypassList) + + $ProxyServerString = "" + + if ($HTTPProxy) { + $ProxyServerString = "http=$HTTPProxy" + + } + if ($HTTPSProxy) { + $ProxyServerString = "$ProxyServerString;https=$HTTPSProxy" + } + + function Add-ProxySettings { + Param( + [Parameter(Mandatory=$False)] + [string]$Proxy + , + [Parameter(Mandatory=$False)] + [string]$BypassProxy + ) + + [string] $start = [System.Text.Encoding]::ASCII.GetString([byte[]](70, 0, 0, 0, 25, 0, 0, 0, 3, 0, 0, 0, 29, 0, 0, 0 ), 0, 16); + [string] $endproxy = [System.Text.Encoding]::ASCII.GetString([byte[]]( 233, 0, 0, 0 ), 0, 4); + [string] $end = [System.Text.Encoding]::ASCII.GetString([byte[]]( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 0, 36); + + [string] $text = "$($start)$($Proxy)$($endproxy)$($BypassProxy)$($end)"; + [byte[]] $data = [System.Text.Encoding]::ASCII.GetBytes($text); + + $regKeyConnections = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" + Set-ItemProperty -Path $regKeyConnections -Name "DefaultConnectionSettings" -Value $data -ErrorVariable err 2>&1 | Out-Null + Write-Host "Added the IE registry key" + if ($err -ne "") { + throw "Failed to set proxy settings: $($err)" + } + } + + if ($ProxyServerString) { + Add-ProxySettings $ProxyServerString $BypassList + + #Also add NetSH proxy settings for Windows-Updates + $set_proxy = "" + if ($BypassList) { + $set_proxy = & cmd.exe /c "netsh winhttp set proxy proxy-server=`"$ProxyServerString`" bypass-list=`"$BypassList`"" + } else { + $set_proxy = & cmd.exe /c "netsh winhttp set proxy proxy-server=`"$ProxyServerString`"" + } + Write-Log "$set_proxy" + + if ($LASTEXITCODE -ne 0) { + exit(1) + } + } +} + +function Clear-ProxySettings { + $regKeyConnections = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" + $item = Get-Item $regKeyConnections + if ($item.Property) { + Remove-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name "DefaultConnectionSettings" + + #We need to pipe the command through Out-String in order to convert its output into a proper string + $reset_proxy = (& cmd.exe /c "netsh winhttp reset proxy") | Out-String + Write-Log "Cleared proxy settings: $reset_proxy" + } else { + Write-Log "No proxy settings set. There is nothing to clear." + } +} + +function Disable-RC4() { + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers' -Name 'RC4 128/128' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers' -Name 'RC4 40/128' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers' -Name 'RC4 56/128' -Force + + Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 128/128' -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 40/128' -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\RC4 56/128' -Value 0 -Name 'Enabled' -Type DWORD +} + +function Disable-TLS1() { + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\' -Name 'TLS 1.0' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0' -Name 'Server' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0' -Name 'Client' -Force + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Value 1 -Name 'DisabledByDefault' -Type DWORD + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client" -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client" -Value 1 -Name 'DisabledByDefault' -Type DWORD +} + +function Disable-TLS11() { + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\' -Name 'TLS 1.1' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1' -Name 'Server' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1' -Name 'Client' -Force + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Value 1 -Name 'DisabledByDefault' -Type DWORD + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Value 0 -Name 'Enabled' -Type DWORD + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Value 1 -Name 'DisabledByDefault' -Type DWORD +} + + +function Enable-TLS12() { + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\' -Name 'TLS 1.2' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2' -Name 'Server' -Force + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2' -Name 'Client' -Force + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" -Value 1 -Name 'Enabled' -Type DWORD + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" -Value 1 -Name 'Enabled' -Type DWORD +} + +function Disable-3DES() { + New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\' -Name 'Triple DES 168' -Force + + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers\Triple DES 168" -Value 0 -Name 'Enabled' -Type DWORD +} + +function Disable-DCOM() { + Set-ItemProperty -Path "HKLM:\Software\Microsoft\OLE" -Value 'N' -Name 'EnableDCOM' +} + +function Get-OSVersionString { + return [System.Environment]::OSVersion.Version.ToString() +} + +function Get-OSVersion { + try + { + $osVersion = Get-OSVersionString + if ($osVersion -match "6\.3\.9600\..+") + { + Write-Log "Found OS version: Windows 2012R2" + "windows2012R2" + } + elseif ($osVersion -match "10\.0\.16299\..+") + { + Write-Log "Found OS version: Windows 1709" + "windows2016" + } + elseif ($osVersion -match "10\.0\.17134\..+") + { + Write-Log "Found OS version: Windows 1803" + "windows1803" + } + elseif ($osVersion -match "10\.0\.17763\..+") + { + Write-Log "Found OS version: Windows 2019" + "windows2019" + } + else { + throw "invalid OS detected" + } + } + catch [Exception] + { + Write-Log $_.Exception.Message + throw $_.Exception + } +} + +function New-VersionFile { + param([string]$Version) + + if (!$Version) { + throw "-Version parameter must be specified as major.minor[.whatever]" + } + + $truncatedVersion = $Version.Split('.')[0..1] -Join '.' + + New-Item -Path "C:\\var\\vcap\\bosh\\etc" -ItemType 'directory' + New-Item -Path "C:\\var\\vcap\\bosh\\etc\\stemcell_version" -ItemType 'file' -Value $truncatedVersion +} + +function Get-WinRMConfig { + Invoke-Expression "winrm get winrm/config" -OutVariable result -ErrorVariable err 2>&1 | Out-Null + + if ($err -ne "") { + throw "Failed to get WinRM config: $err" + } + + return $result +} + +function Get-WUCerts { + Write-Log "Loading certs from windows update server" + $sstfile = SST-Path + Invoke-Certutil -generateSSTFromWU $sstfile + Invoke-Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root -FilePath $sstfile + Invoke-Remove-Item -path $sstfile +} + +function SST-Path { + return [System.IO.Path]::GetTempPath() + 'roots.sst' +} + +function Invoke-Import-Certificate { + Param( + [Parameter(Mandatory=$True)] + [string]$CertStoreLocation + , + [Parameter(Mandatory=$True)] + [string]$FilePath + ) + Import-Certificate -CertStoreLocation $CertStoreLocation -FilePath $FilePath + if ($LASTEXITCODE -ne 0) { + Throw "Error importing cert file from windows update server, exited with $LASTEXITCODE" + } +} + +function Invoke-Certutil { + Param( + [Parameter(Mandatory=$True)] + [string]$generateSSTFromWU + ) + certutil -generateSSTFromWU $generateSSTFromWU + if ($LASTEXITCODE -ne 0) { + Throw "Error generating cert file from windows update server, exited with $LASTEXITCODE" + } +} + +function Invoke-Remove-Item { + Param( + [Parameter(Mandatory=$True)] + [string]$path + ) + Remove-Item -path $path +} + +function Enable-Hyper-V { + Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -norestart +} diff --git a/modules/BOSH.Utils/example.zip b/modules/BOSH.Utils/example.zip new file mode 100644 index 0000000000000000000000000000000000000000..0304684e793f72c79365982c269c15db41edf95b GIT binary patch literal 171 zcmWIWW@h1H00FDuKCgqFZ}eG#Y!K#PkYPy6%t_TNsVE5z;bdU`aQj2VcOWjU;AUWC z`O3(^z#;-v2U5co;LXS+$BfG$37|3t2A~FpEsY=+!dO;_v1mpHc(byB)G-2~ACPtj GaTovq?;@7~ literal 0 HcmV?d00001 diff --git a/modules/BOSH.WinRM/BOSH.WinRM.psd1 b/modules/BOSH.WinRM/BOSH.WinRM.psd1 new file mode 100644 index 00000000..46882026 --- /dev/null +++ b/modules/BOSH.WinRM/BOSH.WinRM.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.WinRM' +ModuleVersion = '0.1' +GUID = '43f3e65d-b18e-4277-abc8-12c60a8f1f52' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Commands for WinRM on a BOSH deployed vm' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils') +FunctionsToExport = @('Enable-WinRM') +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('WinRM') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.WinRM/BOSH.WinRM.psm1 b/modules/BOSH.WinRM/BOSH.WinRM.psm1 new file mode 100644 index 00000000..7be2a423 --- /dev/null +++ b/modules/BOSH.WinRM/BOSH.WinRM.psm1 @@ -0,0 +1,73 @@ +<# +.Synopsis + Enables WinRM +.Description + This cmdlet enables the WinRM endpoint using http and basic auth by default +#> + +function Enable-WinRM { + Write-Log "Start WinRM with defaults" + runCmd 'winrm quickconfig -q' + + Write-Log "Getting WinRM config" + runCmd 'winrm get winrm/config' + + Write-Log "Override defaults to allow unlimited shells/processes/memory" + runCmd 'winrm set winrm/config @{MaxTimeoutms="7200000"}' + runCmd 'winrm set winrm/config/winrs @{MaxMemoryPerShellMB="0"}' + runCmd 'winrm set winrm/config/winrs @{MaxProcessesPerShell="0"}' + runCmd 'winrm set winrm/config/winrs @{MaxShellsPerUser="0"}' + runCmd 'winrm set winrm/config/winrs @{MaxConcurrentUsers="30"}' + runCmd 'winrm set winrm/config/service @{MaxConcurrentOperationsPerUser="5000"}' + + Write-Log "Enable HTTP" + runCmd 'winrm quickconfig -transport:http' + + Write-Log "Enable insecure basic auth over http" + runCmd 'winrm set winrm/config/service/auth @{Basic="true"}' + runCmd 'winrm set winrm/config/client/auth @{Basic="true"}' + runCmd 'winrm set winrm/config/service @{AllowUnencrypted="true"}' + + Write-Log "Win RM listener Address/Port" + runCmd 'winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"}' + + Write-Log "Ensure the Windows firewall allows WinRM traffic through" + Enable-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" + + Write-Log "Win RM port open" + runCmd 'netsh firewall add portopening TCP 5985 "Port 5985"' + + Write-Log "Getting WinRM config after" + runCmd 'winrm get winrm/config' +} + +function runCmd { + Param( + [string]$arg + ) + $command_log_ouput = & cmd.exe /c $arg + + Write-Log "Running: $arg" + Write-Log "$command_log_ouput" + + if ($LASTEXITCODE -ne 0) { + Write-Log "Error running: $arg" + } +} + +function Write-Log { + Param ( + [Parameter(Mandatory=$True,Position=1)][string]$Message, + [string]$LogFile="C:\provision\log.log" + ) + + $LogDir = (split-path $LogFile -parent) + If ((Test-Path $LogDir) -ne $True) { + New-Item -Path $LogDir -ItemType Directory -Force + } + + $msg = "{0} {1}" -f (Get-Date -Format o), $Message + Add-Content -Path $LogFile -Value $msg -Encoding 'UTF8' + Write-Host $msg +} + diff --git a/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.Tests.ps1 b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.Tests.ps1 new file mode 100644 index 00000000..06dcdb68 --- /dev/null +++ b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.Tests.ps1 @@ -0,0 +1,215 @@ +Remove-Module -Name BOSH.WindowsUpdates -ErrorAction Ignore +Import-Module ./BOSH.WindowsUpdates.psm1 + +Remove-Module -Name BOSH.Utils -ErrorAction Ignore +Import-Module ../BOSH.Utils/BOSH.Utils.psm1 + +Describe "Disable-AutomaticUpdates" { + + BeforeEach { + $oldWuauStatus = (Get-Service wuauserv).Status + $oldWuauStartMode = ( Get-Service wuauserv ).StartType + + { Set-Service -Name wuauserv -StartupType "Manual" } | Should Not Throw + { Set-Service -Name wuauserv -Status "Running" } | Should Not Throw + + + $oldAUOptions = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').AUOptions + $oldEnableFeaturedSoftware = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').EnableFeaturedSoftware + $oldIncludeRecUpdates = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').IncludeRecommendedUpdates + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 2 -Name 'AUOptions' + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 2 -Name 'EnableFeaturedSoftware' + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 2 -Name 'IncludeRecommendedUpdates' + + { Disable-AutomaticUpdates } | Should Not Throw + } + + AfterEach { + if ($oldAUOptions -eq "") { + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name 'AUOptions' + } else { + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value $oldAUOptions -Name 'AUOptions' + } + + if ($oldEnableFeaturedSoftware -eq "") { + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name 'EnableFeaturedSoftware' + } else { + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value $oldEnableFeaturedSoftware -Name 'EnableFeaturedSoftware' + } + + if ($oldIncludeRecUpdates -eq "") { + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name 'IncludeRecommendedUpdates' + } else { + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value $oldAUOptions -Name 'IncludeRecommendedUpdates' + } + + { Set-Service -Name wuauserv -StartupType $oldWuauStartMode } | Should Not Throw + if ($oldWuauStatus -eq "Stopped") { + Stop-Service wuauserv + } else { + { Set-Service -Name wuauserv -Status $oldWuauStatus } | Should Not Throw + } + } + + It "stops and disables the Windows Updates service" { + (Get-Service -Name "wuauserv").Status | Should Be "Stopped" + (Get-WmiObject -Class Win32_Service -Property StartMode -Filter "Name='wuauserv'").StartMode | Should Be "Disabled" + } + + It "sets registry keys to stop automatically installing updates" { + (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').AUOptions | Should Be "1" + (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').EnableFeaturedSoftware | Should Be "0" + (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update').IncludeRecommendedUpdates | Should Be "0" + } +} + +Describe "Enable-SecurityPatches" { + It "enables CVE-2015-6161" { + $handlerHardeningPath32Exists = $false + $oldIExplore32 = "" + if (Test-Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING") { + $handlerHardeningPathExists32 = $true + $oldIExplore32 = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING").'iexplore.exe' + } + + $handlerHardeningPath64Exists = $false + $oldIExplore64 = "" + if (Test-Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING") { + $handlerHardeningPath64Exists = $true + $oldIExplore64 = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING").'iexplore.exe' + } + + { Enable-CVE-2015-6161 } | Should Not Throw + + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING").'iexplore.exe' | Should Be "1" + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING").'iexplore.exe' | Should Be "1" + + if ($handlerHardeningPath32Exists) { + if ($oldIExplore32 -eq "") + { + Remove-Item-Property -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" -Name "iexplore.exe" + } else { + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" -Value $oldIExplore32 -Name "iexplore.exe" + } + } else { + Remove-Item "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" + } + + if ($handlerHardeningPath32Exists) { + if ($oldIExplore64 -eq "") + { + Remove-Item-Property -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" -Name "iexplore.exe" + } else { + Set-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" -Value $oldIExplore64 -Name "iexplore.exe" + } + } else { + Remove-Item "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" + } + } + + It "enables CVE-2017-8529" { + $disclosureFixPathExists32 = $false + $oldIExplore32 = "" + if (Test-Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX") { + $disclosureFixPathExists32 = $true + $oldIExplore32 = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX").'iexplore.exe' + } + + $disclosureFixPathExists64 = $false + $oldIExplore64 = "" + if (Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX") { + $disclosureFixPathExists64 = $true + $oldIExplore64 = (Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX").'iexplore.exe' + } + + { Enable-CVE-2017-8529 } | Should Not Throw + + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX").'iexplore.exe' | Should Be "1" + (Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX").'iexplore.exe' | Should Be "1" + + if ($disclosureFixPathExists32) { + if ($oldIExplore32 -eq "") + { + Remove-Item-Property -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" -Name "iexplore.exe" + } else { + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" -Value $oldIExplore32 -Name "iexplore.exe" + } + } else { + Remove-Item "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" + } + + if ($disclosureFixPathExists64) { + if ($oldIExplore64 -eq "") + { + Remove-Item-Property -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" -Name "iexplore.exe" + } else { + Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" -Value $oldIExplore64 -Name "iexplore.exe" + } + } else { + Remove-Item "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" + } + } + + It "enables CredSSP" { + $credSSPPathExists = $false + $credSSPParamPathExists = $false + $oldEcryptOracle = "" + if ( Test-Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP" ) + { + $credSSPPathExists = $true + if ( Test-Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters") { + $credSSPParamPathExists = $true + $oldEcryptOracle = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters").AllowEncryptionOracle + } + } + + { Enable-CredSSP } | Should Not Throw + + (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters").AllowEncryptionOracle | Should Be "1" + + if ($credSSPPathExists) { + if ( $credSSPParamPathExists ) { + if ($oldEcryptOracle -eq "") + { + Remove-Item-Property -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters" -Name "AllowEncryptionOracle" + } else { + Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters" -Value $oldEcryptOracle -Name "AllowEncryptionOracle" + } + } else { + Remove-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters" + } + } else { + Remove-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP" -Recurse + + } + } +} + +Describe "Upgrade-PSVersion" { + It "Only installs if powershell 5.1 or above is not installed" { + Mock Test-PSVersion { $true } -ModuleName BOSH.WindowsUpdates + Mock Invoke-WebRequest { } -ModuleName BOSH.WindowsUpdates + Mock Start-Process { } -ModuleName BOSH.WindowsUpdates + + { Upgrade-PSVersion } | Should Not Throw + + Assert-MockCalled Test-PSVersion -Times 1 -Scope It -ModuleName BOSH.WindowsUpdates + Assert-MockCalled Invoke-WebRequest -Times 0 -Scope It -ModuleName BOSH.WindowsUpdates + Assert-MockCalled Start-Process -Times 0 -Scope It -ModuleName BOSH.WindowsUpdates + } + + It "Only installs if powershell 5.1 or above is not installed" { + Mock Test-PSVersion { $false } -ModuleName BOSH.WindowsUpdates + Mock Invoke-WebRequest { } -ModuleName BOSH.WindowsUpdates + Mock Start-Process { } -ModuleName BOSH.WindowsUpdates + + { Upgrade-PSVersion } | Should Not Throw + + Assert-MockCalled Test-PSVersion -Times 1 -Scope It -ModuleName BOSH.WindowsUpdates + Assert-MockCalled Invoke-WebRequest -Times 1 -Scope It -ParameterFilter { $Uri -eq "https://go.microsoft.com/fwlink/?linkid=839516" -and $Outfile -eq "C:\provision\PS51.msu" -and $UseBasicParsing.IsPresent } -ModuleName BOSH.WindowsUpdates + Assert-MockCalled Start-Process -Times 1 -Scope It -ParameterFilter { $FilePath -eq "C:\provision\PS51.msu" -and $ArgumentList -eq '/quiet /norestart /log:"C:\provision\psupgrade.log"' -and $Wait.IsPresent -and $Passthru.IsPresent } -ModuleName BOSH.WindowsUpdates + } +} + +Remove-Module -Name BOSH.WindowsUpdates -ErrorAction Ignore +Remove-Module -Name BOSH.Utils -ErrorAction Ignore diff --git a/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psd1 b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psd1 new file mode 100644 index 00000000..ab772cf1 --- /dev/null +++ b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psd1 @@ -0,0 +1,21 @@ +@{ +RootModule = 'BOSH.WindowsUpdates' +ModuleVersion = '0.1' +GUID = 'f46b71ee-e11d-4f80-34c7-665d64250ae8' +Author = 'BOSH' +Copyright = '(c) 2017 BOSH' +Description = 'Install Windows Updates on a BOSH deployed vm' +PowerShellVersion = '4.0' +RequiredModules = @('BOSH.Utils','BOSH.WinRM','BOSH.Autologon') +FunctionsToExport = '*' +CmdletsToExport = @() +VariablesToExport = '*' +AliasesToExport = @() +PrivateData = @{ + PSData = @{ + Tags = @('Windows', 'Updates') + LicenseUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder/blob/master/LICENSE' + ProjectUri = 'https://github.com/cloudfoundry-incubator/bosh-windows-stemcell-builder' + } +} +} diff --git a/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psm1 b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psm1 new file mode 100644 index 00000000..c382847c --- /dev/null +++ b/modules/BOSH.WindowsUpdates/BOSH.WindowsUpdates.psm1 @@ -0,0 +1,385 @@ +<# +.Synopsis + Install Windows Updates +.Description + This cmdlet installs all available Windows Updates in batches +#> + +# Do not place these inside a function - they will not behave as expected +$script:ScriptName = $MyInvocation.MyCommand.ToString() +$script:ScriptPath = $MyInvocation.MyCommand.Path + +function Find-WindowsUpdatesTask { + $task = Get-ScheduledTask -TaskName "InstallWindowsUpdates" -ErrorAction SilentlyContinue + return $task -ne $null +} + +function Register-WindowsUpdatesTask { + $Prin = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Administrators" -RunLevel Highest + $action = New-ScheduledTaskAction -Execute 'Powershell.exe' ` + -Argument "-Command `"Install-WindowsUpdates`" " + $trigger = New-ScheduledTaskTrigger -AtLogon -RandomDelay 00:00:30 + Register-ScheduledTask -Principal $Prin -Action $action -Trigger $trigger -TaskName "InstallWindowsUpdates" -Description "InstallWindowsUpdates" +} + +function Unregister-WindowsUpdatesTask { + $task = Find-WindowsUpdatesTask + if ($task) + { + Write-Log "Restart Scheduled Task Exists - Removing It" + Unregister-ScheduledTask -TaskName "InstallWindowsUpdates" -Confirm:$false + } +} + +function Wait-WindowsUpdates { + Param([string]$Password,[string]$User) + + Enable-Autologon -Password $Password -User $User + + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + + disable-service("WinRM") + + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" +} + +function Install-WindowsUpdates { + + # Set registry key so that we will receive the Jan 2018 patches (KB4056895) + REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat /f /v cadca5fe-87d3-4b96-b7fb-a231484277cc /t REG_DWORD /d 0 + + # Set registry keys so that KB4056898 will be enabled + # Set registry keys to cover CVE-2018-11091 CVE-2018-12126 CVE-2018-12127 CVE-2018-12130 CVE-2017-5753 CVE-2017-5715 CVE-2017-5754 CVE-2018-3639 CVE-2018-3615 CVE-2018-3620 CVE-2018-3646 + reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride /t REG_DWORD /d 72 /f + reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f + reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization" /v MinVmVersionForCpuBasedMitigations /t REG_SZ /d "1.0" /f + + if (test-path "C:\provision\patch.msu") { + Write-Log "Already installed out-of-band patch" + } else { + Set-Service -Name wuauserv -StartupType Manual + Start-Service -Name wuauserv + + Invoke-WebRequest -UseBasicParsing -Uri 'http://download.windowsupdate.com/d/msdownload/update/software/secu/2018/01/windows8.1-kb4056898-x64_ad6c91c5ec12608e4ac179b2d15586d244f0d2f3.msu' -Outfile C:\provision\patch.msu + wusa.exe C:\provision\patch.msu /quiet + start-sleep 200 + } + + $script:UpdateSession = New-Object -ComObject 'Microsoft.Update.Session' + $script:UpdateSession.ClientApplicationID = 'BOSH.WindowsUpdates' + $script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher() + $script:SearchResult = New-Object -ComObject 'Microsoft.Update.UpdateColl' + + $script:Cycles = 0 + $script:CycleUpdateCount = 0 + $script:MaxUpdatesPerCycle=500 + $script:RestartRequired=0 + $script:MoreUpdates=0 + $script:MaxCycles=5 + + Get-UpdateBatch + if ($script:MoreUpdates -eq 1) { + Install-UpdateBatch + } else { + Invoke-RebootOrComplete + } +} + +function Invoke-RebootOrComplete() { + $RegistryEntry = "InstallWindowsUpdates" + switch ($script:RestartRequired) { + 0 { + Unregister-WindowsUpdatesTask + + Write-Log "No Restart Required" + Get-UpdateBatch + + if (($script:MoreUpdates -eq 1) -and ($script:Cycles -le $script:MaxCycles)) { + Install-UpdateBatch + } elseif ($script:Cycles -gt $script:MaxCycles) { + Write-Log "Exceeded Cycle Count - Stopping" + Enable-WinRM + Disable-Autologon + } else { + Write-Log "Done Installing Windows Updates" + Enable-WinRM + Disable-Autologon + } + + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + } + 1 { + $prop = Find-WindowsUpdatesTask + if (-not $prop ) { + Write-Log "Restart Scheduled Task Does Not Exist - Creating It" + Register-WindowsUpdatesTask + } else { + Write-Log "Restart Scheduled Task Exists Already" + } + + Write-Log "Restart Required - Restarting..." + Restart-Computer + } + default { + Write-Log "Unsure If A Restart Is Required" + break + } + } +} + +function Install-UpdateBatch() { + $script:Cycles++ + Write-Log "Evaluating Available Updates with limit of $($script:MaxUpdatesPerCycle):" + $UpdatesToDownload = New-Object -ComObject 'Microsoft.Update.UpdateColl' + $script:i = 0; + if ($Host.Version.Major -eq 5) { + $CurrentUpdates = $SearchResult.Updates + } else { + $CurrentUpdates = $SearchResult.Updates | Select-Object + } + while($script:i -lt $SearchResult.Updates.Count -and $script:CycleUpdateCount -lt $script:MaxUpdatesPerCycle) { + $Update = $CurrentUpdates[$script:i] + if (($null -ne $Update) -and (!$Update.IsDownloaded)) { + [bool]$addThisUpdate = $false + if ($Update.InstallationBehavior.CanRequestUserInput) { + Write-Log "> Skipping: $($Update.Title) because it requires user input" + } else { + if (!($Update.EulaAccepted)) { + Write-Log "> Note: $($Update.Title) has a license agreement that must be accepted. Accepting the license." + $Update.AcceptEula() + [bool]$addThisUpdate = $true + $script:CycleUpdateCount++ + } else { + [bool]$addThisUpdate = $true + $script:CycleUpdateCount++ + } + } + + if ([bool]$addThisUpdate) { + Write-Log "Adding: $($Update.Title)" + $UpdatesToDownload.Add($Update) |Out-Null + } + } + $script:i++ + } + + if ($UpdatesToDownload.Count -eq 0) { + Write-Log "No Updates To Download..." + } else { + Write-Log 'Downloading Updates...' + $ok = 0; + while (! $ok) { + try { + $Downloader = $UpdateSession.CreateUpdateDownloader() + $Downloader.Updates = $UpdatesToDownload + $Downloader.Download() + $ok = 1; + } catch { + Write-Log $_.Exception | Format-List -force + Write-Log "Error downloading updates. Retrying in 30s." + $script:attempts = $script:attempts + 1 + Start-Sleep -s 30 + } + } + } + + $UpdatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl' + [bool]$rebootMayBeRequired = $false + Write-Log 'The following updates are downloaded and ready to be installed:' + foreach ($Update in $SearchResult.Updates) { + if (($Update.IsDownloaded)) { + Write-Log "> $($Update.Title)" + $UpdatesToInstall.Add($Update) |Out-Null + + if ($Update.InstallationBehavior.RebootBehavior -gt 0){ + [bool]$rebootMayBeRequired = $true + } + } + } + + if ($UpdatesToInstall.Count -eq 0) { + Write-Log 'No updates available to install...' + $script:MoreUpdates=0 + $script:RestartRequired=0 + Enable-WinRM + + Write-Log "Getting WinRM config" + $winrm_config = Get-WinRMConfig + Write-Log "$winrm_config" + break + } + + if ($rebootMayBeRequired) { + Write-Log 'These updates may require a reboot' + $script:RestartRequired=1 + } + + Write-Log 'Installing updates...' + + $Installer = $script:UpdateSession.CreateUpdateInstaller() + $Installer.Updates = $UpdatesToInstall + $InstallationResult = $Installer.Install() + + Write-Log "Installation Result: $($InstallationResult.ResultCode)" + Write-Log "Reboot Required: $($InstallationResult.RebootRequired)" + Write-Log 'Listing of updates installed and individual installation results:' + if ($InstallationResult.RebootRequired) { + $script:RestartRequired=1 + } else { + $script:RestartRequired=0 + } + + for($i=0; $i -lt $UpdatesToInstall.Count; $i++) { + New-Object -TypeName PSObject -Property @{ + Title = $UpdatesToInstall.Item($i).Title + Result = $InstallationResult.GetUpdateResult($i).ResultCode + } + Write-Log "Item: $UpdatesToInstall.Item($i).Title" + Write-Log "Result: $InstallationResult.GetUpdateResult($i).ResultCode" + } + + Invoke-RebootOrComplete +} + +function Get-UpdateBatch() { + Write-Log "Checking For Windows Updates" + $Username = $env:USERDOMAIN + "\" + $env:USERNAME + + New-EventLog -Source $script:ScriptName -LogName 'Windows Powershell' -ErrorAction SilentlyContinue + + $Message = "Script: " + $script:ScriptPath + "`nScript User: " + $Username + "`nStarted: " + (Get-Date).toString() + + Write-EventLog -LogName 'Windows Powershell' -Source $script:ScriptName -EventID "104" -EntryType "Information" -Message $Message + Write-Log $Message + + $script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher() + $script:successful = $FALSE + $script:attempts = 0 + $script:maxAttempts = 12 + while(-not $script:successful -and $script:attempts -lt $script:maxAttempts) { + try { + $script:SearchResult = $script:UpdateSearcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0") + $script:successful = $TRUE + } catch { + Write-Log $_.Exception | Format-List -force + Write-Log "Search call to UpdateSearcher was unsuccessful. Retrying in 10s." + $script:attempts = $script:attempts + 1 + Start-Sleep -s 10 + } + } + + if ($SearchResult.Updates.Count -ne 0) { + $Message = "There are " + $SearchResult.Updates.Count + " more updates." + Write-Log $Message + try { + for($i=0; $i -lt $script:SearchResult.Updates.Count; $i++) { + Write-Log $script:SearchResult.Updates.Item($i).Title + Write-Log $script:SearchResult.Updates.Item($i).Description + Write-Log $script:SearchResult.Updates.Item($i).RebootRequired + Write-Log $script:SearchResult.Updates.Item($i).EulaAccepted + } + $script:MoreUpdates=1 + } catch { + Write-Log $_.Exception | Format-List -force + Write-Log "Showing SearchResult was unsuccessful. Rebooting." + $script:RestartRequired=1 + $script:MoreUpdates=0 + Invoke-RebootOrComplete + Write-Log "Show never happen to see this text!" + Restart-Computer + } + } else { + Write-Log 'There are no applicable updates' + $script:RestartRequired=0 + $script:MoreUpdates=0 + } +} + +function Search-InstalledUpdates() { + $Session = New-Object -ComObject Microsoft.Update.Session + $Searcher = $Session.CreateUpdateSearcher() + $Searcher.Search("IsInstalled=1").Updates | Sort-Object LastDeploymentChangeTime | ForEach-Object { "KB$($_.KBArticleIDs) | $($_.Title)" } +} + +function Test-InstalledUpdates() { + Write-Host "Running Get-HotFix:" + Get-HotFix + $Session = New-Object -ComObject Microsoft.Update.Session + Write-Host "Session: $Session" + $Searcher = $Session.CreateUpdateSearcher() + Write-Host "Searcher: $Searcher" + $UninstalledUpdates = $Searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0").Updates + if ($UninstalledUpdates.Count -ne 0) { + Write-Log "The following updates are not currently installed:" + foreach ($Update in $UninstalledUpdates) { + Write-Log "> $($Update.Title)" + } + Throw 'There are uninstalled updates' + } +} + +<# +.Synopsis + Disable Automatic Updates +.Description + This cmdlet disables automatic Windows Updates +#> +function Disable-AutomaticUpdates() { + Stop-Service -Name wuauserv + Set-Service -Name wuauserv -StartupType Disabled + + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 1 -Name 'AUOptions' + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 0 -Name 'EnableFeaturedSoftware' + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Value 0 -Name 'IncludeRecommendedUpdates' +} + +function Enable-CVE-2015-6161() { + #Enable MS15-124 - Internet Explorer ASLR Bypass fix - CVE-2015-6161 + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" /t REG_DWORD /v "iexplore.exe" /d 1 /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING" /t REG_DWORD /v "iexplore.exe" /d 1 /f +} + +function Enable-CVE-2017-8529() { + #Enable Microsoft Browser Information Disclosure Vulnerability - CVE-2017-8529 + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" /v iexplore.exe /t REG_DWORD /d 1 /f + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX" /v iexplore.exe /t REG_DWORD /d 1 /f + +} + +function Enable-CredSSP() { + #Enable CredSSP updates - CVE-2018-0886 + #Policy set to "mitigated" + reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters" /v AllowEncryptionOracle /t REG_DWORD /d 1 /f +} + +function Upgrade-PSVersion () { + if (Test-PSVersion) { + Write-Log "Upgrade-PSVersion: No need to upgrade. PSVersion is 5 or above" + return + } + + $existingProtocol = [Net.ServicePointManager]::SecurityProtocol + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Write-Log "Upgrade-PSVersion: Downloading." + + $MSUPath = "c:\provision\PS51.msu" + Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=839516" -UseBasicParsing -OutFile $MSUPath + + Write-Log "Upgrade-PSVersion: Downloaded. Installing." + + $p = Start-Process -FilePath $MSUPath -ArgumentList '/quiet /norestart /log:"C:\provision\psupgrade.log"' -Wait -PassThru + Write-Log "Upgrade-PSVersion: Installed. Process exit code: $($p.ExitCode)" + [Net.ServicePointManager]::SecurityProtocol = $existingProtocol +} + +function Test-PSVersion { + $version = $PSVersionTable.PSVersion + Write-Log "Powershell is $version" + $version.Major -ge 5 +} diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 00000000..357ac2ee --- /dev/null +++ b/modules/README.md @@ -0,0 +1,30 @@ +Powershell scripts to set up a Windows VM in a manner appropriate for a BOSH Stemcell. + +## Testing + +Tests are written using the Pester testing framework and must be run in Powershell on a Windows environment. + +The test suite for each module currently assumes that the tests are being run with the module as the current working directory. + +This requires iterating through the module directories to run all the tests: + +``` +cd stembuild +foreach ($module in (Get-ChildItem "./modules").Name) { + Push-Location "modules/$module" + $results=Invoke-Pester -PassThru + if ($results.FailedCount -gt 0) { + $result += $results.FailedCount + } + Pop-Location +} +echo "Failed Tests: $result" +``` + +If you just need to test a single module, you could do this: + +``` +cd "stembuild\module\BOSH." +Invoke-Pester +``` +