CT-PS-Standard.psm1

<#
--------------------------------------------------------------------------------------------------
 
This is a standard module with a set of standard functions used across multiple scripts within CT.
 
Any changes to this module need to be published using the powershell script "Build_CT_Module.ps1"
 
--------------------------------------------------------------------------------------------------
HOW TO IMPORT INTO SCRIPT:
--------------------------------------------------------------------------------------------------
This module should be imported using the commands below (do not copy the asterix's, just whats between).
This will import this module AND initialise the script with all the standard features required by scripts,
including all the log files for each transaction
*****************
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Module -Name CT-PS-Standard -Force -AllowClobber
Initialize-Script
*****************
 
--------------------------------------------------------------------------------------------------
 
 
--------------------------------------------------------------------------------------------------
LOG FILES
--------------------------------------------------------------------------------------------------
There are four log files initialised by this module that can be used for output.
You can write to each of these logs accordingly.
 
$Output_Log: This is the standard console output.
$Transcript_log: This is where the transcript is written to. You will need to start and stop the transcript inside your script by using the command "Start-Transcript -Path $Transcript_log -append | Out-Null"
$API_log: This is where the output from API posts should be sent.
$Install_log: This is where output from MSIEXEC commands should be logged
 
--------------------------------------------------------------------------------------------------
 
 
#>


function Initialize-Script {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    <#
    ---- STANDARD SCRIPT BLOCK ----
    This block is standard script settings used across all scripts and enables some consistency.
    It sets up some standard variables and starts transcript so any errors are caught and can be viewed.
    If installations are taking place via MSIEXEC, it also establishes the log file.
    If you are installing more than one installation, make sure you setup multiple Install_Log variables for each installation and name the end of the log file accordingly
    It starts transcript to the transcript log file so the history can be accessed anytime in the future
    The script then tests for the CT_DEST folder and creates it if its not there.
 
    #>

    
    Begin{
        # Display some troubleshoot info
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Write-Verbose "MyInvocation info:
         
        MyCommand: $($MyInvocation.MyCommand)
        ScriptName: $($MyInvocation.ScriptName)
        PSScriptRoot: $($MyInvocation.PSScriptRoot)
        PSCommandPath: $($MyInvocation.PSCommandPath)
        InvocationName: $($MyInvocation.InvocationName)
        ScriptLineNumber: $($MyInvocation.ScriptLineNumber)
        "


        $P_MyInvocation = $PSCmdlet.SessionState.PSVariable.Get('MyInvocation').Value

        Write-Verbose "Parent MyInvocation info:
         
        MyCommand: $($P_MyInvocation.MyCommand)
        ScriptName: $($P_MyInvocation.ScriptName)
        PSScriptRoot: $($P_MyInvocation.PSScriptRoot)
        PSCommandPath: $($P_MyInvocation.PSCommandPath)
        InvocationName: $($P_MyInvocation.InvocationName)
        ScriptLineNumber: $($P_MyInvocation.ScriptLineNumber)
        "


        $global:ErrorActionPreference = "Stop"

        if ($MyInvocation.ScriptName.Length -gt 4) {
            try {
                $Global:Script_Name = ($MyInvocation.ScriptName).Replace("$($MyInvocation.PSScriptRoot)\","")
                $Global:Script_Name = ($Global:Script_Name).Substring(0,($Global:Script_Name).Length-4)
            } catch {
                $Global:Script_Name = "Terminal"
            }
        } else {
            $Global:Script_Name = "Terminal"
        }

        $global:CT_DEST="C:\CT"  # Where the files are downloaded to
        $global:DateStamp = get-date -Format yyyyMMddTHHmmss # A formatted date strong

        $global:Output_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_output.log"  # The output
        $global:Transcript_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_transcript.log"  # The powershell transcript file
        $global:API_log = "$($CT_DEST)\logs\$($Script_Name)\$($DateStamp)_API.log"
        $global:Install_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_install.log"  # The powershell installation file



    }

    Process {

<# This next block is better placed in the script itself, not in this module
 
        write-output "********************** Script $($Script_Name) starting. **********************"
        Write-OutputLog "Output log : $($Output_log)" -verbose:$VerbosePreference
        Write-OutputLog "Transcript log : $($Transcript_log)" -verbose:$VerbosePreference
        Write-OutputLog "API log : $($API_log)" -verbose:$VerbosePreference
        Write-OutputLog "Install log (if used) : $($Install_log)" -verbose:$VerbosePreference
        Write-Output "Log files can be found at $($CT_DEST)\logs\$($Script_Name)\"
 
 #>

 
        # Check for a CT folder on the C: and if not, create it, however that location should already exist as part of the Start-Transcript command.
        if(-not( Test-Path -Path $CT_DEST )) {
            try{
                mkdir $CT_DEST > $null
                #Transcript-Log "New folder created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                Write-Error "Cannot create folder $CT_DEST. $($Error[0].Exception.Message)"
                Write-Error $_ -verbose:$VerbosePreference
                #Stop-Transcript
                exit 1
            }
        }

        if(-not( Test-Path -Path "$($CT_DEST)\logs" )) {
            try{
                mkdir "$($CT_DEST)\logs" > $null
                #Transcript-Log "New logs folder created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                Write-Error "Cannot create logs folder in $CT_DEST. $($Error[0].Exception.Message)"
                Write-Error $_ -verbose:$VerbosePreference
                #Stop-Transcript
                exit 3
            }
        }

        if(-not( Test-Path -Path "$($CT_DEST)\logs\$($Script_Name)" )) {
            try{
                mkdir "$($CT_DEST)\logs\$($Script_Name)" > $null
                #Transcript-Log "New logs folder for $($Script_Name) created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                Write-Error "Cannot create logs folder for $($Script_Name) in $CT_DEST. $($Error[0].Exception.Message)"
                Write-Error $_
                #Stop-Transcript
                exit 4
            }
        }

        $global:CT_Reg_Path = "HKLM:\Software\CT\Monitoring"
        $global:CT_Reg_Key = "$($CT_Reg_Path)\$($Script_Name)"
        if(-not( Test-Path -Path $CT_Reg_Key )) {
            try{
                $CTMonitoringReg = New-Item -Path $CT_Reg_Path -Name $Script_Name -Force
                Set-ItemProperty -Path "HKLM:\Software\CT" -Name "CustomerNo" -Value $customer
            }catch{
                #Can't create the regkey, therefore cannot continue
                Write-Error "Cannot create registry key at $($CT_Reg_Key). $($Error[0].Exception.Message)"
                Write-Error "$($CTMonitoringReg)"
                Write-Error $_
                #Stop-Transcript
                exit 5
            }
        }

        # Create Transcript header
        Write-OutputLog "**********************"
        Write-OutputLog "Script: $($PSCmdlet.MyInvocation.ScriptName)."
        Write-OutputLog "Start time: $($DateStamp)"
        Write-OutputLog "Username: $($env:USERDOMAIN)\$($env:USERNAME)"
        Write-OutputLog "Execution Policy Preference: $($env:PSExecutionPolicyPreference)"
        Write-OutputLog "Machine: $($env:COMPUTERNAME) ($($env:OS))"
        Write-OutputLog "Process ID: $($PID))"
        Write-OutputLog "PSVersion: $($PSVersionTable.PSVersion)"
        Write-OutputLog "PSEdition: $($PSVersionTable.PSEdition)"
        Write-OutputLog "PSCompatibleVersions: $($PSVersionTable.PSCompatibleVersions)"
        Write-OutputLog "BuildVersion: $($PSVersionTable.BuildVersion)"
        Write-OutputLog "CLRVersion: $($PSVersionTable.CLRVersion)"
        Write-OutputLog "WSManStackVersion: $($PSVersionTable.WSManStackVersion)"
        Write-OutputLog "PSRemotingProtocolVersion: $($PSVersionTable.PSRemotingProtocolVersion)"
        Write-OutputLog "SerializationVersion: $($PSVersionTable.SerializationVersion)"
        Write-OutputLog "**********************"


        <#
        ---- END STANDARD SCRIPT BLOCK----
        #>

    }

}


Function Input-Box {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory=$True)]
        $message,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory=$True)]
        $title,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $defaultvalue
    )
    Process{
        [void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
        Add-Type -AssemblyName Microsoft.VisualBasic

        return [Microsoft.VisualBasic.Interaction]::InputBox($message, $title, $defaultvalue)
    }
}



