<# The MIT License (MIT) Copyright © 2018-2021 Swisscom (Schweiz) AG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #> #region PowerSponse #region CONTRIBUTING <# See CONSTRIBUTING.md Enable new Function 1. Add function to Export-ModuleMember at the bottom of this file 2. Add function to FunctionsToExport in the .psd1 file 3. Add new function to repository that Invoke-PowerSponse can handle it (Repository.ps1) #> #endregion Function Invoke-PowerSponse() { [CmdletBinding(SupportsShouldProcess=$True)] param( [string] $RuleFile = $(throw "you have to provide a rule"), [string[]] $ComputerName, [string] $ComputerList, [boolean] $OnlineCheck = $false, [switch] $IgnoreMissing, [switch] $PrintCommand ) $Function = $MyInvocation.MyCommand Write-Verbose "$Function Entering $Function" $ret = @() $WhatIfPassed = ($PSBoundParameters.ContainsKey('whatif') -and $PSBoundParameters['whatif'].ispresent) Write-Verbose "$Function OnlineCheck: $OnlineCheck" Write-Progress -Activity "Running $Function" -Status "Initializing..." $ParsedRule = Get-PowerSponseRule -RuleFile $RuleFile $Arguments="OnlineCheck: $OnlineCheck, RuleFile: $RuleFile" # Todo Check custom local repository defintion (overwrite defaults) $MissingActions = @() if (!$IgnoreMissing) { foreach ($Rule in $ParsedRule) { $Actions = $Rule.Action foreach ($Action in $Actions) { $ActionName = $Action.type if (!$Script:Repository.$ActionName) { $Status="fail" $MissingActions += $ActionName Write-Verbose $ActionName } } } if ($MissingActions) { $Reason="Following actions are not defined in the repository: $(($MissingActions | sort -unique)-join', '). Use -IgnoreMissing to force execution." New-PowerSponseObject -Function $Function -Status $Status -Reason $Reason -ComputerName $target -Arguments $Arguments Write-Verbose "$Function Leaving $Function" return } } # read targets $targets = Get-Target -ComputerList:$(if ($ComputerList){$ComputerList}) -ComputerName:$(if ($ComputerName){$ComputerName}) # apply each rule for each target foreach ($target in $targets) { Write-Progress -Activity "Running $Function" -Status "Check connection to $target..." if ($OnlineCheck -and !(Test-Connection $target -Quiet -Count 1)) { $Status="fail" $Reason="Offline" $ret += New-PowerSponseObject -Function $Function -Status $Status -Reason $Reason -ComputerName $target -Arguments $Arguments } else { Write-Verbose "$Function Processing target $target" foreach ($Rule in $ParsedRule) { $RuleName = $Rule.name Write-Progress -Activity "Running $Function" -Status "Processing rule $($RuleName) on $target..." Write-Verbose "$Function Processing Rule: $RuleName" $Actions = $Rule.Action foreach ($Action in $Actions) { $ActionName = $Action.type $CustomAction = $Action.action $CustomMethod = $Action.method Write-Progress -Activity "Running $Function" -Status "Processing Category $ActionName from rule $RuleName on host $target..." $val = $null if ($Script:Repository.$ActionName) { if (!$CustomAction) { $CommandAction = "$($Script:Repository.$ActionName.$("Action$($Script:Repository.$ActionName.DefaultAction)"))" Write-Verbose "Rule $RuleName - Action $ActionName - Using default action: $CommandAction" } else { $AvailableActions = $Script:Repository.$ActionName.Actions if ($AvailableActions -contains $CustomAction) { $CommandAction = "$($Script:Repository.$ActionName.$("Action$CustomAction"))" Write-Verbose "Rule $RuleName - Action $ActionName - Using custom action: $CommandAction" } else { $CommandAction = "$($Script:Repository.$ActionName.$("Action$($Script:Repository.$ActionName.DefaultAction)"))" write-error "`"$CustomAction`" is not a valid action. Allowed actions: $(($AvailableActions)-join", "). Default action is used: $CommandAction" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandAction" } } if (!$CustomMethod) { $CommandMethod = "$($Script:Repository.$ActionName.DefaultMethod)" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandMethod" } else { $AvailableMethods = $Script:Repository.$ActionName.Methods if ($AvailableMethods -contains $CustomMethod) { $CommandMethod = $CustomMethod Write-Verbose "Rule $RuleName - Action $ActionName - Using custom method: $CommandMethod" } else { write-error "$CustomMethod is not a valid method. Allowed methods: $(($AvailableMethods)-join", "). Default method is used." $CommandMethod = "$($Script:Repository.$ActionName.DefaultMethod)" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandMethod" } } Write-Progress -Activity "Running $Function" -Status "Processing action `"$CommandAction`" and from category `"$ActionName`" from rule `"$RuleName`" on host `"$target`"..." $Command = "`$val = " $Command += $CommandAction $DefaultParams = @{ #OnlineCheck = $OnlineCheck ComputerName = $target } $Command += " @DefaultParams" $Command += " -Method $CommandMethod" $Command += " -WhatIf:`$$WhatIfPassed" $Params = $Script:Repository.$ActionName.Parameter foreach ($Param in $Params.Keys) { if (!$($Action.$Param)) { write-error "No value for parameter $($params.$Param) for action $ActionName in rule $RuleName" # todo fix command if no value is available - abort? $Command = "" } else { $Command += " $($Params.$Param) `"$($Action.$Param)`"" } } $Params = $Script:Repository.$ActionName.ParameterOpt foreach ($Param in $Params.Keys) { if ($Action.PSobject.Properties.name -match $Param) # is key in rule available? { if ($Action.$Param) # is value in rule available? { $Command += " $($Params.$Param) `"$($Action.$Param)`"" } else # empty value for key means switch param { $Command += " $($Params.$Param):`$true" } } } if ($PrintCommand) { write $Command } else { Invoke-Expression $Command } } $ret += $val } # foreach category within rule } # foreach rule $Status="pass" $Reason="Successfully invoked PowerSponse$(if ($PrintCommand) {" without executing the commands (PrintCommand)"})" $ret += New-PowerSponseObject -Function $Function -Status $Status -Reason $Reason -ComputerName $target -Arguments $Arguments } # host available } # foreach target $ret Write-Verbose "$Function Leaving $Function" } # Invoke-PowerSponse function Get-PowerSponseRule() { [CmdletBinding(SupportsShouldProcess=$True)] param( [string] $RuleFile = $(throw "you have to provide a rule"), [validateset("xml","json")] [string] $method = "" ) $Function = $MyInvocation.MyCommand Write-Verbose "$Function Entering $Function" if (!(Test-Path $RuleFile)) { throw "$RuleFile not found" } if (!$method) { # check extension of rule file and decide which method should be used $fileExt = [System.IO.Path]::GetExtension("$RuleFile") $method = $fileExt[1..$fileExt.Length]-join'' write-verbose "Using method $method based on file extension $fileExt" } else { write-verbose "Using method $method supplied by parameter" } # removes duplicate entries which is normal in CORE rules # NOT USABLE if ($method -match "json") { try { $Rules = (get-content "$RuleFile" -raw) | ConvertFrom-Json $Rules = $Rules.PowerSponse.Rule } catch { throw "JSON could not be parsed - check scheme and syntax: $($_.exception.message)" } } # more flexible than ConvertFrom-String # good for meta data etc. # BAD: ORDNER IS NOT MAINTAINED elseif ($method -match "xml") { try { $Rules = ([xml] (get-content $RuleFile)).PowerSponse.Rule } catch { throw "XML could not be parsed - check scheme and syntax: $($_.exception.message)" } } if (!$Rules) { write-error "Could not read PowerSponse rules - check scheme" } else { $Rules } foreach ($rule in $rules) { write-verbose "Rule $($Rule.Name)" write-verbose "------------------" write-verbose "Author: $($Rule.author)" write-verbose "Date: $($Rule.date)" write-verbose "Links: $($Rule.links)" write-verbose "------------------" foreach ($act in $Rule.action) { Write-Verbose "$($act.type)" } Write-Verbose "------------------" } Write-Verbose "$Function Leaving $Function" } #Get-PowerSponseRule Function New-CleanupPackage() { [CmdletBinding(SupportsShouldProcess=$True)] param( [Parameter(Mandatory=$true)] [string] $RuleFile = $(throw "you have to provide a rule"), [string] $ComputerName = "localhost", [string] $OutputPath = $ModuleRoot, [string] $PackageName = "Cleanup-$([guid]::NewGuid().Guid).ps1", [switch] $IgnoreMissing ) $Function = $MyInvocation.MyCommand Write-Verbose "$Function Entering $Function" Write-Progress -Activity "Running $Function" -Status "Build cleanup package..." if (!(Test-Path $OutputPath)) { write-error "$OutputPath not found" } $WhatIfPassed = ($PSBoundParameters.ContainsKey('whatif') -and $PSBoundParameters['whatif'].ispresent) # path to cleanup file $FilePath = "$OutputPath\$PackageName" $ParsedRule = Get-PowerSponseRule -RuleFile $RuleFile # Todo Check custom local repository defintion (overwrite defaults) if (!$IgnoreMissing) { foreach ($Rule in $ParsedRule) { $MissingActions = @() $Actions = $Rule.Action foreach ($Action in $Actions) { $ActionName = $Action.type if (!$Script:Repository.$ActionName) { $Status="fail" $MissingActions += $ActionName Write-Verbose $ActionName } } } if ($MissingActions) { $Reason="Following actions are not defined in the repository: $(($MissingActions | sort -unique)-join', '). Use -IgnoreMissing to force execution." New-PowerSponseObject -Function $Function -Status $Status -Reason $Reason -ComputerName $target -Arguments $Arguments Write-Verbose "$Function Leaving $Function" return } } # read all functions and write them into the cleanup file $FunctionFiles = get-childitem -Filter *.ps1 -path $ModuleRoot\functions\ | ? { ` $_.FullName -notmatch "\\test\\" -and ` $_.FullName -notmatch "\\bin\\" -and ` $_.FullName -notmatch "Template.ps1"` } $Functions = $FunctionFiles | get-content $Functions | Set-Content $FilePath # write commands for cleanup "" | Add-Content $FilePath "#####" | Add-Content $FilePath "## PowerSponse Cleanup Package for $ComputerName and rulefile $RuleFile" | Add-Content $FilePath "#####" | Add-Content $FilePath "" | Add-Content $FilePath "`$ModuleRoot = `$PSScriptRoot" | Add-Content $FilePath "" | Add-Content $FilePath "`$ret = @()" | Add-Content $FilePath "" | Add-Content $FilePath foreach ($Rule in $ParsedRule) { $RuleName = $Rule.name "## PowerSponse cleanup commands rule $RuleName" | Add-Content $FilePath $Actions = $Rule.Action foreach ($Action in $Actions) { $ActionName = $Action.type $CustomAction = $Action.action $CustomMethod = $Action.method if ($Script:Repository.$ActionName) { if (!$CustomAction) { $CommandAction = "$($Script:Repository.$ActionName.$("Action$($Script:Repository.$ActionName.DefaultAction)"))" Write-Verbose "Rule $RuleName - Action $ActionName - Using default action: $CommandAction" } else { $AvailableActions = $Script:Repository.$ActionName.Actions if ($AvailableActions -contains $CustomAction) { $CommandAction = "$($Script:Repository.$ActionName.$("Action$CustomAction"))" Write-Verbose "Rule $RuleName - Action $ActionName - Using custom action: $CommandAction" } else { $CommandAction = "$($Script:Repository.$ActionName.$("Action$($Script:Repository.$ActionName.DefaultAction)"))" write-error "`"$CustomAction`" is not a valid action. Allowed actions: $(($AvailableActions)-join", "). Default action is used: $CommandAction" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandAction" } } if (!$CustomMethod) { $CommandMethod = "$($Script:Repository.$ActionName.DefaultMethod)" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandMethod" } else { $AvailableMethods = $Script:Repository.$ActionName.Methods if ($AvailableMethods -contains $CustomMethod) { $CommandMethod = $CustomMethod Write-Verbose "Rule $RuleName - Action $ActionName - Using custom method: $CommandMethod" } else { write-error "$CustomMethod is not a valid method. Allowed methods: $(($AvailableMethods)-join", "). Default method is used." $CommandMethod = "$($Script:Repository.$ActionName.DefaultMethod)" Write-Verbose "Rule $RuleName - Action $ActionName - Using default method: $CommandMethod" } } Write-Progress -Activity "Running $Function" -Status "Processing action `"$CommandAction`" and from category `"$ActionName`" from rule `"$RuleName`" on host `"$ComputerName`"..." $Command = "`$ret += " $Command += $CommandAction $Command += " -ComputerName:$ComputerName" $Command += " -Method $CommandMethod" $Command += " -WhatIf:`$$WhatIfPassed" $Params = $Script:Repository.$ActionName.Parameter foreach ($Param in $Params.Keys) { if (!$($Action.$Param)) { write-error "No value for parameter $($params.$Param) for action $ActionName." # todo fix command if no value is available - abort? $Command = "" } else { $Command += " $($Params.$Param) `"$($Action.$Param)`"" } } $Params = $Script:Repository.$ActionName.ParameterOpt foreach ($Param in $Params.Keys) { if ($Action.PSobject.Properties.name -match $Param) # is key in rule available? { if ($Action.$Param) # is value in rule available? { $Command += " $($Params.$Param) `"$($Action.$Param)`"" } else # empty value for key means switch param { $Command += " $($Params.$Param):`$true" } } } Write-Verbose $Command $Command | Add-Content $FilePath } } # foreach category within rule "" | Add-Content $FilePath "#####" | Add-Content $FilePath } # foreach rule "" | Add-Content $FilePath "`$ret" | Add-Content $FilePath "" | Add-Content $FilePath "#####" | Add-Content $FilePath Write-host "Wrote cleanup script to $FilePath" Write-Verbose "$Function Leaving $Function" } # New-CleanupPackage function Get-PowerSponseRepository() { # Serialize and Deserialize data using BinaryFormatter $ms = New-Object System.IO.MemoryStream $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter $bf.Serialize($ms, $Script:Repository) $ms.Position = 0 $dataDeep = $bf.Deserialize($ms) $ms.Close() return $dataDeep } function Set-PowerSponseRepository() { param( [System.Collections.Hashtable] $NewRepository ) if ($NewRepository) { $Script:Repository = $NewRepository } else { write-error "New repository is empty" } } Function Import-PowerSponseRepository() { $Script:Repository = {} . "$ModuleRoot\Repository.ps1" } #region INITIALIZATION # Module path for all functions $ModuleRoot = $PSScriptRoot # load repository Import-PowerSponseRepository # Load all functions Get-ChildItem -Path "$ModuleRoot\functions\" -Exclude "Template.ps1" -Filter *.ps1 -Recurse | % { . $_.FullName} #endregion Export-ModuleMember @( 'Invoke-PowerSponse', 'New-CleanupPackage', 'Get-PowerSponseRule', 'Get-Process', 'Start-Process', 'Stop-Process', 'Start-Service', 'Stop-Service', 'Enable-Service', 'Disable-Service', 'Get-ScheduledTask', 'Enable-ScheduledTask', 'Disable-ScheduledTask', 'Stop-Computer', 'Restart-Computer', 'Get-NetworkInterface', 'Enable-NetworkInterface', 'Disable-NetworkInterface', 'Get-Autoruns', 'Enable-RemoteRegistry', 'Disable-RemoteRegistry', 'Get-PowerSponseRepository', 'Set-PowerSponseRepository', 'Import-PowerSponseRepository', 'Get-FileHandle', 'Invoke-PsExec', 'Find-File', 'Find-Directory', 'Get-Certificate' 'Remove-File', 'Remove-Directory' ) #endregion