diff --git a/PSUtilities/ScriptTemplate.ps1 b/PSUtilities/ScriptTemplate.ps1 index bd8b539..4dfd530 100644 --- a/PSUtilities/ScriptTemplate.ps1 +++ b/PSUtilities/ScriptTemplate.ps1 @@ -16,10 +16,12 @@ param ( # Allows modules required by the script to be predefined, and loaded or installed as needed [Parameter(DontShow = $true)] [Array] $ModuleNames = @(), - # Sets the script storage directory into the user profile instead of in the ProgramData folder - [Parameter(Mandatory = $false)] [Switch] $UserScript = $false, + # Sets the script storage location + [Parameter(DontShow = $true)] [ValidateSet('System', 'User', 'Custom')] [String] $ScriptType = "System", + # Allows adding verbose logging and what-if command simulation to the script + [Parameter(DontShow = $true)] [Switch] $Dev, # Allows for the script logs to output to a nonstandard location. Overrides user/computer script paths - [Parameter(Mandatory = $false)] [String] $LogDir, + [Parameter(Mandatory = $false)] [String] $ScriptDir, # Flag to disable log file output [Parameter(Mandatory = $false)] [Switch] $NoLog, # Allows for uninstallation of script @@ -55,7 +57,7 @@ If ($Env:PROCESSOR_ARCHITECTURE -ne "AMD64") { #endregion Architecture Check #region Imports -# Import Test-CommandExists, Add-LogEntry, Add-ListItem, Start-Logging, Import-JSONConfig +# Import Test-CommandExists, Add-LogEntry, Add-ListItem, Import-JSONConfig, Import-YAMLConfig, Set-RegistryKey, Test-AsAdmin, Format-Hyperlink Invoke-WebRequest "https://gitea.taco.quest/Mindfang/ProjectTools/raw/branch/main/PSUtilities/ScriptTools.ps1" -OutFile "$Env:Temp\ScriptTools.ps1" Import-Module -Name "$Env:Temp\ScriptTools.ps1" -Force #endregion Imports @@ -66,11 +68,13 @@ Import-Module -Name "$Env:Temp\ScriptTools.ps1" -Force #region Prep # Set the working directory for logs and additional files $ThisScript = ([IO.FileInfo]$MyInvocation.MyCommand.Definition).BaseName -If ($UserScript) { - $AppDir = "$Env:AppData\Mindfang\$ThisScript" -} -Else { - $AppDir = "$Env:ProgramData\Mindfang\$ThisScript" +Switch ($ScriptType) { + "System" { $AppDir = "$Env:AppData\Mindfang\$ThisScript" } + "User" { $AppDir = "$Env:ProgramData\Mindfang\$ThisScript" } + "Custom" { + If ($ScriptDir) { $AppDir = $ScriptDir } + Else { $AppDir = $PWD } + } } # Create specified appdir if it does not already exist @@ -79,7 +83,7 @@ If (-Not (Test-Path $AppDir)) { } # Begin recording transcript file -If (-not $NoLog) { +If (-Not $NoLog) { If (-not $LogDir) { $LogDir = $AppDir } @@ -87,24 +91,58 @@ If (-not $NoLog) { } # Load any modules required by script -ForEach ($Module in $ModuleNames) { - If (-not(Get-Module -ListAvailable -Name $Module)) { - Try { - Add-LogEntry "Module `"$Module`" not found, installing" - Install-Module -Name $Module -Scope CurrentUser -Force -AllowClobber - } +If ($ModuleNames) { + If ([Net.ServicePointManager]::SecurityProtocol -ne [Net.SecurityProtocolType]::SystemDefault) { + Add-LogEntry "Upgrading TLS security protocol to 1.2" + Try { [Net.ServicePointManager]::SecurityProtocol = @([Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls12) } Catch { - Add-LogEntry "Module failed to install automatically! Manaully install the module, then re-run the script." -As Error - Exit 1 + Add-LogEntry "TLS upgrade failed, script is exiting" -As Error + Exit } } - Import-Module $Module + If ((Get-PackageProvider).Name -notcontains "NuGet") { + Add-LogEntry "Installing NuGet package provider" + Try { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop } + Catch { + Add-LogEntry "Installation failed, script is exiting" -As Error + Exit + } + } + If ((Get-PSRepository).Name -notcontains "PSGallery") { + Add-LogEntry "Registering PSGallery as script source" + Try { Register-PSRepository -Default -InstallationPolicy Trusted -ErrorAction Stop } + Catch { + Add-LogEntry "Unable to register PSGallery, script is exiting" -As Error + Exit + } + } + ElseIf (Get-PSRepository | Where-Object { $_.Name -eq "PSGallery" -and $_.InstallationPolicy -ne "Trusted" }) { + Add-LogEntry "Trusting packages from PSGallery" + Try { Set-PSRepository PSGallery -InstallationPolicy Trusted -ErrorAction Stop } + Catch { + Add-LogEntry "Unable to set PSGallery as trusted, script is exiting" -As Error + Exit + } + } + ForEach ($Module in $ModuleNames) { + If (-Not (Get-Module -ListAvailable -Name $Module)) { + Try { + Add-LogEntry "Module `"$Module`" not found, installing" + Install-Module -Name $Module -Scope CurrentUser -Force -AllowClobber + } + Catch { + Add-LogEntry "Module failed to install automatically! Manaully install the module, then re-run the script." -As Error + Exit 1 + } + } + Import-Module $Module + } } #endregion Prep #region Execution Try { - If ( -not $Uninstall) { + If (-Not $Uninstall) { #TODO: "Install actions" for the script go here } Else { @@ -113,14 +151,16 @@ Try { } Catch { # Write error to logs, if an exception is caught - Write-Output "Script execution failed!" - Write-Output $_ - Write-Output $_.InvocationInfo - Write-Output $_.ScriptStackTrace + Write-Host "Script execution failed!" + Write-Host $_ + Write-Host $_.InvocationInfo + Write-Host $_.ScriptStackTrace Exit 1 } Finally { # Stop transcript, even if an error has occurred - Stop-Transcript + If (-Not $NoLog) { + Stop-Transcript + } } #endregion Execution \ No newline at end of file diff --git a/PSUtilities/ScriptTools.ps1 b/PSUtilities/ScriptTools.ps1 index 77b4f20..2f710d1 100644 --- a/PSUtilities/ScriptTools.ps1 +++ b/PSUtilities/ScriptTools.ps1 @@ -32,23 +32,13 @@ Function Test-CommandExists { Detail on what the script does, if this is needed. #> - Param ( - $Command + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Command ) - $OldPreference = $ErrorActionPreference - $ErrorActionPreference = 'Stop' Try { - If (Get-Command $Command) { - Return $true - } - } - Catch { - Return $false - } - Finally { - $ErrorActionPreference = $OldPreference + If (Get-Command $Command -ErrorAction Stop) { Return $true } } + Catch { Return $false } } Function Add-LogEntry { @@ -85,23 +75,24 @@ Function Add-LogEntry { Detail on what the script does, if this is needed. #> - Param ( - [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]$Message, - [Parameter(Mandatory = $false)] [ValidateSet('Notify', 'Warning', 'Error')]$As, - [Parameter(Mandatory = $false)] [Switch]$NewLine + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Message, + [Parameter(Mandatory = $false)] [ValidateSet('Notify', 'Warning', 'Error', 'Debug')] [String] $As, + [Parameter(Mandatory = $false)] [Switch] $NewLine ) Switch ($As) { "Notify" { $FGC = "Cyan" } "Warning" { $FGC = "Yellow" } "Error" { $FGC = "Red" } + "Debug" { $FGC = "Magenta" } Default { $FGC = "White" } } - If ($Newline) { $Output += "`n" } + If ($Newline) { $Output += "`n" } $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $Output += "[$Timestamp] $Message" - Write-Host $Output -ForegroundColor $FGC + If ($As -eq "Debug") { If ($Dev) { Write-Host $Output -ForegroundColor $FGC } } + Else { Write-Host $Output -ForegroundColor $FGC } } Function Add-ListItem () { @@ -138,75 +129,16 @@ Function Add-ListItem () { Detail on what the script does, if this is needed. #> - Param ( - [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()]$Message, + [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [String] $Message, [Parameter(Mandatory = $false)] [Int16] $Indent = 0 ) - For ($Counter = 0; $Counter -le $Indent; $Counter++) { $Output += "`t" } + For ($Counter = 0; $Counter -le $Indent; $Counter++) { $Output += " " } $Output += "* $Message" Write-Host $Output } -Function Start-Logging { - <# - .SYNOPSIS - A brief description of the function or script. - - .DESCRIPTION - A longer description. - - .PARAMETER FirstParameter - Description of each of the parameters. - Note: - To make it easier to keep the comments synchronized with changes to the parameters, - the preferred location for parameter documentation comments is not here, - but within the param block, directly above each parameter. - - .PARAMETER SecondParameter - Description of each of the parameters. - - .INPUTS - Description of objects that can be piped to the script. - - .OUTPUTS - Description of objects that are output by the script. - - .EXAMPLE - Example of how to run the script. - - .LINK - Links to further documentation. - - .NOTES - Detail on what the script does, if this is needed. - - #> - - Param ( - [Parameter(Mandatory)][String]$ScriptName, - [String]$LogDir, - [Switch]$User, - [Switch]$Append - ) - If (Not $LogDir -eq $null) { - If ($User) { - $LogDir = "$Env:USERPROFILE\Mindfang\$ScriptName" - } - Else { - $LogDir = "$Env:ProgramData\Mindfang\$ScriptName" - } - } - - If ($Append) { - Add-LogEntry (Start-Transcript -Path "$LogDir\$ScriptName.log" -Append) - } - Else { - Add-LogEntry (Start-Transcript -Path "$LogDir\$ScriptName.log") - } -} - Function Import-JSONConfig () { <# .SYNOPSIS @@ -242,21 +174,180 @@ Function Import-JSONConfig () { #> Param ( - [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $ConfigFile + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigFile ) If (Test-Path -Path $ConfigFile) { Try { Return (Get-Content -Raw -Path $ConfigFile | ConvertFrom-Json) } Catch { - Add-LogEntry "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error - Write-Host $_ + Write-Output "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error + Write-Output $_ Exit 1 } } Else { - Add-LogEntry "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error - Write-Host $_ + Write-Output "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error + Write-Output $_ Exit 1 } } + +Function Import-YAMLConfig () { + <# + .SYNOPSIS + A brief description of the function or script. + + .DESCRIPTION + A longer description. + + .PARAMETER FirstParameter + Description of each of the parameters. + Note: + To make it easier to keep the comments synchronized with changes to the parameters, + the preferred location for parameter documentation comments is not here, + but within the param block, directly above each parameter. + + .PARAMETER SecondParameter + Description of each of the parameters. + + .INPUTS + Description of objects that can be piped to the script. + + .OUTPUTS + Description of objects that are output by the script. + + .EXAMPLE + Example of how to run the script. + + .LINK + Links to further documentation. + + .NOTES + Detail on what the script does, if this is needed. + + #> + Param ( + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigFile + ) + If (Test-CommandExists -Command "ConvertFrom-Yaml") { + If (Test-Path -Path $ConfigFile) { + Try { + Return (Get-Content -Raw -Path $ConfigFile | ConvertFrom-Json) + } + Catch { + Write-Output "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error + Write-Output $_ + Exit 1 + } + } + Else { + Write-Output "Error loading config file! Please make sure the correct path was provided, and that it is a properly formatted JSON file" -As Error + Write-Output $_ + Exit 1 + } + } + Else { + Write-Output "Cannot import YAML config, module `"powershell-yaml`" is not installed" + Exit 1 + } + +} + +Function Set-RegistryKey { + Param( + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $TestPath, + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $TestKey, + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $TestValue, + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $TestType + ) + If (-Not (Test-Path -Path $TestPath)) { + Write-Output "Registry path not found, creating..." + New-Item $TestPath -Force -ErrorAction SilentlyContinue | Out-Null + } + If ($null -ne (Get-Item -Path $TestPath).GetValue($TestKey)) { + If ((Get-ItemPropertyValue -Path $TestPath -Name $TestKey) -eq $TestValue) { + Write-Output "Current key value is `"$((Get-Item -Path $TestPath).GetValue($TestKey))`", no action needed" + } + Else { + Write-Output "Setting value of `"$($TestKey)`" to `"$($TestValue)`"" + Set-ItemProperty -Path $TestPath -Name $TestKey -Value $TestValue + } + } + Else { + Write-Output "Registry key does not exist, creating..." + New-ItemProperty -Path $TestPath -Name $TestKey -Value $TestValue -PropertyType $TestType -Force -ErrorAction SilentlyContinue | Out-Null + } +} + +Function Test-AsAdmin () { + <# + .SYNOPSIS + A brief description of the function or script. + + .DESCRIPTION + A longer description. + + .PARAMETER FirstParameter + Description of each of the parameters. + Note: + To make it easier to keep the comments synchronized with changes to the parameters, + the preferred location for parameter documentation comments is not here, + but within the param block, directly above each parameter. + + .PARAMETER SecondParameter + Description of each of the parameters. + + .INPUTS + Description of objects that can be piped to the script. + + .OUTPUTS + Description of objects that are output by the script. + + .EXAMPLE + Example of how to run the script. + + .LINK + Links to further documentation. + + .NOTES + Detail on what the script does, if this is needed. + + #> + Param ( + [Parameter(Mandatory = $false)] [String] $AdditionalFlags + ) + Write-Output "Checking to see if script has been run with administrative privileges" + if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + Write-Output "Performing script elevation, please approve the admin prompt" + Start-Sleep -Seconds 3 + if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) { + $CommandLine = "$AdditionalFlags -File `"" + $MyInvocation.MyCommand.Path + "`" " + (Save-Parameters $MyInvocation.BoundParameters) + " " + $MyInvocation.UnboundArguments + Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine + Exit + } + } + Else { + Write-Output "Script already running as admin" + } +} + +# Origin: https://lucyllewy.com/powershell-clickable-hyperlinks/ +Function Format-Hyperlink { + Param( + [Parameter(ValueFromPipeline = $true, Position = 0)][ValidateNotNullOrEmpty()] [Uri] $Uri, + [Parameter(Mandatory = $false, Position = 1)] [String] $Label + ) + + If (($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) -and -not $Env:WT_SESSION) { + # Fallback for Windows users not inside Windows Terminal + If ($Label) { + Return "$Label ($Uri)" + } + Return "$Uri" + } + If ($Label) { + Return "`e]8;;$Uri`e\$Label`e]8;;`e\" + } + Return "$Uri" +} \ No newline at end of file