Function Write-OutputLog {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $output
    )
    Process{
        if ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -eq $true) {
            Write-Verbose "(Line $($MyInvocation.ScriptLineNumber)) $($output)"
        }
        $output | Out-File -FilePath "$($Output_log)" -Append
    }
}


# Writes to the API log and optionally console if -Verbose flag is set at script level
Function Write-APILog {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $output
    )
    Process{
        if ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -eq $true) {
            Write-Verbose "(Line $($MyInvocation.ScriptLineNumber)) $($output)"
        }
        $output | Out-File -FilePath "$($API_log)" -Append
    }
}


Function Request-Download {
    # Downloads a file using BITS if possible, and if BITS is not available, downloads directly from URL
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $FILE_URL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $FILE_LOCAL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch] $NoBITS # This is for when BITS should not be used
    )

    Process{
        # Test for existing file and remove if it exists
        if(Test-Path -Path $MSIfFILE_LOCALile70 -PathType Leaf ) {
            try {
                Remove-Item $FILE_LOCAL -Force
            } catch {
                #Can't remove the MSI, therefore cannot continue
                Write-OutputLog "Cannot remove $FILE_LOCAL. Unable to continue. $($Error[0].Exception.Message)"
                return $Error
            }
        }


        if (!(Get-Module -ListAvailable -Name "BitsTransfer") -and !($NoBITS)) {
            try{
                Import-Module BitsTransfer -Force
            } catch {
                $NoBITS = $true
            }
        }




        if (!($NoBITS)) {
            # Check if BranchCache Distributed Mode is enabled, and if not, enable it so BITS uses computers on the subnet to download where available
            $BCStatus = Get-BCStatus
            if ($BCStatus.ClientConfiguration.CurrentClientMode -ne "DistributedCache") {
                try {
                    Enable-BCDistributed -Verbose -Force
                    Write-OutputLog "BranchCache Distributed Mode is now enabled" -Verbose:$VerbosePreference
                } catch {
                    #BranchCache cannot be enabled to work with BITS. BITS will download over the internet connection instead of cached copies on the local subnet
                    Write-OutputLog "Cannot enable BranchCache Distributed Mode. $($Error[0].ErrorDetails). The installation files will download over the internet connection instead of cached copies on the local subnet" -Verbose:$VerbosePreference
                }
            } else {
                Write-OutputLog "BranchCache Distributed Mode is already enabled in distributed mode on this computer" -Verbose:$VerbosePreference
            }
            $DownloadJob = Start-BitsTransfer -Priority Normal -DisplayName "$($DateStamp) $($FILE_LOCAL)" -Source $FILE_URL -Destination $FILE_LOCAL
        } else {
            try {
                $DownloadJob = Invoke-WebRequest -Uri $FILE_URL -OutFile $FILE_LOCAL -SkipCertificateCheck -PassThru
            } catch {
                Write-OutputLog "Cannot download $($FILE_URL). $_" -Verbose:$VerbosePreference
                return $_
            }
        }
        return $DownloadJob

    }
}


