├── .gitattributes ├── Invoke-CommandAs.bootstrap.ps1 ├── Invoke-CommandAs.build.ps1 ├── Invoke-CommandAs ├── Invoke-CommandAs.psd1 ├── Invoke-CommandAs.psm1 ├── Private │ └── Invoke-ScheduledTask.ps1 └── Public │ └── Invoke-CommandAs.ps1 ├── LICENSE.md ├── README.md ├── Tests ├── Invoke-CommandAs.Tests.ps1 └── PSScriptAnalyzer.Tests.ps1 └── azure-pipelines.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Invoke-CommandAs.bootstrap.ps1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.PowerShell.Commands 2 | [CmdletBinding()] 3 | param( 4 | # 5 | [ValidateSet("CurrentUser", "AllUsers")] 6 | $Scope = "CurrentUser" 7 | ) 8 | 9 | [ModuleSpecification[]]$RequiredModules = @( 10 | @{ ModuleName = "InvokeBuild"; RequiredVersion = "5.4.2" } 11 | @{ ModuleName = "Pester"; RequiredVersion = "4.4.4" } 12 | @{ ModuleName = "BuildHelpers"; RequiredVersion = "2.0.3" } 13 | @{ ModuleName = "PSScriptAnalyzer"; RequiredVersion = "1.17.1" } 14 | ) 15 | 16 | $Policy = (Get-PSRepository PSGallery).InstallationPolicy 17 | Set-PSRepository PSGallery -InstallationPolicy Trusted 18 | 19 | try { 20 | $RequiredModules | Install-Module -Scope $Scope -Repository PSGallery -SkipPublisherCheck -Verbose 21 | } finally { 22 | Set-PSRepository PSGallery -InstallationPolicy $Policy 23 | } 24 | 25 | $RequiredModules | Import-Module -------------------------------------------------------------------------------- /Invoke-CommandAs.build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Build script (https://github.com/nightroman/Invoke-Build) 4 | #> 5 | 6 | param ($Configuration = 'Development') 7 | 8 | #region Set-BuildEnvironment 9 | Try { Set-BuildEnvironment -ErrorAction SilentlyContinue -Force } Catch { } 10 | #endregion 11 | 12 | #region use the most strict mode 13 | Set-StrictMode -Version Latest 14 | #endregion 15 | 16 | #region Task to run all Pester tests in folder .\tests 17 | task Test { 18 | 19 | $OutputPath = New-Item -Path '.\TestResults' -ItemType Directory -Force -Verbose 20 | 21 | $PesterParams = @{ 22 | Script = '.\Tests' 23 | OutputFile = "${OutputPath}\TestResults.xml" 24 | CodeCoverage = 'Invoke-CommandAs\*\*.ps1' 25 | CodeCoverageOutputFile = "${OutputPath}\CodeCoverage.xml" 26 | CodeCoverageOutputFileFormat = 'JaCoCo' 27 | } 28 | 29 | $Result = Invoke-Pester @PesterParams -PassThru 30 | 31 | if ($Result.FailedCount -gt 0) { 32 | throw 'Pester tests failed' 33 | } 34 | 35 | } 36 | #endregion 37 | 38 | #region Task to create a single Script file 39 | task CreateScriptFile { 40 | 41 | $ScriptFile = New-Item "${env:BHProjectPath}\Scripts\Invoke-CommandAs.ps1" -ItemType File -Force 42 | 43 | $ModuleManifest = Test-ModuleManifest -Path $env:BHPSModuleManifest 44 | [System.Version]$ManifestVersion = $ModuleManifest.Version 45 | 46 | '#####################################################################' | Set-Content -Path $ScriptFile 47 | '# Name : {0}' -f $ModuleManifest.Name | Add-Content -Path $ScriptFile 48 | '# Version : {0}' -f $ManifestVersion | Add-Content -Path $ScriptFile 49 | '# Description : {0}' -f $ModuleManifest.Description | Add-Content -Path $ScriptFile 50 | '# ProjectUri : {0}' -f $ModuleManifest.ProjectUri | Add-Content -Path $ScriptFile 51 | '# Author : {0}' -f $ModuleManifest.Author | Add-Content -Path $ScriptFile 52 | '#####################################################################' | Add-Content -Path $ScriptFile 53 | '' | Add-Content -Path $ScriptFile 54 | 55 | Get-ChildItem "${env:BHModulePath}\*\*.ps1" -Recurse | Get-Content | Add-Content -Path $ScriptFile 56 | 57 | } 58 | #endregion 59 | 60 | #region Task to update the Module Manifest file with info from the Changelog in Readme. 61 | task UpdateManifest { 62 | # Import PlatyPS. Needed for parsing README for Change Log versions 63 | #Import-Module -Name PlatyPS 64 | 65 | $ModuleManifest = Test-ModuleManifest -Path $env:BHPSModuleManifest 66 | [System.Version]$ManifestVersion = $ModuleManifest.Version 67 | Write-Output -InputObject ('Manifest Version : {0}' -f $ManifestVersion) 68 | 69 | Try { 70 | $PSGalleryModule = Find-Module -Name $env:BHProjectName -Repository PSGallery 71 | [System.Version]$PSGalleryVersion = $PSGalleryModule.Version 72 | } Catch { 73 | [System.Version]$PSGalleryVersion = '0.0.0' 74 | } 75 | Write-Output -InputObject ('PSGallery Version : {0}' -f $PSGalleryVersion) 76 | 77 | If ($PSGalleryVersion -ge $ManifestVersion) { 78 | 79 | [System.Version]$Version = New-Object -TypeName System.Version -ArgumentList ($PSGalleryVersion.Major, $PSGalleryVersion.Minor, ($PSGalleryVersion.Build + 1)) 80 | Write-Output -InputObject ('Updated Version : {0}' -f $Version) 81 | Update-ModuleManifest -ModuleVersion $Version -Path .\Invoke-CommandAs\Invoke-CommandAs.psd1 # -ReleaseNotes $ReleaseNotes 82 | 83 | } 84 | 85 | } 86 | #endregion 87 | 88 | #region Task to Publish Module to PowerShell Gallery 89 | task PublishModule -If ($Configuration -eq 'Production') { 90 | Try { 91 | 92 | # Publish to gallery with a few restrictions 93 | if( 94 | $env:BHModulePath -and 95 | $env:BHBuildSystem -ne 'Unknown' -and 96 | $env:BHBranchName -eq "master" -and 97 | $env:BHCommitMessage -match '!publish' 98 | ) 99 | { 100 | 101 | # Build a splat containing the required details and make sure to Stop for errors which will trigger the catch 102 | $params = @{ 103 | Path = "${env:BHModulePath}" 104 | NuGetApiKey = "${env:NUGETAPIKEY}" 105 | ErrorAction = 'Stop' 106 | } 107 | Publish-Module @params 108 | Write-Output -InputObject ('Invoke-CommandAs PowerShell Module version published to the PowerShell Gallery') 109 | 110 | } 111 | else 112 | { 113 | "Skipping deployment: To deploy, ensure that...`n" + 114 | "`t* You are in a known build system (Current: $ENV:BHBuildSystem)`n" + 115 | "`t* You are committing to the master branch (Current: $ENV:BHBranchName) `n" + 116 | "`t* Your commit message includes !publish (Current: $ENV:BHCommitMessage)" | 117 | Write-Host 118 | } 119 | 120 | } 121 | Catch { 122 | throw $_ 123 | } 124 | } 125 | #endregion 126 | 127 | #region Default Task. Runs Test, UpdateManifest, PublishModule Tasks 128 | task . Test, UpdateManifest, PublishModule 129 | #endregion 130 | -------------------------------------------------------------------------------- /Invoke-CommandAs/Invoke-CommandAs.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Invoke-CommandAs' 3 | # 4 | # Generated by: Marc R Kellerman 5 | # mkellerman@outlook.com 6 | # @mkellerman 7 | # 8 | # Generated on: 01/01/2019 9 | # 10 | 11 | @{ 12 | 13 | # Script module or binary module file associated with this manifest. 14 | RootModule = 'Invoke-CommandAs.psm1' 15 | 16 | # Version number of this module. 17 | ModuleVersion = '3.1.7' 18 | 19 | # Supported PSEditions 20 | # CompatiblePSEditions = @() 21 | 22 | # ID used to uniquely identify this module 23 | GUID = '9b7281cf-c80f-44bb-96c0-ed1137056164' 24 | 25 | # Author of this module 26 | Author = 'Marc R Kellerman' 27 | 28 | # Company or vendor of this module 29 | CompanyName = 'Marc R Kellerman' 30 | 31 | # Copyright statement for this module 32 | Copyright = '(c) 2019 MKellerman. All rights reserved.' 33 | 34 | # Description of the functionality provided by this module 35 | Description = 'Invoke Command as System/User on Local/Remote computer using ScheduleTask.' 36 | 37 | # Minimum version of the Windows PowerShell engine required by this module 38 | PowerShellVersion = '3.0' 39 | 40 | # Name of the Windows PowerShell host required by this module 41 | # PowerShellHostName = '' 42 | 43 | # Minimum version of the Windows PowerShell host required by this module 44 | # PowerShellHostVersion = '' 45 | 46 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 47 | # DotNetFrameworkVersion = '' 48 | 49 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 50 | # CLRVersion = '' 51 | 52 | # Processor architecture (None, X86, Amd64) required by this module 53 | # ProcessorArchitecture = '' 54 | 55 | # Modules that must be imported into the global environment prior to importing this module 56 | # RequiredModules = @() 57 | 58 | # Assemblies that must be loaded prior to importing this module 59 | # RequiredAssemblies = @() 60 | 61 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 62 | # ScriptsToProcess = @() 63 | 64 | # Type files (.ps1xml) to be loaded when importing this module 65 | # TypesToProcess = @() 66 | 67 | # Format files (.ps1xml) to be loaded when importing this module 68 | # FormatsToProcess = @() 69 | 70 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 71 | # NestedModules = @() 72 | 73 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 74 | FunctionsToExport = @('Invoke-CommandAs') 75 | 76 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 77 | CmdletsToExport = @() 78 | 79 | # Variables to export from this module 80 | VariablesToExport = '*' 81 | 82 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 83 | AliasesToExport = @() 84 | 85 | # DSC resources to export from this module 86 | # DscResourcesToExport = @() 87 | 88 | # List of all modules packaged with this module 89 | # ModuleList = @() 90 | 91 | # List of all files packaged with this module 92 | # FileList = @() 93 | 94 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 95 | PrivateData = @{ 96 | 97 | PSData = @{ 98 | 99 | # Tags applied to this module. These help with module discovery in online galleries. 100 | Tags = @('PSRemoting','PSExec') 101 | 102 | # A URL to the license for this module. 103 | # LicenseUri = '' 104 | 105 | # A URL to the main website for this project. 106 | ProjectUri = 'https://github.com/mkellerman/Invoke-CommandAs' 107 | 108 | # A URL to an icon representing this module. 109 | # IconUri = '' 110 | 111 | # ReleaseNotes of this module 112 | # ReleaseNotes = '' 113 | 114 | } # End of PSData hashtable 115 | 116 | } # End of PrivateData hashtable 117 | 118 | # HelpInfo URI of this module 119 | # HelpInfoURI = '' 120 | 121 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 122 | # DefaultCommandPrefix = '' 123 | 124 | } 125 | 126 | -------------------------------------------------------------------------------- /Invoke-CommandAs/Invoke-CommandAs.psm1: -------------------------------------------------------------------------------- 1 | 2 | #Get public and private function definition files. 3 | $Public = @( Get-ChildItem -Path $PSScriptRoot\public\*.ps1 -Recurse -ErrorAction SilentlyContinue ) 4 | $Private = @( Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -ErrorAction SilentlyContinue ) 5 | 6 | #Dot source the files. 7 | Foreach ($import in @($Public + $Private)) { 8 | Try { 9 | . $import.fullname 10 | } 11 | Catch { 12 | Write-Error -Message "Failed to import function $($import.fullname): $_" 13 | } 14 | } 15 | 16 | Export-ModuleMember -Function $Public.Basename 17 | -------------------------------------------------------------------------------- /Invoke-CommandAs/Private/Invoke-ScheduledTask.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ScheduledTask { 2 | 3 | #Requires -Version 3.0 4 | 5 | [cmdletbinding()] 6 | Param( 7 | [Parameter(Mandatory = $true)][ScriptBlock]$ScriptBlock, 8 | [Parameter(Mandatory = $false)][Object[]]$ArgumentList, 9 | [Parameter(Mandatory = $false)][PSCredential][System.Management.Automation.CredentialAttribute()]$AsUser, 10 | [Parameter(Mandatory = $false)][Switch]$AsSystem, 11 | [Parameter(Mandatory = $false)][String]$AsInteractive, 12 | [Parameter(Mandatory = $false)][String]$AsGMSA, 13 | [Parameter(Mandatory = $false)][Switch]$RunElevated 14 | 15 | ) 16 | 17 | Process { 18 | 19 | $JobName = [guid]::NewGuid().Guid 20 | Write-Verbose "$(Get-Date): ScheduledJob: Name: ${JobName}" 21 | 22 | $UseScheduledTask = If (Get-Command 'Register-ScheduledTask' -ErrorAction SilentlyContinue) { $True } Else { $False } 23 | 24 | Try { 25 | 26 | $JobParameters = @{ } 27 | $JobParameters['Name'] = $JobName 28 | If ($RunElevated.IsPresent) { 29 | $JobParameters['ScheduledJobOption'] = New-ScheduledJobOption -RunElevated -StartIfOnBattery -ContinueIfGoingOnBattery 30 | } 31 | Else { 32 | $JobParameters['ScheduledJobOption'] = New-ScheduledJobOption -StartIfOnBattery -ContinueIfGoingOnBattery 33 | } 34 | 35 | $JobArgumentList = @{ } 36 | If ($ScriptBlock) { $JobArgumentList['ScriptBlock'] = $ScriptBlock } 37 | If ($ArgumentList) { $JobArgumentList['ArgumentList'] = $ArgumentList } 38 | 39 | # Little bit of inception to get $Using variables to work. 40 | # Collect $Using:variables, Rename and set new variables inside the job. 41 | 42 | # Inspired by Boe Prox, and his https://github.com/proxb/PoshRSJob module 43 | # and by Warren Framem and his https://github.com/RamblingCookieMonster/Invoke-Parallel module 44 | 45 | $JobArgumentList['Using'] = @() 46 | $UsingVariables = $ScriptBlock.ast.FindAll({$args[0] -is [System.Management.Automation.Language.UsingExpressionAst]},$True) 47 | If ($UsingVariables) { 48 | 49 | $ScriptText = $ScriptBlock.Ast.Extent.Text 50 | $ScriptOffSet = $ScriptBlock.Ast.Extent.StartOffset 51 | ForEach ($SubExpression in ($UsingVariables.SubExpression | Sort-Object { $_.Extent.StartOffset } -Descending)) { 52 | 53 | $Name = '__using_{0}' -f (([Guid]::NewGuid().guid) -Replace '-') 54 | $Expression = $SubExpression.Extent.Text.Replace('$Using:','$').Replace('${Using:','${'); 55 | $Value = [System.Management.Automation.PSSerializer]::Serialize((Invoke-Expression $Expression)) 56 | $JobArgumentList['Using'] += [PSCustomObject]@{ Name = $Name; Value = $Value } 57 | $ScriptText = $ScriptText.Substring(0, ($SubExpression.Extent.StartOffSet - $ScriptOffSet)) + "`${Using:$Name}" + $ScriptText.Substring(($SubExpression.Extent.EndOffset - $ScriptOffSet)) 58 | 59 | } 60 | $JobArgumentList['ScriptBlock'] = [ScriptBlock]::Create($ScriptText.TrimStart("{").TrimEnd("}")) 61 | } 62 | 63 | $JobScriptBlock = [ScriptBlock]::Create(@" 64 | 65 | Param(`$Parameters) 66 | 67 | `$JobParameters = @{} 68 | If (`$Parameters.ScriptBlock) { `$JobParameters['ScriptBlock'] = [ScriptBlock]::Create(`$Parameters.ScriptBlock) } 69 | If (`$Parameters.ArgumentList) { `$JobParameters['ArgumentList'] = `$Parameters.ArgumentList } 70 | 71 | If (`$Parameters.Using) { 72 | `$Parameters.Using | % { Set-Variable -Name `$_.Name -Value ([System.Management.Automation.PSSerializer]::Deserialize(`$_.Value)) } 73 | Start-Job @JobParameters | Receive-Job -Wait -AutoRemoveJob 74 | } Else { 75 | Invoke-Command @JobParameters 76 | } 77 | "@) 78 | 79 | Write-Verbose "$(Get-Date): ScheduledJob: Register" 80 | $ScheduledJob = Register-ScheduledJob @JobParameters -ScriptBlock $JobScriptBlock -ArgumentList $JobArgumentList -ErrorAction Stop 81 | 82 | If ($AsSystem -or $AsInteractive -or $AsUser -or $AsGMSA) { 83 | 84 | # Use ScheduledTask to execute the ScheduledJob to execute with the desired credentials. 85 | 86 | If ($UseScheduledTask) { 87 | 88 | # For Windows 8 / Server 2012 and Newer 89 | 90 | Write-Verbose "$(Get-Date): ScheduledTask: Register" 91 | $TaskParameters = @{ TaskName = $ScheduledJob.Name } 92 | $TaskParameters['Action'] = New-ScheduledTaskAction -Execute $ScheduledJob.PSExecutionPath -Argument $ScheduledJob.PSExecutionArgs 93 | $TaskParameters['Settings'] = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries 94 | If ($AsSystem) { 95 | $TaskParameters['Principal'] = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest 96 | } ElseIf ($AsGMSA) { 97 | $TaskParameters['Principal'] = New-ScheduledTaskPrincipal -UserID $AsGMSA -LogonType Password -RunLevel Highest 98 | } ElseIf ($AsInteractive) { 99 | $TaskParameters['Principal'] = New-ScheduledTaskPrincipal -UserID $AsInteractive -LogonType Interactive -RunLevel Highest 100 | } ElseIf ($AsUser) { 101 | $TaskParameters['User'] = $AsUser.GetNetworkCredential().UserName 102 | $TaskParameters['Password'] = $AsUser.GetNetworkCredential().Password 103 | } 104 | If ($RunElevated.IsPresent) { 105 | $TaskParameters['RunLevel'] = 'Highest' 106 | } 107 | 108 | $ScheduledTask = Register-ScheduledTask @TaskParameters -ErrorAction Stop 109 | 110 | Write-Verbose "$(Get-Date): ScheduledTask: Start" 111 | $CimJob = $ScheduledTask | Start-ScheduledTask -AsJob -ErrorAction Stop 112 | $CimJob | Wait-Job | Remove-Job -Force -Confirm:$False 113 | 114 | Write-Verbose "$(Get-Date): ScheduledTask: Wait" 115 | While (($ScheduledTaskInfo = $ScheduledTask | Get-ScheduledTaskInfo).LastTaskResult -eq 267009) { Start-Sleep -Milliseconds 200 } 116 | 117 | } Else { 118 | 119 | # For Windows 7 / Server 2008 R2 120 | 121 | Write-Verbose "$(Get-Date): ScheduleService: Register" 122 | $ScheduleService = New-Object -ComObject("Schedule.Service") 123 | $ScheduleService.Connect() 124 | $ScheduleTaskFolder = $ScheduleService.GetFolder("\") 125 | $TaskDefinition = $ScheduleService.NewTask(0) 126 | $TaskDefinition.Principal.RunLevel = $RunElevated.IsPresent 127 | $TaskAction = $TaskDefinition.Actions.Create(0) 128 | $TaskAction.Path = $ScheduledJob.PSExecutionPath 129 | $TaskAction.Arguments = $ScheduledJob.PSExecutionArgs 130 | 131 | If ($AsUser) { 132 | $Username = $AsUser.GetNetworkCredential().UserName 133 | $Password = $AsUser.GetNetworkCredential().Password 134 | $LogonType = 1 135 | } ElseIf ($AsInteractive) { 136 | $Username = $AsInteractive 137 | $Password = $null 138 | $LogonType = 3 139 | $TaskDefinition.Principal.RunLevel = 1 140 | } ElseIf ($AsSystem) { 141 | $Username = "System" 142 | $Password = $null 143 | $LogonType = 5 144 | } ElseIf ($AsGMSA) { 145 | # Needs to be tested 146 | $Username = $AsGMSA 147 | $Password = $null 148 | $LogonType = 5 149 | } 150 | 151 | 152 | $RegisteredTask = $ScheduleTaskFolder.RegisterTaskDefinition($ScheduledJob.Name,$TaskDefinition,6,$Username,$Password,$LogonType) 153 | 154 | Write-Verbose "$(Get-Date): ScheduleService: Start" 155 | $ScheduledTask = $RegisteredTask.Run($null) 156 | 157 | Write-Verbose "$(Get-Date): ScheduleService: Wait: Start" 158 | Do { $ScheduledTaskInfo = $ScheduleTaskFolder.GetTasks(1) | Where-Object Name -eq $ScheduledTask.Name; Start-Sleep -Milliseconds 100 } 159 | While ($ScheduledTaskInfo.State -eq 3 -and $ScheduledTaskInfo.LastTaskResult -eq 267045) 160 | 161 | Write-Verbose "$(Get-Date): ScheduleService: Wait: End" 162 | Do { $ScheduledTaskInfo = $ScheduleTaskFolder.GetTasks(1) | Where-Object Name -eq $ScheduledTask.Name; Start-Sleep -Milliseconds 100 } 163 | While ($ScheduledTaskInfo.State -eq 4) 164 | 165 | } 166 | 167 | If ($ScheduledTaskInfo.LastRunTime.Year -ne (Get-Date).Year) { 168 | Write-Error 'Task was unable to be executed.' 169 | Return 170 | } 171 | 172 | } Else { 173 | 174 | # It no other credentials where provided, execute the ScheduledJob as is. 175 | Write-Verbose "$(Get-Date): ScheduledTask: Start" 176 | $ScheduledJob.StartJob() | Out-Null 177 | 178 | } 179 | 180 | Write-Verbose "$(Get-Date): ScheduledJob: Get" 181 | $Job = Get-Job -Name $ScheduledJob.Name -ErrorAction SilentlyContinue 182 | 183 | Write-Verbose "$(Get-Date): ScheduledJob: Receive" 184 | If ($Job) { $Job | Wait-Job | Receive-Job -Wait -AutoRemoveJob } 185 | 186 | } Catch { 187 | 188 | Write-Verbose "$(Get-Date): TryCatch: Error" 189 | Write-Error $_ 190 | 191 | } Finally { 192 | 193 | Write-Verbose "$(Get-Date): ScheduledJob: Unregister" 194 | If ($ScheduledJob) { Get-ScheduledJob -Id $ScheduledJob.Id -ErrorAction SilentlyContinue | Unregister-ScheduledJob -Force -Confirm:$False | Out-Null } 195 | 196 | Write-Verbose "$(Get-Date): ScheduledTask: Unregister" 197 | If ($ScheduledTask) { 198 | If ($UseScheduledTask) { 199 | $ScheduledTask | Get-ScheduledTask -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$False | Out-Null 200 | } Else { 201 | $ScheduleTaskFolder.DeleteTask($ScheduledTask.Name, 0) | Out-Null 202 | } 203 | } 204 | 205 | } 206 | 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /Invoke-CommandAs/Public/Invoke-CommandAs.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-CommandAs { 2 | 3 | #Requires -Version 3.0 4 | 5 | <# 6 | 7 | .SYNOPSIS 8 | 9 | Invoke Command as System/User on Local/Remote computer using ScheduleTask. 10 | 11 | .DESCRIPTION 12 | 13 | Invoke Command as System/User on Local/Remote computer using ScheduleTask. 14 | ScheduledJob will be executed with current user credentials if no -As or -AsSystem is provided. 15 | 16 | Using ScheduledJob as they are ran in the background and the output can be retreived by any other process. 17 | Using ScheduledTask to Run the ScheduledJob, since you can allow Tasks to run as System or provide any credentials. 18 | 19 | Because the ScheduledJob is executed by the Task Scheduler, it is invoked locally as a seperate process and not from within the current Powershell Session. 20 | Resolving the Double Hop limitations by Powershell Remote Sessions. 21 | 22 | By Marc R Kellerman (@mkellerman) 23 | 24 | .PARAMETER AsSystem 25 | 26 | ScheduledJob will be executed using 'NT AUTHORITY\SYSTEM'. 27 | 28 | .PARAMETER AsInteractive 29 | 30 | ScheduledJob will be executed using another users Interactive session. 31 | 32 | .PARAMETER AsGMSA 33 | 34 | ScheduledJob will be executed as the specified GMSA. For Example, 'domain\gmsa$' 35 | 36 | .PARAMETER AsUser 37 | 38 | ScheduledJob will be executed using this user. Specifies a user account that has permission to perform this action. The default is the current user. 39 | 40 | Type a user name, such as User01 or Domain01\User01. Or, enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, this cmdlet prompts you for a password. 41 | 42 | #> 43 | 44 | #Requires -Version 3 45 | 46 | #Parameters generated using ProxyCommand on v5.1 47 | #[System.Management.Automation.ProxyCommand]::Create((gcm Invoke-Command)) 48 | 49 | [CmdletBinding(DefaultParameterSetName='InProcess', HelpUri='http://go.microsoft.com/fwlink/?LinkID=135225', RemotingCapability='OwnedByCommand')] 50 | param( 51 | [Parameter(ParameterSetName='Session', Position=0)] 52 | [Parameter(ParameterSetName='FilePathRunspace', Position=0)] 53 | [ValidateNotNullOrEmpty()] 54 | [System.Management.Automation.Runspaces.PSSession[]] 55 | ${Session}, 56 | 57 | [Parameter(ParameterSetName='FilePathComputerName', Position=0)] 58 | [Parameter(ParameterSetName='ComputerName', Position=0)] 59 | [Alias('Cn')] 60 | [ValidateNotNullOrEmpty()] 61 | [string[]] 62 | ${ComputerName}, 63 | 64 | [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)] 65 | [Parameter(ParameterSetName='Uri', ValueFromPipelineByPropertyName=$true)] 66 | [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)] 67 | [Parameter(ParameterSetName='FilePathUri', ValueFromPipelineByPropertyName=$true)] 68 | [Parameter(ParameterSetName='VMId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 69 | [Parameter(ParameterSetName='VMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 70 | [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 71 | [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 72 | [pscredential] 73 | [System.Management.Automation.CredentialAttribute()] 74 | ${Credential}, 75 | 76 | [Parameter(ParameterSetName='ComputerName')] 77 | [Parameter(ParameterSetName='FilePathComputerName')] 78 | [ValidateRange(1, 65535)] 79 | [int] 80 | ${Port}, 81 | 82 | [Parameter(ParameterSetName='ComputerName')] 83 | [Parameter(ParameterSetName='FilePathComputerName')] 84 | [switch] 85 | ${UseSSL}, 86 | 87 | [Parameter(ParameterSetName='Uri', ValueFromPipelineByPropertyName=$true)] 88 | [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)] 89 | [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)] 90 | [Parameter(ParameterSetName='FilePathUri', ValueFromPipelineByPropertyName=$true)] 91 | [Parameter(ParameterSetName='ContainerId', ValueFromPipelineByPropertyName=$true)] 92 | [Parameter(ParameterSetName='VMId', ValueFromPipelineByPropertyName=$true)] 93 | [Parameter(ParameterSetName='VMName', ValueFromPipelineByPropertyName=$true)] 94 | [Parameter(ParameterSetName='FilePathContainerId', ValueFromPipelineByPropertyName=$true)] 95 | [Parameter(ParameterSetName='FilePathVMId', ValueFromPipelineByPropertyName=$true)] 96 | [Parameter(ParameterSetName='FilePathVMName', ValueFromPipelineByPropertyName=$true)] 97 | [string] 98 | ${ConfigurationName}, 99 | 100 | [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)] 101 | [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)] 102 | [string] 103 | ${ApplicationName}, 104 | 105 | [Parameter(ParameterSetName='FilePathRunspace')] 106 | [Parameter(ParameterSetName='Session')] 107 | [Parameter(ParameterSetName='Uri')] 108 | [Parameter(ParameterSetName='FilePathComputerName')] 109 | [Parameter(ParameterSetName='ComputerName')] 110 | [Parameter(ParameterSetName='FilePathUri')] 111 | [Parameter(ParameterSetName='VMId')] 112 | [Parameter(ParameterSetName='VMName')] 113 | [Parameter(ParameterSetName='ContainerId')] 114 | [Parameter(ParameterSetName='FilePathVMId')] 115 | [Parameter(ParameterSetName='FilePathVMName')] 116 | [Parameter(ParameterSetName='FilePathContainerId')] 117 | [int] 118 | ${ThrottleLimit}, 119 | 120 | [Parameter(ParameterSetName='Uri', Position=0)] 121 | [Parameter(ParameterSetName='FilePathUri', Position=0)] 122 | [Alias('URI','CU')] 123 | [ValidateNotNullOrEmpty()] 124 | [uri[]] 125 | ${ConnectionUri}, 126 | 127 | [Parameter(ParameterSetName='FilePathRunspace')] 128 | [Parameter(ParameterSetName='Session')] 129 | [Parameter(ParameterSetName='Uri')] 130 | [Parameter(ParameterSetName='FilePathComputerName')] 131 | [Parameter(ParameterSetName='ComputerName')] 132 | [Parameter(ParameterSetName='FilePathUri')] 133 | [Parameter(ParameterSetName='VMId')] 134 | [Parameter(ParameterSetName='VMName')] 135 | [Parameter(ParameterSetName='ContainerId')] 136 | [Parameter(ParameterSetName='FilePathVMId')] 137 | [Parameter(ParameterSetName='FilePathVMName')] 138 | [Parameter(ParameterSetName='FilePathContainerId')] 139 | [switch] 140 | ${AsJob}, 141 | 142 | [Parameter(ParameterSetName='FilePathUri')] 143 | [Parameter(ParameterSetName='FilePathComputerName')] 144 | [Parameter(ParameterSetName='Uri')] 145 | [Parameter(ParameterSetName='ComputerName')] 146 | [Alias('Disconnected')] 147 | [switch] 148 | ${InDisconnectedSession}, 149 | 150 | [Parameter(ParameterSetName='ComputerName')] 151 | [Parameter(ParameterSetName='FilePathComputerName')] 152 | [ValidateNotNullOrEmpty()] 153 | [string[]] 154 | ${SessionName}, 155 | 156 | [Parameter(ParameterSetName='VMId')] 157 | [Parameter(ParameterSetName='Session')] 158 | [Parameter(ParameterSetName='Uri')] 159 | [Parameter(ParameterSetName='FilePathComputerName')] 160 | [Parameter(ParameterSetName='FilePathRunspace')] 161 | [Parameter(ParameterSetName='FilePathUri')] 162 | [Parameter(ParameterSetName='ComputerName')] 163 | [Parameter(ParameterSetName='VMName')] 164 | [Parameter(ParameterSetName='ContainerId')] 165 | [Parameter(ParameterSetName='FilePathVMId')] 166 | [Parameter(ParameterSetName='FilePathVMName')] 167 | [Parameter(ParameterSetName='FilePathContainerId')] 168 | [Alias('HCN')] 169 | [switch] 170 | ${HideComputerName}, 171 | 172 | [Parameter(ParameterSetName='ComputerName')] 173 | [Parameter(ParameterSetName='Session')] 174 | [Parameter(ParameterSetName='Uri')] 175 | [Parameter(ParameterSetName='FilePathComputerName')] 176 | [Parameter(ParameterSetName='FilePathRunspace')] 177 | [Parameter(ParameterSetName='FilePathUri')] 178 | [Parameter(ParameterSetName='ContainerId')] 179 | [Parameter(ParameterSetName='FilePathContainerId')] 180 | [string] 181 | ${JobName}, 182 | 183 | [Parameter(ParameterSetName='VMId', Mandatory=$true, Position=1)] 184 | [Parameter(ParameterSetName='Session', Mandatory=$true, Position=1)] 185 | [Parameter(ParameterSetName='Uri', Mandatory=$true, Position=1)] 186 | [Parameter(ParameterSetName='InProcess', Mandatory=$true, Position=0)] 187 | [Parameter(ParameterSetName='ComputerName', Mandatory=$true, Position=1)] 188 | [Parameter(ParameterSetName='VMName', Mandatory=$true, Position=1)] 189 | [Parameter(ParameterSetName='ContainerId', Mandatory=$true, Position=1)] 190 | [Alias('Command')] 191 | [ValidateNotNull()] 192 | [scriptblock] 193 | ${ScriptBlock}, 194 | 195 | [Parameter(ParameterSetName='InProcess')] 196 | [switch] 197 | ${NoNewScope}, 198 | 199 | [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, Position=1)] 200 | [Parameter(ParameterSetName='FilePathRunspace', Mandatory=$true, Position=1)] 201 | [Parameter(ParameterSetName='FilePathUri', Mandatory=$true, Position=1)] 202 | [Parameter(ParameterSetName='FilePathComputerName', Mandatory=$true, Position=1)] 203 | [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, Position=1)] 204 | [Parameter(ParameterSetName='FilePathContainerId', Mandatory=$true, Position=1)] 205 | [Alias('PSPath')] 206 | [ValidateNotNull()] 207 | [string] 208 | ${FilePath}, 209 | 210 | [Parameter(ParameterSetName='Uri')] 211 | [Parameter(ParameterSetName='FilePathUri')] 212 | [switch] 213 | ${AllowRedirection}, 214 | 215 | [Parameter(ParameterSetName='ComputerName')] 216 | [Parameter(ParameterSetName='Uri')] 217 | [Parameter(ParameterSetName='FilePathComputerName')] 218 | [Parameter(ParameterSetName='FilePathUri')] 219 | [System.Management.Automation.Remoting.PSSessionOption] 220 | ${SessionOption}, 221 | 222 | [Parameter(ParameterSetName='FilePathComputerName')] 223 | [Parameter(ParameterSetName='ComputerName')] 224 | [Parameter(ParameterSetName='Uri')] 225 | [Parameter(ParameterSetName='FilePathUri')] 226 | [System.Management.Automation.Runspaces.AuthenticationMechanism] 227 | ${Authentication}, 228 | 229 | [Parameter(ParameterSetName='FilePathComputerName')] 230 | [Parameter(ParameterSetName='ComputerName')] 231 | [Parameter(ParameterSetName='Uri')] 232 | [Parameter(ParameterSetName='FilePathUri')] 233 | [switch] 234 | ${EnableNetworkAccess}, 235 | 236 | [Parameter(ParameterSetName='ContainerId')] 237 | [Parameter(ParameterSetName='FilePathContainerId')] 238 | [switch] 239 | ${RunAsAdministrator}, 240 | 241 | [Parameter(ValueFromPipeline=$true)] 242 | [psobject] 243 | ${InputObject}, 244 | 245 | [Alias('Args')] 246 | [System.Object[]] 247 | ${ArgumentList}, 248 | 249 | [Parameter(ParameterSetName='VMId', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] 250 | [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] 251 | [Alias('VMGuid')] 252 | [ValidateNotNullOrEmpty()] 253 | [guid[]] 254 | ${VMId}, 255 | 256 | [Parameter(ParameterSetName='VMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 257 | [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 258 | [ValidateNotNullOrEmpty()] 259 | [string[]] 260 | ${VMName}, 261 | 262 | [Parameter(ParameterSetName='ContainerId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 263 | [Parameter(ParameterSetName='FilePathContainerId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 264 | [ValidateNotNullOrEmpty()] 265 | [string[]] 266 | ${ContainerId}, 267 | 268 | [Parameter(ParameterSetName='ComputerName')] 269 | [Parameter(ParameterSetName='Uri')] 270 | [string] 271 | ${CertificateThumbprint}, 272 | 273 | [Parameter(Mandatory = $false)] 274 | [Alias("System")] 275 | [switch] 276 | ${AsSystem}, 277 | 278 | [Parameter(Mandatory = $false)] 279 | [Alias("Interactive")] 280 | [string] 281 | ${AsInteractive}, 282 | 283 | [Parameter(Mandatory = $false)] 284 | [Alias("GMSA")] 285 | [string] 286 | ${AsGMSA}, 287 | 288 | [Parameter(Mandatory = $false)] 289 | [Alias("User")] 290 | [pscredential] 291 | [System.Management.Automation.CredentialAttribute()] 292 | ${AsUser}, 293 | 294 | [Parameter(Mandatory = $false)] 295 | [Alias("Elevated")] 296 | [switch] 297 | ${RunElevated} 298 | 299 | ) 300 | 301 | Process { 302 | 303 | $IsVerbose = $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent 304 | 305 | # Collect all the parameters, and prepare them to be splatted to the Invoke-Command 306 | [hashtable]$CommandParameters = $PSBoundParameters 307 | $ParameterNames = @('AsSystem', 'AsInteractive', 'AsUser', 'AsGMSA', 'RunElevated', 'FilePath','ScriptBlock', 'ArgumentList') 308 | ForEach ($ParameterName in $ParameterNames) { 309 | $CommandParameters.Remove($ParameterName) 310 | } 311 | 312 | If ($FilePath) { 313 | $ScriptContent = Get-Content -Path $FilePath -Raw 314 | $ScriptBlock = [ScriptBlock]::Create($ScriptContent) 315 | } 316 | 317 | If ($AsUser -or $AsSystem -or $AsGMSA -or $AsInteractive -or $RunElevated) { 318 | 319 | If ($ComputerName -or $Session) { 320 | 321 | Write-Verbose "$(Get-Date): [CommandAs]: Invoke-Command -> Invoke-ScheduledTask" 322 | 323 | # Collect the functions to bring with us in the remote session: 324 | $_Function = ${Function:Invoke-ScheduledTask}.Ast.Extent.Text 325 | 326 | # Collect the $Using variables to load in the remote session: 327 | $_Using = @() 328 | $UsingVariables = $ScriptBlock.ast.FindAll({$args[0] -is [System.Management.Automation.Language.UsingExpressionAst]},$True) 329 | If ($UsingVariables) { 330 | 331 | $ScriptText = $ScriptBlock.Ast.Extent.Text 332 | $ScriptOffSet = $ScriptBlock.Ast.Extent.StartOffset 333 | ForEach ($SubExpression in ($UsingVariables.SubExpression | Sort-Object { $_.Extent.StartOffset } -Descending)) { 334 | 335 | $Name = '__using_{0}' -f (([Guid]::NewGuid().guid) -Replace '-') 336 | $Expression = $SubExpression.Extent.Text.Replace('$Using:','$').Replace('${Using:','${'); 337 | $Value = [System.Management.Automation.PSSerializer]::Serialize((Invoke-Expression $Expression)) 338 | $_Using += [PSCustomObject]@{ Name = $Name; Value = $Value } 339 | $ScriptText = $ScriptText.Substring(0, ($SubExpression.Extent.StartOffSet - $ScriptOffSet)) + "`${Using:$Name}" + $ScriptText.Substring(($SubExpression.Extent.EndOffset - $ScriptOffSet)) 340 | 341 | } 342 | $ScriptBlock = [ScriptBlock]::Create($ScriptText.TrimStart("{").TrimEnd("}")) 343 | } 344 | 345 | Invoke-Command @CommandParameters -ScriptBlock { 346 | 347 | If ($PSVersionTable.PSVersion.Major -lt 3) { 348 | 349 | $ErrorMsg = "The function 'Invoke-ScheduledTask' cannot be run because it contained a '#requires' " + ` 350 | "statement for PowerShell 3.0. The version of PowerShell that is required by the " + ` 351 | "module does not match the remotly running version of PowerShell $($PSVersionTable.PSVersion.ToString())." 352 | Throw $ErrorMsg 353 | Return 354 | 355 | } 356 | 357 | # Create the functions/variables we packed up with us previously: 358 | $Using:_Function | ForEach-Object { Invoke-Expression $_ } 359 | $Using:_Using | ForEach-Object { Set-Variable -Name $_.Name -Value ([System.Management.Automation.PSSerializer]::Deserialize($_.Value)) } 360 | 361 | $Parameters = @{} 362 | If ($Using:ScriptBlock) { $Parameters['ScriptBlock'] = [ScriptBlock]::Create($Using:ScriptBlock) } 363 | If ($Using:ArgumentList) { $Parameters['ArgumentList'] = $Using:ArgumentList } 364 | If ($Using:AsUser) { $Parameters['AsUser'] = $Using:AsUser } 365 | If ($Using:AsSystem) { $Parameters['AsSystem'] = $Using:AsSystem.IsPresent } 366 | If ($Using:AsInteractive) { $Parameters['AsInteractive'] = $Using:AsInteractive } 367 | If ($Using:AsGMSA) { $Parameters['AsGMSA'] = $Using:AsGMSA } 368 | If ($Using:RunElevated) { $Parameters['RunElevated'] = $Using:RunElevated } 369 | If ($Using:IsVerbose) { $Parameters['Verbose'] = $Using:IsVerbose } 370 | 371 | Invoke-ScheduledTask @Parameters 372 | 373 | } 374 | 375 | } Else { 376 | 377 | Write-Verbose "$(Get-Date): [CommandAs]: Invoke-ScheduledTask" 378 | 379 | If ($PSVersionTable.PSVersion.Major -lt 3) { 380 | 381 | $ErrorMsg = "The function 'Invoke-ScheduledTask' cannot be run because it contained a '#requires' " + ` 382 | "statement for PowerShell 3.0. The version of PowerShell that is required by the " + ` 383 | "module does not match the currently running version of PowerShell $($PSVersionTable.PSVersion.ToString())." 384 | Throw $ErrorMsg 385 | Return 386 | 387 | } 388 | 389 | $Parameters = @{} 390 | If ($ScriptBlock) { $Parameters['ScriptBlock'] = $ScriptBlock } 391 | If ($ArgumentList) { $Parameters['ArgumentList'] = $ArgumentList } 392 | If ($AsUser) { $Parameters['AsUser'] = $AsUser } 393 | If ($AsSystem) { $Parameters['AsSystem'] = $AsSystem.IsPresent } 394 | If ($AsInteractive) { $Parameters['AsInteractive'] = $AsInteractive } 395 | If ($AsGMSA) { $Parameters['AsGMSA'] = $AsGMSA } 396 | If ($RunElevated) { $Parameters['RunElevated'] = $RunElevated.IsPresent } 397 | If ($IsVerbose) { $Parameters['Verbose'] = $IsVerbose } 398 | 399 | Invoke-ScheduledTask @Parameters 400 | 401 | } 402 | 403 | } Else { 404 | 405 | Write-Verbose "$(Get-Date): [CommandAs]: Invoke-Command" 406 | 407 | If ($ScriptBlock) { $CommandParameters['ScriptBlock'] = $ScriptBlock } 408 | If ($ArgumentList) { $CommandParameters['ArgumentList'] = $ArgumentList } 409 | If ($FilePath) { $CommandParameters['FilePath'] = $FilePath } 410 | 411 | Invoke-Command @CommandParameters 412 | 413 | } 414 | 415 | } 416 | 417 | } 418 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marc R Kellerman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PSGallery Version](https://img.shields.io/powershellgallery/v/Invoke-CommandAs.svg?style=for-the-badge&label=PowerShell%20Gallery)](https://www.powershellgallery.com/packages/Invoke-CommandAs/) 2 | [![PSGallery Downloads](https://img.shields.io/powershellgallery/dt/Invoke-CommandAs.svg?style=for-the-badge&label=Downloads)](https://www.powershellgallery.com/packages/Invoke-CommandAs/) 3 | 4 | [![Azure Pipeline](https://img.shields.io/azure-devops/build/mkellerman/Invoke-CommandAs/8.svg?style=for-the-badge&label=Azure%20Pipeline)](https://dev.azure.com/mkellerman/Invoke-CommandAs/_build?definitionId=8) 5 | 6 | # Invoke-CommandAs 7 | 8 | ``` 9 | .SYNOPSIS 10 | 11 | Invoke Command as System/User on Local/Remote computer using ScheduleTask. 12 | 13 | .DESCRIPTION 14 | 15 | Invoke Command as System/User on Local/Remote computer using ScheduleTask. 16 | ScheduledJob will be executed with current user credentials if no -AsUser or -AsSystem is provided. 17 | 18 | Using ScheduledJob as they are ran in the background and the output can be retreived by any other process. 19 | Using ScheduledTask to Run the ScheduledJob, since you can allow Tasks to run as System or provide any credentials. 20 | 21 | Because the ScheduledJob is executed by the Task Scheduler, it is invoked locally as a seperate process and not from within the current Powershell Session. 22 | Resolving the Double Hop limitations by Powershell Remote Sessions. 23 | 24 | ``` 25 | ## Examples 26 | 27 | ```powershell 28 | # Execute Locally. 29 | Invoke-CommandAs -ScriptBlock { Get-Process } 30 | 31 | # Execute As System. 32 | Invoke-CommandAs -ScriptBlock { Get-Process } -AsSystem 33 | 34 | # Execute As a GMSA. 35 | Invoke-CommandAs -ScriptBlock { Get-Process } -AsGMSA 'domain\gmsa$' 36 | 37 | # Execute As Credential of another user. 38 | Invoke-CommandAs -ScriptBlock { Get-Process } -AsUser $Credential 39 | 40 | # Execute As Interactive session of another user. 41 | Invoke-CommandAs -ScriptBlock { Get-Process } -AsInteractive 'username' 42 | 43 | ``` 44 | ### You can execute all the same commands as above against a remote machine. 45 | ### Use -ComputerName/Credential or -Session to authenticate 46 | ```powershell 47 | # Execute Remotely using ComputerName/Credential. 48 | Invoke-CommandAs -ComputerName 'VM01' -Credential $Credential -ScriptBlock { Get-Process } 49 | 50 | # Execute Remotely using Session. 51 | Invoke-CommandAs -Session $PSSession -ScriptBlock { Get-Process } 52 | 53 | # Execute Remotely using PSSession and execute ScriptBlock as SYSTEM. 54 | Invoke-CommandAs -Session $PSSession -ScriptBlock { Get-Process } -AsSystem 55 | 56 | # Execute Remotely on multiple Computers at the same time. 57 | Invoke-CommandAs -ComputerName 'VM01', 'VM02' -Credential $Credential -ScriptBlock { Get-Process } 58 | 59 | # Execute Remotely as Job. 60 | Invoke-CommandAs -Session $PSSession -ScriptBlock { Get-Process } -AsJob 61 | ``` 62 | 63 | ## How to see if it works: 64 | ```powershell 65 | $ScriptBlock = { [System.Security.Principal.Windowsidentity]::GetCurrent() } 66 | Invoke-CommandAs -ScriptBlock $ScriptBlock -AsSystem 67 | ``` 68 | 69 | ## Install Module (PSv5): 70 | ```powershell 71 | Install-Module -Name Invoke-CommandAs 72 | ``` 73 | 74 | ## Install Module (PSv4 or earlier): 75 | ``` 76 | Copy Invoke-CommandAs folder to: 77 | C:\Program Files\WindowsPowerShell\Modules\Invoke-CommandAs 78 | ``` 79 | 80 | ## Import Module directly from GitHub: 81 | ``` 82 | $WebClient = New-Object Net.WebClient 83 | $WebClient.DownloadString("https://raw.githubusercontent.com/mkellerman/Invoke-CommandAs/master/Invoke-CommandAs/Private/Invoke-ScheduledTask.ps1") | Set-Content -Path ".\Invoke-ScheduledTask.ps1" 84 | $WebClient.DownloadString("https://raw.githubusercontent.com/mkellerman/Invoke-CommandAs/master/Invoke-CommandAs/Public/Invoke-CommandAs.ps1") | Set-Content -Path ".\Invoke-CommandAs.ps1" 85 | Import-Module ".\Invoke-ScheduledTask.ps1" 86 | Import-Module ".\Invoke-CommandAs.ps1" 87 | ``` 88 | One liner (dont write to disk): 89 | ``` 90 | "Public/Invoke-CommandAs.ps1", "Private/Invoke-ScheduledTask.ps1" | % { 91 | . ([ScriptBlock]::Create((New-Object Net.WebClient).DownloadString("https://raw.githubusercontent.com/mkellerman/Invoke-CommandAs/master/Invoke-CommandAs/${_}"))) 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /Tests/Invoke-CommandAs.Tests.ps1: -------------------------------------------------------------------------------- 1 | Try { Set-BuildEnvironment -Path "${PSScriptRoot}\.." -ErrorAction SilentlyContinue -Force } Catch { } 2 | 3 | Remove-Module $ENV:BHProjectName -ErrorAction SilentlyContinue -Force -Confirm:$False 4 | $Script:Module = Import-Module $ENV:BHPSModuleManifest -Force -PassThru 5 | 6 | Describe 'Get-Module -Name Invoke-CommandAs' { 7 | Context 'Strict mode' { 8 | 9 | Set-StrictMode -Version Latest 10 | 11 | It 'Should Import' { 12 | $Script:Module.Name | Should be $ENV:BHProjectName 13 | } 14 | It 'Should have ExportedFunctions' { 15 | $Script:Module.ExportedFunctions.Keys -contains 'Invoke-CommandAs' | Should be $True 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PSScriptAnalyzer.Tests.ps1: -------------------------------------------------------------------------------- 1 | Try { Set-BuildEnvironment -Path "${PSScriptRoot}\.." -ErrorAction Stop -Force } 2 | Catch { Set-BuildEnvironment -Path ".\.." -ErrorAction Stop -Force -Force } 3 | 4 | Remove-Module ${env:BHProjectName} -ErrorAction SilentlyContinue -Force -Confirm:$False 5 | $Script:Module = Import-Module ${env:BHPSModuleManifest} -Force -PassThru 6 | 7 | Describe "General project validation" { 8 | 9 | Context 'Basic Module Testing' { 10 | # Original idea from: https://kevinmarquette.github.io/2017-01-21-powershell-module-continious-delivery-pipeline/ 11 | [array]$scripts = Get-ChildItem ${env:BHModulePath} -Include *.ps1, *.psm1, *.psd1 -Recurse 12 | [array]$testCase = $scripts | Foreach-Object { 13 | @{ 14 | FilePath = $_.fullname 15 | FileName = $_.Name 16 | 17 | } 18 | } 19 | It "Script should be valid powershell" -TestCases $testCase { 20 | param( 21 | $FilePath, 22 | $FileName 23 | ) 24 | 25 | $FilePath | Should Exist 26 | 27 | $contents = Get-Content -Path $FilePath -ErrorAction Stop 28 | $errors = $null 29 | $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) 30 | $errors.Count | Should Be 0 31 | } 32 | 33 | It "Module '${env:BHProjectName}' can import cleanly" { 34 | { $Script:Module = Import-Module ${env:BHPSModuleManifest} -Force -PassThru } | Should Not Throw 35 | } 36 | } 37 | 38 | Context 'Manifest Testing' { 39 | It 'Valid Module Manifest' { 40 | { 41 | $Script:Manifest = Test-ModuleManifest -Path ${env:BHPSModuleManifest} -ErrorAction Stop -WarningAction SilentlyContinue 42 | } | Should Not Throw 43 | } 44 | It 'Valid Manifest Name' { 45 | $Script:Manifest.Name | Should be ${env:BHProjectName} 46 | } 47 | It 'Generic Version Check' { 48 | $Script:Manifest.Version -as [Version] | Should Not BeNullOrEmpty 49 | } 50 | It 'Valid Manifest Description' { 51 | $Script:Manifest.Description | Should Not BeNullOrEmpty 52 | } 53 | It 'Valid Manifest Root Module' { 54 | $Script:Manifest.RootModule | Should Be "${env:BHProjectName}.psm1" 55 | } 56 | It 'Valid Manifest GUID' { 57 | $Script:Manifest.Guid | Should be '9b7281cf-c80f-44bb-96c0-ed1137056164' 58 | } 59 | It 'No Format File' { 60 | $Script:Manifest.ExportedFormatFiles | Should BeNullOrEmpty 61 | } 62 | 63 | It 'Required Modules' { 64 | $Script:Manifest.RequiredModules | Should BeNullOrEmpty 65 | } 66 | } 67 | 68 | Context 'Exported Functions' { 69 | 70 | [array]$ManifestFunctions = $Script:Manifest.ExportedFunctions.Keys 71 | [array]$ExportedFunctions = $Script:Module.ExportedFunctions.Keys 72 | [array]$ExpectedFunctions = (Get-ChildItem -Path "${env:BHModulePath}\public" -Filter *.ps1 -Recurse | Select-Object -ExpandProperty Name ) -replace '\.ps1$' 73 | [array]$CommandFunctions = Get-Command -Module $Script:Module.Name -CommandType Function | Select-Object -ExpandProperty Name 74 | 75 | [array]$testCase = $ExpectedFunctions | Foreach-Object {@{FunctionName = $_}} 76 | It "Function should be in manifest" -TestCases $testCase -Skip { 77 | param($FunctionName) 78 | $FunctionName -in $ManifestFunctions | Should Be $true 79 | } 80 | 81 | It "Function should be exported" -TestCases $testCase { 82 | param($FunctionName) 83 | $FunctionName -in $ExportedFunctions | Should Be $true 84 | $FunctionName -in $CommandFunctions | Should Be $true 85 | } 86 | 87 | It 'Number of Functions Exported compared to Manifest' -Skip { 88 | $CommandFunctions.Count | Should be $ManifestFunctions.Count 89 | } 90 | 91 | It 'Number of Functions Exported compared to Files' { 92 | $CommandFunctions.Count | Should be $ExpectedFunctions.Count 93 | } 94 | 95 | $InternalFunctions = (Get-ChildItem -Path "${env:BHModulePath}\private" -Filter *.ps1 | Select-Object -ExpandProperty Name ) -replace '\.ps1$' 96 | $testCase = $InternalFunctions | Foreach-Object {@{FunctionName = $_}} 97 | It "Internal function is not directly accessible outside the module" -TestCases $testCase { 98 | param($FunctionName) 99 | { . $FunctionName } | Should Throw 100 | } 101 | } 102 | 103 | Context 'Exported Aliases' { 104 | It 'Proper Number of Aliases Exported compared to Manifest' { 105 | $ExportedCount = Get-Command -Module ${env:BHProjectName} -CommandType Alias | Measure-Object | Select-Object -ExpandProperty Count 106 | $ManifestCount = $Manifest.ExportedAliases.Count 107 | 108 | $ExportedCount | Should be $ManifestCount 109 | } 110 | 111 | It 'Proper Number of Aliases Exported compared to Files' { 112 | $AliasCount = Get-ChildItem -Path "${env:BHModulePath}\public" -Filter *.ps1 | Select-String "New-Alias" | Measure-Object | Select-Object -ExpandProperty Count 113 | $ManifestCount = $Manifest.ExportedAliases.Count 114 | 115 | $AliasCount | Should be $ManifestCount 116 | } 117 | } 118 | } 119 | 120 | Describe "ScriptAnalyzer" -Tag 'Compliance' { 121 | 122 | # Get a list of all internal and Exported functions 123 | $ItemFiles = @() 124 | $ItemFiles += Get-ChildItem -Path "${env:BHModulePath}\private" -Filter *.ps1 -Recurse 125 | $ItemFiles += Get-ChildItem -Path "${env:BHModulePath}\public" -Filter *.ps1 -Recurse 126 | 127 | $PSScriptAnalyzerSettings = @{ 128 | Severity = @('Error', 'Warning') 129 | ExcludeRule = @('PSUseSingularNouns', 'PSUseShouldProcessForStateChangingFunctions', 'PSAvoidUsingInvokeExpression' ) 130 | } 131 | 132 | Context "Strict mode" { 133 | 134 | Set-StrictMode -Version Latest 135 | ForEach ($ItemFile in $ItemFiles) { 136 | 137 | $ScriptAnalyzerResults = Invoke-ScriptAnalyzer -Path $ItemFile.FullName @PSScriptAnalyzerSettings 138 | $TestResults = $ScriptAnalyzerResults | Foreach-Object { 139 | @{ 140 | RuleName = $_.RuleName 141 | ScriptName = $_.ScriptName 142 | Message = $_.Message 143 | Severity = $_.Severity 144 | Line = $_.Line 145 | } 146 | } 147 | If ($TestResults) { 148 | It "Function should not use on line " -TestCases $TestResults { 149 | param( 150 | $RuleName, 151 | $ScriptName, 152 | $Message, 153 | $Severity, 154 | $Line 155 | ) 156 | $ScriptName | Should BeNullOrEmpty 157 | } 158 | } Else { 159 | It "Function $($ItemFile.Name) should return no errors" { 160 | $TestResults | Should BeNullOrEmpty 161 | } 162 | } 163 | 164 | } 165 | 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | name: $(Build.DefinitionName)_$(Date:yyyyMMdd) 2 | 3 | resources: 4 | - repo: self 5 | 6 | queue: 7 | name: Hosted VS2017 8 | 9 | steps: 10 | - powershell: './Invoke-CommandAs.bootstrap.ps1' 11 | displayName: 'Invoke Bootstrap' 12 | 13 | - powershell: 'Invoke-Build -Configuration Production -Task Test' 14 | displayName: 'Invoke Pester Tests' 15 | 16 | - task: PublishTestResults@2 17 | displayName: 'Publish Test Results' 18 | inputs: 19 | testResultsFormat: NUnit 20 | testResultsFiles: 'TestResults-*.xml' 21 | searchFolder: '$(System.DefaultWorkingDirectory)/TestResults' 22 | 23 | - task: PublishCodeCoverageResults@1 24 | displayName: 'Publish Code Coverage' 25 | inputs: 26 | summaryFileLocation: '$(System.DefaultWorkingDirectory)/TestResults/CodeCoverage-*.xml' 27 | reportDirectory: '$(System.DefaultWorkingDirectory)/TestResults' 28 | 29 | - task: PublishBuildArtifacts@1 30 | displayName: 'Publish Artifact: Invoke-CommandAs' 31 | inputs: 32 | PathtoPublish: '$(System.DefaultWorkingDirectory)/Invoke-CommandAs' 33 | ArtifactName: Invoke-CommandAs 34 | 35 | - powershell: 'Invoke-Build -Configuration Production -Task UpdateManifest, PublishModule' 36 | displayName: 'Publish PowerShell Module' --------------------------------------------------------------------------------