function Get-CurrentLineNumber {
    # Downloads a file using BITS if possible, and if BITS is not available, downloads directly from URL
    [CmdletBinding()]
    param()
    #$LineNumber =
    #$LineNo = Get-ChildItem $MyInvocation.ScriptLineNumber
    return $PSCmdlet.MyInvocation.ScriptLineNumber
}



function Update-WMF {
    [CmdletBinding()]
    param(
        [Parameter()] [switch] $ForceReboot # Forces a reboot of the machine after update has completed
    )


    $OSInfo = (Get-WMIObject win32_operatingsystem)
    $OSBuild = $OSInfo.buildnumber
    $OSArch = $OSInfo.OSArchitecture
    $PowerShellVersion = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor/10)
    $dotnetversion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -Name Release).Release
    Write-Host "DotNet Framework version $($dotnetversion) found."
    Write-Host "Powershell version $($PSVersionTable.PSVersion.ToString()) found."
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
    if ($dotnetversion -lt 379893) {
        Write-Host "Updating DotNet Framework to 4.5.2"
        $dotnet_URL = "https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe"
        $dotnet_File = "C:\CT\NDP452-KB2901907-x86-x64-AllOS-ENU.exe"
        try {
            Invoke-WebRequest -Uri $dotnet_URL -OutFile $dotnet_File -Verbose:$VerbosePreference
        } catch {
            write-host "Cannot download DotNet Framework 4.5.2. $_"
            return $false
        }

        write-host "Installing DotNet Framework 4.5.2"
        $DotNetInstall = Start-Process -FilePath $dotnet_File -ArgumentList "/q /norestart" -Wait -NoNewWindow -PassThru -Verbose:$VerbosePreference
        if (@(0,3010) -contains $DotNetInstall.ExitCode) {
            write-host "DotNet Framework 4.5.2 installed successfully. A reboot of this computer is required to complete the installation."
        } else {
            write-host "Unable to install DotNet Framework 4.5.2. Error code $($DotNetInstall.ExitCode) - $_"
            #Stop-Transcript
            return $false
        }
    }



    if($OSBuild -eq "9600" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu"
    } elseif($OSBuild -eq "9200" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        if($OSArch -eq "64-bit"){
            $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/W2K12-KB3191565-x64.msu"
        } else {
            $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu"
        }    
    }

    if ($WMF_URL) {
        
        $WMF_File = "C:\CT\WMF51.msu"

        # Test for existing WMF file and remove if it exists
        if(Test-Path -Path $WMF_File -PathType Leaf ) {
            try {
                Remove-Item $WMF_File -Force -Verbose:$VerbosePreference
                #write-host "Found old WMF update and removed."
            } catch {
                #Can't remove the WMF, therefore cannot continue
                write-host "Cannot remove $WMF_File. Unable to continue. $($Error[0].Exception.Message)"
                #Stop-Transcript
                return $false
            }
        }
        
        # Download WMF
        try {
            Invoke-WebRequest -Uri $WMF_URL -OutFile $WMF_File -Verbose:$VerbosePreference
        } catch {
            write-host "Cannot download WMF. $($WMFjob.ErrorContextDescription)"
            #Stop-Transcript
            return $false
        }
            

        write-host "Installing WMF"
        $WMFUpgrade = Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList "$($WMF_File) /quiet /norestart" -Wait -NoNewWindow -PassThru -Verbose:$VerbosePreference
        if (@(0,3010) -contains $WMFUpgrade.ExitCode) {
            write-host "WMF installed successfully. A reboot of this computer is required to complete the installation."
        } else {
            write-host "Unable to install WMF. Error code $($WMFUpgrade.ExitCode)"
            #Stop-Transcript
            return $false
        }
    }

    if ($ForceReboot) {
        Start-Sleep -Seconds 60
        Restart-Computer -Force -Verbose:$VerbosePreference
    }
    return $true

}



function Update-PowerShell {
    [CmdletBinding()]
    param(
        [Parameter()] [switch] $ForceReboot # Forces a reboot of the machine after update has completed
    )

    $WMFupgrade = $false
    $OSInfo = (Get-WMIObject win32_operatingsystem)
    $OSBuild = $OSInfo.buildnumber
    $PowerShellVersion = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor/10)
    $dotnetversion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -Name Release).Release
    Write-Host "DotNet Framework version $($dotnetversion) found."
    Write-Host "Powershell version $($PSVersionTable.PSVersion.ToString()) found."
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    
    if ($dotnetversion -lt 379893) {
        $WMFupgrade = $true
    }



    if($OSBuild -eq "9600" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMFupgrade = $true
    } elseif($OSBuild -eq "9200" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMFupgrade = $true
    }

    if($WMFupgrade -eq $true) {
        if(Update-WMF -eq $false) {
            write-host "Failed to upgrade WMF to 5.1. Please install DotNet Framework 4.5.2 and WMF 5.1 before upgrading PowerShell"
            return $false
        }
    }

    Write-Host "Now will attempt to install latest PowerShell version alongside Windows PowerShell 5.1."
    try {
        Invoke-Expression -Command "& { $(Invoke-RestMethod -Uri 'https://aka.ms/install-powershell.ps1') } -UseMSI -Quiet" -Verbose:$VerbosePreference
        #iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet"
    } catch {
        Write-Host "Unable to install latest PowerShell"
        Write-Host $_
        return $false
    }
    return $true

}