From 08b5ce3a92646167e66e32765d49382e984347e5 Mon Sep 17 00:00:00 2001 From: Carrie Roberts Date: Wed, 19 Jun 2024 12:34:42 -0600 Subject: [PATCH] Switch Atomic Runner to use a Service instead of a Scheduled Task (#188) --- Invoke-AtomicRedTeam.psd1 | 197 +-- Invoke-AtomicRedTeam.psm1 | 2 +- Public/AtomicRunnerService.ps1 | 1131 +++++++++++++++++ Public/Invoke-AtomicRunner.ps1 | 512 ++++---- Public/Invoke-KickoffAtomicRunner.ps1 | 128 +- .../Invoke-KillProcessTree.ps1 | 0 Public/Invoke-RunnerScheduleMethods.ps1 | 14 +- Public/Invoke-SetupAtomicRunner.ps1 | 334 +++-- Public/config.ps1 | 6 +- 9 files changed, 1765 insertions(+), 559 deletions(-) create mode 100644 Public/AtomicRunnerService.ps1 rename {Private => Public}/Invoke-KillProcessTree.ps1 (100%) diff --git a/Invoke-AtomicRedTeam.psd1 b/Invoke-AtomicRedTeam.psd1 index 686c6b0..728dc69 100644 --- a/Invoke-AtomicRedTeam.psd1 +++ b/Invoke-AtomicRedTeam.psd1 @@ -1,98 +1,99 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'Invoke-AtomicRedTeam.psm1' - - # Version number of this module. - ModuleVersion = '2.1.0' - - # ID used to uniquely identify this module - GUID = '8f492621-18f8-432e-9532-b1d54d3e90bd' - - # Author of this module - Author = 'Casey Smith @subTee, Josh Rickard @MSAdministrator, Carrie Roberts @OrOneEqualsOne, Matt Graeber @mattifestation' - - # Company or vendor of this module - CompanyName = 'Red Canary, Inc.' - - # Copyright statement for this module - Copyright = '(c) 2021 Red Canary. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'A PowerShell module that runs Atomic Red Team tests from yaml definition files.' - - # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.0' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - # AtomicClassSchema.ps1 needs to be present in the caller's scope in order for the built-in classes to surface properly. - ScriptsToProcess = @('Private\AtomicClassSchema.ps1', 'Public\config.ps1') - - # 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. - FunctionsToExport = @( - 'Invoke-AtomicTest', - 'Get-AtomicTechnique', - 'New-AtomicTechnique', - 'New-AtomicTest', - 'New-AtomicTestInputArgument', - 'New-AtomicTestDependency', - 'Start-AtomicGUI', - 'Stop-AtomicGUI', - 'Invoke-SetupAtomicRunner', - 'Invoke-GenerateNewSchedule', - 'Invoke-RefreshExistingSchedule', - 'Invoke-AtomicRunner', - 'Get-Schedule', - 'Invoke-KickoffAtomicRunner', - 'Get-PreferredIPAddress' - ) - - # Variables to export from this module - VariablesToExport = '*' - - NestedModules = @( - "Public\Default-ExecutionLogger.psm1", - "Public\Attire-ExecutionLogger.psm1", - "Public\Syslog-ExecutionLogger.psm1", - "Public\WinEvent-ExecutionLogger.psm1" - ) - - # 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. - PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('Security', 'Defense') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/redcanaryco/invoke-atomicredteam/blob/master/LICENSE.txt' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/redcanaryco/invoke-atomicredteam' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - ReleaseNotes = @' -1.0.2 ------ -* Add support for custom execution loggers - -1.0.1 ------ -* Adding 'powershell-yaml' to RequiredModules in the module manifest - -1.0.0 ------ -* Initial release for submission to the PowerShell Gallery -'@ - - } # End of PSData hashtable - - } # End of PrivateData hashtable -} +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Invoke-AtomicRedTeam.psm1' + + # Version number of this module. + ModuleVersion = '2.1.0' + + # ID used to uniquely identify this module + GUID = '8f492621-18f8-432e-9532-b1d54d3e90bd' + + # Author of this module + Author = 'Casey Smith @subTee, Josh Rickard @MSAdministrator, Carrie Roberts @OrOneEqualsOne, Matt Graeber @mattifestation' + + # Company or vendor of this module + CompanyName = 'Red Canary, Inc.' + + # Copyright statement for this module + Copyright = '(c) 2021 Red Canary. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'A PowerShell module that runs Atomic Red Team tests from yaml definition files.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('powershell-yaml') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # AtomicClassSchema.ps1 needs to be present in the caller's scope in order for the built-in classes to surface properly. + ScriptsToProcess = @('Private\AtomicClassSchema.ps1', 'Public\config.ps1') + + # 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. + FunctionsToExport = @( + 'Invoke-AtomicTest', + 'Get-AtomicTechnique', + 'New-AtomicTechnique', + 'New-AtomicTest', + 'New-AtomicTestInputArgument', + 'New-AtomicTestDependency', + 'Start-AtomicGUI', + 'Stop-AtomicGUI', + 'Invoke-SetupAtomicRunner', + 'Invoke-GenerateNewSchedule', + 'Invoke-RefreshExistingSchedule', + 'Invoke-AtomicRunner', + 'Get-Schedule', + 'Invoke-KickoffAtomicRunner', + 'Get-PreferredIPAddress', + 'Invoke-KillProcessTree' + ) + + # Variables to export from this module + VariablesToExport = '*' + + NestedModules = @( + "Public\Default-ExecutionLogger.psm1", + "Public\Attire-ExecutionLogger.psm1", + "Public\Syslog-ExecutionLogger.psm1", + "Public\WinEvent-ExecutionLogger.psm1" + ) + + # 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. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Security', 'Defense') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/redcanaryco/invoke-atomicredteam/blob/master/LICENSE.txt' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/redcanaryco/invoke-atomicredteam' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = @' +1.0.2 +----- +* Add support for custom execution loggers + +1.0.1 +----- +* Adding 'powershell-yaml' to RequiredModules in the module manifest + +1.0.0 +----- +* Initial release for submission to the PowerShell Gallery +'@ + + } # End of PSData hashtable + + } # End of PrivateData hashtable +} \ No newline at end of file diff --git a/Invoke-AtomicRedTeam.psm1 b/Invoke-AtomicRedTeam.psm1 index 546874f..4c7f261 100644 --- a/Invoke-AtomicRedTeam.psm1 +++ b/Invoke-AtomicRedTeam.psm1 @@ -6,7 +6,7 @@ if([bool]$artConfig.absb -and ($artConfig.OS -eq "windows")){ } #Get public and private function definition files. -$Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -ErrorAction SilentlyContinue ) +$Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -Exclude AtomicRunnerService.ps1 -ErrorAction SilentlyContinue ) $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -Recurse -Exclude "AtomicClassSchema.ps1" -ErrorAction SilentlyContinue ) # Make sure the Atomic Class Schema is available first (a workaround so PSv5.0 doesn't give errors) diff --git a/Public/AtomicRunnerService.ps1 b/Public/AtomicRunnerService.ps1 new file mode 100644 index 0000000..9530d2a --- /dev/null +++ b/Public/AtomicRunnerService.ps1 @@ -0,0 +1,1131 @@ +# Slightly modified the PSService example below to kick-off the atomic runner +############################################################################### +# # +# File name PSService.ps1 # +# # +# Description A sample service in a standalone PowerShell script # +# # +# Notes The latest PSService.ps1 version is available in GitHub # +# repository https://github.com/JFLarvoire/SysToolsLib/ , # +# in the PowerShell subdirectory. # +# Please report any problem in the Issues tab in that # +# GitHub repository in # +# https://github.com/JFLarvoire/SysToolsLib/issues # +# If you do submit a pull request, please add a comment at # +# the end of this header with the date, your initials, and # +# a description of the changes. Also update $scriptVersion. # +# # +# The initial version of this script was described in an # +# article published in the May 2016 issue of MSDN Magazine. # +# https://msdn.microsoft.com/en-us/magazine/mt703436.aspx # +# This updated version has one major change: # +# The -Service handler in the end has been rewritten to be # +# event-driven, with a second thread waiting for control # +# messages coming in via a named pipe. # +# This allows fixing a bug of the original version, that # +# did not stop properly, and left a zombie process behind. # +# The drawback is that the new code is significantly longer,# +# due to the added PowerShell thread management routines. # +# On the other hand, these thread management routines are # +# reusable, and will allow building much more powerful # +# services. # +# # +# Dynamically generates a small PSService.exe wrapper # +# application, that in turn invokes this PowerShell script. # +# # +# Some arguments are inspired by Linux' service management # +# arguments: -Start, -Stop, -Restart, -Status # +# Others are more in the Windows' style: -Setup, -Remove # +# # +# The actual start and stop operations are done when # +# running as SYSTEM, under the control of the SCM (Service # +# Control Manager). # +# # +# To create your own service, make a copy of this file and # +# rename it. The file base name becomes the service name. # +# Then implement your own service code in the if ($Service) # +# {block} at the very end of this file. See the TO DO # +# comment there. # +# There are global settings below the script param() block. # +# They can easily be changed, but the defaults should be # +# suitable for most projects. # +# # +# Service installation and usage: See the dynamic help # +# section below, or run: help .\PSService.ps1 -Detailed # +# # +# Debugging: The Log function writes messages into a file # +# called C:\Windows\Logs\PSService.log (or actually # +# ${env:windir}\Logs\$serviceName.log). # +# It is very convenient to monitor what's written into that # +# file with a WIN32 port of the Unix tail program. Usage: # +# tail -f C:\Windows\Logs\PSService.log # +# # +# History # +# 2015-07-10 JFL jf.larvoire@hpe.com created this script. # +# 2015-10-13 JFL Made this script completely generic, and added comments # +# in the header above. # +# 2016-01-02 JFL Moved the Event Log name into new variable $logName. # +# Improved comments. # +# 2016-01-05 JFL Fixed the StartPending state reporting. # +# 2016-03-17 JFL Removed aliases. Added missing explicit argument names. # +# 2016-04-16 JFL Moved the official repository on GitHub. # +# 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.# +# 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its # +# finally block was not executed, and a zombie task often # +# remained. Fixed by using a named pipe to send messages # +# to the service task. # +# 2016-06-05 JFL Finalized the event-driven service handler. # +# Fixed the default command setting in PowerShell v2. # +# Added a sample -Control option using the new pipe. # +# 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. # +# 2016-06-09 JFL Finalized the PSThread management routines error handling.# +# This finally fixes issue #1. # +# 2016-08-22 JFL Fixed issue #3 creating the log and install directories. # +# Thanks Nischl. # +# 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in # +# a language-independent way. Thanks A Gonzalez. # +# 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.# +# Added a $ServiceDescription string global setting, and # +# use it for the service registration. # +# Added comments about Windows event logs limitations. # +# 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. # +# 2017-05-10 CJG Added execution policy bypass flag. # +# 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing # +# orphaned process left after stoping the service. # +# 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the # +# script parameters. # +# 2017-12-10 JFL Removed the unreliable service account detection tests, # +# and instead use dedicated -SCMStart and -SCMStop # +# arguments in the PSService.exe helper app. # +# Renamed variable userName as currentUserName. # +# Renamed arguments ServiceUser and ServicePassword to the # +# more standard UserName and Password. # +# Also added the standard argument -Credential. # +# # +############################################################################### +#Requires -version 2 + +<# + .SYNOPSIS + A sample Windows service, in a standalone PowerShell script. + + .DESCRIPTION + This script demonstrates how to write a Windows service in pure PowerShell. + It dynamically generates a small AtomicRunnerService.exe wrapper, that in turn + invokes this PowerShell script again for its start and stop events. + + .PARAMETER Start + Start the service. + + .PARAMETER Stop + Stop the service. + + .PARAMETER Restart + Stop then restart the service. + + .PARAMETER Status + Get the current service status: Not installed / Stopped / Running + + .PARAMETER Setup + Install the service. + Optionally use the -Credential or -UserName arguments to specify the user + account for running the service. By default, uses the LocalSystem account. + Known limitation with the old PowerShell v2: It is necessary to use -Credential + or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default. + + .PARAMETER Credential + User and password credential to use for running the service. + For use with the -Setup command. + Generate a PSCredential variable with the Get-Credential command. + + .PARAMETER UserName + User account to use for running the service. + For use with the -Setup command, in the absence of a Credential variable. + The user must have the "Log on as a service" right. To give him that right, + open the Local Security Policy management console, go to the + "\Security Settings\Local Policies\User Rights Assignments" folder, and edit + the "Log on as a service" policy there. + Services should always run using a user account which has the least amount + of privileges necessary to do its job. + Three accounts are special, and do not require a password: + * LocalSystem - The default if no user is specified. Highly privileged. + * LocalService - Very few privileges, lowest security risk. + Apparently not enough privileges for running PowerShell. Do not use. + * NetworkService - Idem, plus network access. Same problems as LocalService. + + .PARAMETER Password + Password for UserName. If not specified, you will be prompted for it. + It is strongly recommended NOT to use that argument, as that password is + visible on the console, and in the task manager list. + Instead, use the -UserName argument alone, and wait for the prompt; + or, even better, use the -Credential argument. + + .PARAMETER Remove + Uninstall the service. + + .PARAMETER Service + Run the service in the background. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMStart + Process Service Control Manager start requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMStop + Process Service Control Manager stop requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER Control + Send a control message to the service thread. + + .PARAMETER Version + Display this script version and exit. + + .EXAMPLE + # Setup the service and run it for the first time + C:\PS>.\AtomicRunnerService.ps1 -Status + Not installed + C:\PS>.\AtomicRunnerService.ps1 -Setup + C:\PS># At this stage, a copy of AtomicRunnerService.ps1 is present in the path + C:\PS>AtomicRunnerService -Status + Stopped + C:\PS>AtomicRunnerService -Start + C:\PS>AtomicRunnerService -Status + Running + C:\PS># Load the log file in Notepad.exe for review + C:\PS>notepad ${ENV:windir}\Logs\AtomicRunnerService.log + + .EXAMPLE + # Stop the service and uninstall it. + C:\PS>AtomicRunnerService -Stop + C:\PS>AtomicRunnerService -Status + Stopped + C:\PS>AtomicRunnerService -Remove + C:\PS># At this stage, no copy of AtomicRunnerService.ps1 is present in the path anymore + C:\PS>.\AtomicRunnerService.ps1 -Status + Not installed + + .EXAMPLE + # Configure the service to run as a different user + C:\PS>$cred = Get-Credential -UserName LAB\Assistant + C:\PS>.\AtomicRunnerService -Setup -Credential $cred + + .EXAMPLE + # Send a control message to the service, and verify that it received it. + C:\PS>AtomicRunnerService -Control Hello + C:\PS>Notepad C:\Windows\Logs\AtomicRunnerService.log + # The last lines should contain a trace of the reception of this Hello message +#> + +[CmdletBinding(DefaultParameterSetName='Status')] +Param( + [Parameter(ParameterSetName='Start', Mandatory=$true)] + [Switch]$Start, # Start the service + + [Parameter(ParameterSetName='Stop', Mandatory=$true)] + [Switch]$Stop, # Stop the service + + [Parameter(ParameterSetName='Restart', Mandatory=$true)] + [Switch]$Restart, # Restart the service + + [Parameter(ParameterSetName='Status', Mandatory=$false)] + [Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status + + [Parameter(ParameterSetName='Setup', Mandatory=$true)] + [Parameter(ParameterSetName='Setup2', Mandatory=$true)] + [Switch]$Setup, # Install the service + + [Parameter(ParameterSetName='Setup', Mandatory=$true)] + [String]$UserName, # Set the service to run as this user + + [Parameter(ParameterSetName='Setup', Mandatory=$false)] + [SecureString]$Password, # Use this password for the user + + [Parameter(ParameterSetName='Setup', Mandatory=$false)] + [String]$installDir= "${ENV:windir}\System32", # Where to install the service files + + [Parameter(ParameterSetName='Setup2', Mandatory=$false)] + [System.Management.Automation.PSCredential]$Credential, # Service account credential + + [Parameter(ParameterSetName='Remove', Mandatory=$true)] + [Switch]$Remove, # Uninstall the service + + [Parameter(ParameterSetName='Service', Mandatory=$true)] + [Switch]$Service, # Run the service (Internal use only) + + [Parameter(ParameterSetName='SCMStart', Mandatory=$true)] + [Switch]$SCMStart, # Process SCM Start requests (Internal use only) + + [Parameter(ParameterSetName='SCMStop', Mandatory=$true)] + [Switch]$SCMStop, # Process SCM Stop requests (Internal use only) + + [Parameter(ParameterSetName='Control', Mandatory=$true)] + [String]$Control = $null, # Control message to send to the service + + [Parameter(ParameterSetName='Version', Mandatory=$true)] + [Switch]$Version # Get this script version +) +$scriptVersion = "2017-12-10" + +# This script name, with various levels of details +$argv0 = Get-Item $MyInvocation.MyCommand.Definition +$script = $argv0.basename # Ex: AtomicRunnerService +$scriptName = $argv0.name # Ex: AtomicRunnerService.ps1 +$scriptFullName = $argv0.fullname # Ex: C:\Temp\AtomicRunnerService.ps1 + +# Global settings +$serviceName = $script # A one-word name used for net start commands +$serviceDisplayName = "AtomicRunnerService" +$ServiceDescription = "Executes the Invoke-KickOffAtomicRunner Script" +$pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task +$scriptCopy = "$installDir\$scriptName" +$exeName = "$serviceName.exe" +$exeFullName = "$installDir\$exeName" +$logDir = "${ENV:windir}\Logs" # Where to log the service messages +$logFile = "$logDir\$serviceName.log" +$logName = "Application" # Event Log name (Unrelated to the logFile!) +# Note: The current implementation only supports "classic" (ie. XP-compatble) event logs. +# To support new style (Vista and later) "Applications and Services Logs" folder trees, it would +# be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands. +# Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist: +# HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME +# Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source, +# even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist! + +# If the -Version switch is specified, display the script version and exit. +if ($Version) { + Write-Output $scriptVersion + return +} + +#-----------------------------------------------------------------------------# +# # +# Function Now # +# # +# Description Get a string with the current time. # +# # +# Notes The output string is in the ISO 8601 format, except for # +# a space instead of a T between the date and time, to # +# improve the readability. # +# # +# History # +# 2015-06-11 JFL Created this routine. # +# # +#-----------------------------------------------------------------------------# + +Function Now { + Param ( + [Switch]$ms, # Append milliseconds + [Switch]$ns # Append nanoseconds + ) + $Date = Get-Date + $now = "" + $now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day + $now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second + $nsSuffix = "" + if ($ns) { + if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") { + $now += $matches[0] + $ms = $false + } else { + $ms = $true + $nsSuffix = "000" + } + } + if ($ms) { + $now += ".{0:000}$nsSuffix" -f $Date.MilliSecond + } + return $now +} + +#-----------------------------------------------------------------------------# +# # +# Function Log # +# # +# Description Log a string into the AtomicRunnerService.log file # +# # +# Arguments A string # +# # +# Notes Prefixes the string with a timestamp and the user name. # +# (Except if the string is empty: Then output a blank line.)# +# # +# History # +# 2016-06-05 JFL Also prepend the Process ID. # +# 2016-06-08 JFL Allow outputing blank lines. # +# # +#-----------------------------------------------------------------------------# + +Function Log () { + Param( + [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] + [String]$string + ) + if (!(Test-Path $logDir)) { + New-Item -ItemType directory -Path $logDir | Out-Null + } + if ($String.length) { + $string = "$(Now) $pid $currentUserName $string" + } + $string | Out-File -Encoding ASCII -Append "$logFile" +} + +#-----------------------------------------------------------------------------# +# # +# Function Start-PSThread # +# # +# Description Start a new PowerShell thread # +# # +# Arguments See the Param() block # +# # +# Notes Returns a thread description object. # +# The completion can be tested in $_.Handle.IsCompleted # +# Alternative: Use a thread completion event. # +# # +# References # +# https://learn-powershell.net/tag/runspace/ # +# https://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/ +# http://www.codeproject.com/Tips/895840/Multi-Threaded-PowerShell-Cookbook +# # +# History # +# 2016-06-08 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +$PSThreadCount = 0 # Counter of PSThread IDs generated so far +$PSThreadList = @{} # Existing PSThreads indexed by Id + +Function Get-PSThread () { + Param( + [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] + [int[]]$Id = $PSThreadList.Keys # List of thread IDs + ) + $Id | ForEach-Object { $PSThreadList.$_ } +} + +Function Start-PSThread () { + Param( + [Parameter(Mandatory=$true, Position=0)] + [ScriptBlock]$ScriptBlock, # The script block to run in a new thread + [Parameter(Mandatory=$false)] + [String]$Name = "", # Optional thread name. Default: "PSThread$Id" + [Parameter(Mandatory=$false)] + [String]$Event = "", # Optional thread completion event name. Default: None + [Parameter(Mandatory=$false)] + [Hashtable]$Variables = @{}, # Optional variables to copy into the script context. + [Parameter(Mandatory=$false)] + [String[]]$Functions = @(), # Optional functions to copy into the script context. + [Parameter(Mandatory=$false)] + [Object[]]$Arguments = @() # Optional arguments to pass to the script. + ) + + $Id = $script:PSThreadCount + $script:PSThreadCount += 1 + if (!$Name.Length) { + $Name = "PSThread$Id" + } + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + foreach ($VarName in $Variables.Keys) { # Copy the specified variables into the script initial context + $value = $Variables.$VarName + Write-Debug "Adding variable $VarName=[$($Value.GetType())]$Value" + $var = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($VarName, $value, "") + $InitialSessionState.Variables.Add($var) + } + foreach ($FuncName in $Functions) { # Copy the specified functions into the script initial context + $Body = Get-Content function:$FuncName + Write-Debug "Adding function $FuncName () {$Body}" + $func = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($FuncName, $Body) + $InitialSessionState.Commands.Add($func) + } + $RunSpace = [RunspaceFactory]::CreateRunspace($InitialSessionState) + $RunSpace.Open() + $PSPipeline = [powershell]::Create() + $PSPipeline.Runspace = $RunSpace + $PSPipeline.AddScript($ScriptBlock) | Out-Null + $Arguments | ForEach-Object { + Write-Debug "Adding argument [$($_.GetType())]'$_'" + $PSPipeline.AddArgument($_) | Out-Null + } + $Handle = $PSPipeline.BeginInvoke() # Start executing the script + if ($Event.Length) { # Do this after BeginInvoke(), to avoid getting the start event. + Register-ObjectEvent $PSPipeline -EventName InvocationStateChanged -SourceIdentifier $Name -MessageData $Event + } + $PSThread = New-Object PSObject -Property @{ + Id = $Id + Name = $Name + Event = $Event + RunSpace = $RunSpace + PSPipeline = $PSPipeline + Handle = $Handle + } # Return the thread description variables + $script:PSThreadList[$Id] = $PSThread + $PSThread +} + +#-----------------------------------------------------------------------------# +# # +# Function Receive-PSThread # +# # +# Description Get the result of a thread, and optionally clean it up # +# # +# Arguments See the Param() block # +# # +# Notes # +# # +# History # +# 2016-06-08 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +Function Receive-PSThread () { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] + [PSObject]$PSThread, # Thread descriptor object + [Parameter(Mandatory=$false)] + [Switch]$AutoRemove # If $True, remove the PSThread object + ) + Process { + if ($PSThread.Event -and $AutoRemove) { + Unregister-Event -SourceIdentifier $PSThread.Name + Get-Event -SourceIdentifier $PSThread.Name | Remove-Event # Flush remaining events + } + try { + $PSThread.PSPipeline.EndInvoke($PSThread.Handle) # Output the thread pipeline output + } catch { + $_ # Output the thread pipeline error + } + if ($AutoRemove) { + $PSThread.RunSpace.Close() + $PSThread.PSPipeline.Dispose() + $PSThreadList.Remove($PSThread.Id) + } + } +} + +Function Remove-PSThread () { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] + [PSObject]$PSThread # Thread descriptor object + ) + Process { + $_ | Receive-PSThread -AutoRemove | Out-Null + } +} + +#-----------------------------------------------------------------------------# +# # +# Function Send-PipeMessage # +# # +# Description Send a message to a named pipe # +# # +# Arguments See the Param() block # +# # +# Notes # +# # +# History # +# 2016-05-25 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +Function Send-PipeMessage () { + Param( + [Parameter(Mandatory=$true)] + [String]$PipeName, # Named pipe name + [Parameter(Mandatory=$true)] + [String]$Message # Message string + ) + $PipeDir = [System.IO.Pipes.PipeDirection]::Out + $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous + + $pipe = $null # Named pipe stream + $sw = $null # Stream Writer + try { + $pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt) + $sw = new-object System.IO.StreamWriter($pipe) + $pipe.Connect(1000) + if (!$pipe.IsConnected) { + throw "Failed to connect client to pipe $pipeName" + } + $sw.AutoFlush = $true + $sw.WriteLine($Message) + } catch { + Log "Error sending pipe $pipeName message: $_" + } finally { + if ($sw) { + $sw.Dispose() # Release resources + $sw = $null # Force the PowerShell garbage collector to delete the .net object + } + if ($pipe) { + $pipe.Dispose() # Release resources + $pipe = $null # Force the PowerShell garbage collector to delete the .net object + } + } +} + +#-----------------------------------------------------------------------------# +# # +# Function Receive-PipeMessage # +# # +# Description Wait for a message from a named pipe # +# # +# Arguments See the Param() block # +# # +# Notes I tried keeping the pipe open between client connections, # +# but for some reason everytime the client closes his end # +# of the pipe, this closes the server end as well. # +# Any solution on how to fix this would make the code # +# more efficient. # +# # +# History # +# 2016-05-25 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +Function Receive-PipeMessage () { + Param( + [Parameter(Mandatory=$true)] + [String]$PipeName # Named pipe name + ) + $PipeDir = [System.IO.Pipes.PipeDirection]::In + $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous + $PipeMode = [System.IO.Pipes.PipeTransmissionMode]::Message + + try { + $pipe = $null # Named pipe stream + $pipe = New-Object system.IO.Pipes.NamedPipeServerStream($PipeName, $PipeDir, 1, $PipeMode, $PipeOpt) + $sr = $null # Stream Reader + $sr = new-object System.IO.StreamReader($pipe) + $pipe.WaitForConnection() + $Message = $sr.Readline() + $Message + } catch { + Log "Error receiving pipe message: $_" + } finally { + if ($sr) { + $sr.Dispose() # Release resources + $sr = $null # Force the PowerShell garbage collector to delete the .net object + } + if ($pipe) { + $pipe.Dispose() # Release resources + $pipe = $null # Force the PowerShell garbage collector to delete the .net object + } + } +} + +#-----------------------------------------------------------------------------# +# # +# Function Start-PipeHandlerThread # +# # +# Description Start a new thread waiting for control messages on a pipe # +# # +# Arguments See the Param() block # +# # +# Notes The pipe handler script uses function Receive-PipeMessage.# +# This function must be copied into the thread context. # +# # +# The other functions and variables copied into that thread # +# context are not strictly necessary, but are useful for # +# debugging possible issues. # +# # +# History # +# 2016-06-07 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +$pipeThreadName = "Control Pipe Handler" + +Function Start-PipeHandlerThread () { + Param( + [Parameter(Mandatory=$true)] + [String]$pipeName, # Named pipe name + [Parameter(Mandatory=$false)] + [String]$Event = "ControlMessage" # Event message + ) + Start-PSThread -Variables @{ # Copy variables required by function Log() into the thread context + logDir = $logDir + logFile = $logFile + currentUserName = $currentUserName + } -Functions Now, Log, Receive-PipeMessage -ScriptBlock { + Param($pipeName, $pipeThreadName) + try { + Receive-PipeMessage "$pipeName" # Blocks the thread until the next message is received from the pipe + } catch { + Log "$pipeThreadName # Error: $_" + throw $_ # Push the error back to the main thread + } + } -Name $pipeThreadName -Event $Event -Arguments $pipeName, $pipeThreadName +} + +#-----------------------------------------------------------------------------# +# # +# Function Receive-PipeHandlerThread # +# # +# Description Get what the pipe handler thread received # +# # +# Arguments See the Param() block # +# # +# Notes # +# # +# History # +# 2016-06-07 JFL Created this function # +# # +#-----------------------------------------------------------------------------# + +Function Receive-PipeHandlerThread () { + Param( + [Parameter(Mandatory=$true)] + [PSObject]$pipeThread # Thread descriptor + ) + Receive-PSThread -PSThread $pipeThread -AutoRemove +} + +#-----------------------------------------------------------------------------# +# # +# Function $source # +# # +# Description C# source of the AtomicRunnerService.exe stub # +# # +# Arguments # +# # +# Notes The lines commented with "SET STATUS" and "EVENT LOG" are # +# optional. (Or blocks between "// SET STATUS [" and # +# "// SET STATUS ]" comments.) # +# SET STATUS lines are useful only for services with a long # +# startup time. # +# EVENT LOG lines are useful for debugging the service. # +# # +# History # +# 2017-10-04 RBL Updated the OnStop() procedure adding the sections # +# try{ # +# }catch{ # +# }finally{ # +# } # +# This resolved the issue where stopping the service would # +# leave the PowerShell process -Service still running. This # +# unclosed process was an orphaned process that would # +# remain until the pid was manually killed or the computer # +# was rebooted # +# # +#-----------------------------------------------------------------------------# + +$scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.) +$source = @" + using System; + using System.ServiceProcess; + using System.Diagnostics; + using System.Runtime.InteropServices; // SET STATUS + using System.ComponentModel; // SET STATUS + + public enum ServiceType : int { // SET STATUS [ + SERVICE_WIN32_OWN_PROCESS = 0x00000010, + SERVICE_WIN32_SHARE_PROCESS = 0x00000020, + }; // SET STATUS ] + + public enum ServiceState : int { // SET STATUS [ + SERVICE_STOPPED = 0x00000001, + SERVICE_START_PENDING = 0x00000002, + SERVICE_STOP_PENDING = 0x00000003, + SERVICE_RUNNING = 0x00000004, + SERVICE_CONTINUE_PENDING = 0x00000005, + SERVICE_PAUSE_PENDING = 0x00000006, + SERVICE_PAUSED = 0x00000007, + }; // SET STATUS ] + + [StructLayout(LayoutKind.Sequential)] // SET STATUS [ + public struct ServiceStatus { + public ServiceType dwServiceType; + public ServiceState dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + }; // SET STATUS ] + + public enum Win32Error : int { // WIN32 errors that we may need to use + NO_ERROR = 0, + ERROR_APP_INIT_FAILURE = 575, + ERROR_FATAL_APP_EXIT = 713, + ERROR_SERVICE_NOT_ACTIVE = 1062, + ERROR_EXCEPTION_IN_SERVICE = 1064, + ERROR_SERVICE_SPECIFIC_ERROR = 1066, + ERROR_PROCESS_ABORTED = 1067, + }; + + public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter + private System.Diagnostics.EventLog eventLog; // EVENT LOG + private ServiceStatus serviceStatus; // SET STATUS + + public Service_$serviceName() { + ServiceName = "$serviceName"; + CanStop = true; + CanPauseAndContinue = false; + AutoLog = true; + + eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [ + if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) { + System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName"); + } + eventLog.Source = ServiceName; + eventLog.Log = "$logName"; // EVENT LOG ] + EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG + } + + [DllImport("advapi32.dll", SetLastError=true)] // SET STATUS + private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus); + + protected override void OnStart(string [] args) { + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG + // Set the service state to Start Pending. // SET STATUS [ + // Only useful if the startup time is long. Not really necessary here for a 2s startup time. + serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; + serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING; + serviceStatus.dwWin32ExitCode = 0; + serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ] + // Start a child process with another copy of this script + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the completion of the script startup code, that launches the -Service instance + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Running. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG + // Change the service state back to Stopped. // SET STATUS [ + serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; + Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code + if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... + w32ex = e.InnerException as Win32Exception; + } + if (w32ex != null) { // Report the actual WIN32 error + serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; + } else { // Make up a reasonable reason + serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); + } // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG + } + } + + protected override void OnStop() { + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG + // Start a child process with another copy of ourselves + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the PowerShell script to be fully stopped. + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Stopped. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Failed to stop $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG + // Change the service state back to Started. // SET STATUS [ + serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; + Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code + if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... + w32ex = e.InnerException as Win32Exception; + } + if (w32ex != null) { // Report the actual WIN32 error + serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; + } else { // Make up a reasonable reason + serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); + } // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG + } + } + + public static void Main() { + System.ServiceProcess.ServiceBase.Run(new Service_$serviceName()); + } + } +"@ + +#-----------------------------------------------------------------------------# +# # +# Function Main # +# # +# Description Execute the specified actions # +# # +# Arguments See the Param() block at the top of this script # +# # +# Notes # +# # +# History # +# # +#-----------------------------------------------------------------------------# + +# Identify the user name. We use that for logging. +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator" + +if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs +Log $MyInvocation.Line # The exact command line that was used to start us + +# The following commands write to the event log, but we need to make sure the AtomicRunnerService source is defined. +New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue + +# Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block +$Status = ($PSCmdlet.ParameterSetName -eq 'Status') + +if ($SCMStart) { # The SCM tells us to start the service + # Do whatever is necessary to start the service script instance + Log "$scriptName -SCMStart: Starting script '$scriptFullName' -Service" + Write-EventLog -LogName $logName -Source $serviceName -EventId 1001 -EntryType Information -Message "$scriptName -SCMStart: Starting script '$scriptFullName' -Service" + Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptFullName' -Service") + return +} + +if ($Start) { # The user tells us to start the service + Write-Verbose "Starting service $serviceName" + Write-EventLog -LogName $logName -Source $serviceName -EventId 1002 -EntryType Information -Message "$scriptName -Start: Starting service $serviceName" + try { + Start-Service $serviceName -ErrorAction Stop # Ask Service Control Manager to start it + } + catch { + $_ + Write-Host -ForegroundColor Red "Remember, the user '$($artConfig.user)' must have the ""Log on as a service"" right. To add that right, open the Local Security Policy management console, go to the ""\Security Settings\Local Policies\User Rights Assignments"" folder, and edit the ""Log on as a service"" policy there." + Write-Host -ForegroundColor Yellow "If you already have the rights set, then you probably entered the wrong password for the user. Try running the Invoke-SetupAtomicRunner script again and entering the correct password" + } + return +} + +if ($SCMStop) { # The SCM tells us to stop the service + # Do whatever is necessary to stop the service script instance + Write-EventLog -LogName $logName -Source $serviceName -EventId 1003 -EntryType Information -Message "$scriptName -SCMStop: Stopping script $scriptName -Service" + Log "$scriptName -SCMStop: Stopping script $scriptName -Service" + # Send an exit message to the service instance + Send-PipeMessage $pipeName "exit" + return +} + +if ($Stop) { # The user tells us to stop the service + Write-Verbose "Stopping service $serviceName" + Write-EventLog -LogName $logName -Source $serviceName -EventId 1004 -EntryType Information -Message "$scriptName -Stop: Stopping service $serviceName" + Stop-Service $serviceName # Ask Service Control Manager to stop it + return +} + +if ($Restart) { # Restart the service + & $scriptFullName -Stop + & $scriptFullName -Start + return +} + +if ($Status) { # Get the current service status + $spid = $null + $processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object { + $_.CommandLine -match ".*$scriptCopyCname.*-Service" + }) + foreach ($process in $processes) { # There should be just one, but be prepared for surprises. + $spid = $process.ProcessId + Write-Verbose "$serviceName Process ID = $spid" + } + # if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {} + try { + $pss = Get-Service $serviceName -ea stop # Will error-out if not installed + } catch { + "Not Installed" + return + } + $pss.Status + if (($pss.Status -eq "Running") -and (!$spid)) { # This happened during the debugging phase + Write-Error "The Service Control Manager thinks $serviceName is started, but $serviceName.ps1 -Service is not running." + exit 1 + } + return +} + +if ($Setup) { # Install the service + # Check if it's necessary + try { + $pss = Get-Service $serviceName -ea stop # Will error-out if not installed + # Check if this script is newer than the installed copy. + if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) { + Write-Verbose "Service $serviceName is already Installed, but requires upgrade" + & $scriptFullName -Remove + throw "continue" + } else { + Write-Verbose "Service $serviceName is already Installed, and up-to-date" + } + exit 0 + } catch { + # This is the normal case here. Do not throw or write any error! + Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning + # And continue with the installation. + } + if (!(Test-Path $installDir)) { + New-Item -ItemType directory -Path $installDir | Out-Null + } + # Copy the service script into the installation directory + if ($ScriptFullName -ne $scriptCopy) { + Write-Verbose "Installing $scriptCopy" + Copy-Item $ScriptFullName $scriptCopy + } + # Generate the service .EXE from the C# source embedded in this script + try { + Write-Verbose "Compiling $exeFullName" + Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false + } catch { + $msg = $_.Exception.Message + Write-error "Failed to create the $exeFullName service stub. $msg" + exit 1 + } + # Register the service + Write-Verbose "Registering service $serviceName" + if ($UserName -and !$Credential.UserName) { + $emptyPassword = New-Object -Type System.Security.SecureString + switch ($UserName) { + {"LocalService", "NetworkService" -contains $_} { + $Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword) + } + {"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} { + $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword) + } + default { + if (!$Password) { + $Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user" + } else { + $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force + $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword) + } + } + } + } + if ($Credential.UserName) { + Log "$scriptName -Setup # Configuring the service to run as $($Credential.UserName)" + $pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential + } else { + Log "$scriptName -Setup # Configuring the service to run by default as LocalSystem" + $pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic + } + + return +} + +if ($Remove) { # Uninstall the service + # Check if it's necessary + try { + $pss = Get-Service $serviceName -ea stop # Will error-out if not installed + } catch { + Write-Verbose "Already uninstalled" + return + } + Stop-Service $serviceName # Make sure it's stopped + # In the absence of a Remove-Service applet, use sc.exe instead. + Write-Verbose "Removing service $serviceName" + $msg = sc.exe delete $serviceName + if ($LastExitCode) { + Write-Error "Failed to remove the service ${serviceName}: $msg" + exit 1 + } else { + Write-Verbose $msg + } + # Remove the installed files + if (Test-Path $installDir) { + foreach ($ext in ("exe", "pdb", "ps1")) { + $file = "$installDir\$serviceName.$ext" + if (Test-Path $file) { + Write-Verbose "Deleting file $file" + Remove-Item $file + } + } + if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) { + Write-Verbose "Removing directory $installDir" + Remove-Item $installDir + } + } + Log "" # Insert one blank line to separate test sessions logs + return +} + +if ($Control) { # Send a control message to the service + Send-PipeMessage $pipeName $control +} + +if ($Service) { # Run the service + Write-EventLog -LogName $logName -Source $serviceName -EventId 1005 -EntryType Information -Message "$scriptName -Service # Beginning background job" + # Do the service background job + try { + # Start the control pipe handler thread + $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" + + ######### TO DO: Implement your own service code here. ########## + . $profile + $p1,$p2 = Invoke-KickoffAtomicRunner + Log "p1: $($p1.Id) p2: $($p2.Id)" + # Now enter the main service event loop + do { # Keep running until told to exit by the -Stop handler + $event = Wait-Event # Wait for the next incoming event + $source = $event.SourceIdentifier + $message = $event.MessageData + $eventTime = $event.TimeGenerated.TimeofDay + Write-Debug "Event at $eventTime from ${source}: $message" + $event | Remove-Event # Flush the event from the queue + switch ($message) { + "ControlMessage" { # Required. Message received by the control pipe thread + $state = $event.SourceEventArgs.InvocationStateInfo.state + Write-Debug "$script -Service # Thread $source state changed to $state" + switch ($state) { + "Completed" { + $message = Receive-PipeHandlerThread $pipeThread + Log "$scriptName -Service # Received control message: $Message" + if ($message -ne "exit") { # Start another thread waiting for control messages + $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" + } + } + "Failed" { + $error1 = Receive-PipeHandlerThread $pipeThread + Log "$scriptName -Service # $source thread failed: $error1" + Start-Sleep 1 # Avoid getting too many errors + $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" # Retry + } + } + } + default { # Should not happen + Log "$scriptName -Service # Unexpected event from ${source}: $Message" + } + } + } while ($message -ne "exit") + } catch { # An exception occurred while runnning the service + $msg = $_.Exception.Message + $line = $_.InvocationInfo.ScriptLineNumber + Log "$scriptName -Service # Error at line ${line}: $msg" + } finally { # Invoked in all cases: Exception or normally by -Stop + Invoke-KillProcessTree $p1.Id -ErrorAction Ignore + Invoke-KillProcessTree $p2.Id -ErrorAction Ignore + ############### End of the service code example. ################ + + # Terminate the control pipe handler thread + Get-PSThread | Remove-PSThread # Remove all remaining threads + # Flush all leftover events (There may be some that arrived after we exited the while event loop, but before we unregistered the events) + $events = Get-Event | Remove-Event + # Log a termination event, no matter what the cause is. + Write-EventLog -LogName $logName -Source $serviceName -EventId 1006 -EntryType Information -Message "$script -Service # Exiting" + Log "$scriptName -Service # Exiting" + } + return +} \ No newline at end of file diff --git a/Public/Invoke-AtomicRunner.ps1 b/Public/Invoke-AtomicRunner.ps1 index 40b4a67..2dd06b2 100755 --- a/Public/Invoke-AtomicRunner.ps1 +++ b/Public/Invoke-AtomicRunner.ps1 @@ -1,259 +1,253 @@ -. "$PSScriptRoot\Invoke-RunnerScheduleMethods.ps1" - -function Invoke-AtomicRunner { - [CmdletBinding( - SupportsShouldProcess = $true, - PositionalBinding = $false, - ConfirmImpact = 'Medium')] - Param( - [Parameter(Mandatory = $false)] - [switch] - $ShowDetails, - - [Parameter(Mandatory = $false)] - [switch] - $CheckPrereqs, - - [Parameter(Mandatory = $false)] - [switch] - $GetPrereqs, - - [Parameter(Mandatory = $false)] - [switch] - $Cleanup, - - [Parameter(Mandatory = $false)] - [switch] - $ShowDetailsBrief, - - [Parameter(Mandatory = $false)] - [String] - $LoggingModule, - - [Parameter(Mandatory = $false)] - $ListOfAtomics, - - [parameter(Mandatory = $false)] - [ValidateRange(0, [int]::MaxValue)] - [int] $PauseBetweenAtomics, - - [parameter(Mandatory = $false)] - [switch] $scheduledTaskCleanup, - - [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)] - $OtherArgs - ) - Begin { } - Process { - - function Get-GuidFromHostName( $basehostname ) { - $guid = [System.Net.Dns]::GetHostName() -replace $($basehostname + "-"), "" - - if (!$guid) { - LogRunnerMsg "Hostname has not been updated or could not parse out the Guid: " + $guid - return - } - - # Confirm hostname contains a guid - [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$' - - if ($guid -match $guidRegex) { return $guid } else { return "" } - } - - function Invoke-AtomicTestFromScheduleRow ($tr, $Cleanup = $false) { - $theArgs = $tr.InputArgs - if ($theArgs.GetType().Name -ne "Hashtable") { - $tr.InputArgs = ConvertFrom-StringData -StringData $theArgs - } - $sc = $tr.AtomicsFolder - #Run the Test based on if scheduleContext is 'private' or 'public' - if (($sc -eq 'public') -or ($null -eq $sc)) { - Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPublicAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder - } - elseif ($sc -eq 'private') { - Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPrivateAtomicsFolder @htvars -Cleanup:$Cleanup -supressPathToAtomicsFolder - } - if ($timeToPause -gt 0) { - Write-Host "Sleeping for $timeToPause seconds..." - Start-Sleep $timeToPause - } - elseif ($timeToPause -eq 0) { - Write-Host 'Press any key to continue...'; - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); - } - } - - function Rename-ThisComputer ($tr, $basehostname) { - $hash = $tr.auto_generated_guid - - $newHostName = "$basehostname-$hash" - $shouldRename = $true - if ( $newHostName -eq [System.Net.Dns]::GetHostName()) { $shouldRename = $false } - if ($artConfig.verbose) { LogRunnerMsg "Setting hostname to $newHostName" } - - If (Test-Path $artConfig.stopFile) { - LogRunnerMsg "exiting script because $($artConfig.stopFile) exists" - exit - } - - if ($IsLinux) { - if ($shouldRename) { Invoke-Expression $("hostnamectl set-hostname $newHostName") } - Invoke-Expression $("shutdown -r now") - } - if ($IsMacOS) { - if ($shouldRename) { - Invoke-Expression $("/usr/sbin/scutil --set HostName $newHostName") - Invoke-Expression $("/usr/sbin/scutil --set ComputerName $newHostName") - Invoke-Expression $("/usr/sbin/scutil --set LocalHostName $newHostName") - } - Invoke-Expression $("/sbin/shutdown -r now") - } - else { - if ($debug) { LogRunnerMsg "Debug: pretending to rename the computer to $newHostName"; exit } - if (-not $shouldRename) { Restart-Computer -Force } - if ($artConfig.gmsaAccount) { - $retry = $true; $count = 0 - while ($retry) { - # add retry loop to avoid this occassional error "The verification of the MSA failed with error 1355" - Invoke-Command -ComputerName '127.0.0.1' -ConfigurationName 'RenameRunnerEndpoint' -ScriptBlock { Rename-Computer -NewName $Using:newHostName -Force -Restart } - Start-Sleep 120; $count = $count + 1 - LogRunnerMsg "Retrying computer rename $count" - if ($count -gt 15) { $retry = $false } - } - } - else { - $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) - try { - Rename-Computer -NewName $newHostName -Force -DomainCredential $cred -Restart -ErrorAction stop - } - catch { - if ($artConfig.verbose) { LogRunnerMsg $_ } - try { Rename-Computer -NewName $newHostName -Force -LocalCredential $cred -Restart -ErrorAction stop } catch { if ($artConfig.verbose) { LogRunnerMsg $_ } } - } - } - Start-Sleep -seconds 30 - LogRunnerMsg "uh oh, still haven't restarted - should never get to here" - $retry = $true; $count = 0 - while ($retry) { - Restart-Computer -Force - Start-Sleep 300; $count = $count + 1 - LogRunnerMsg "Rename retry $count" - if ($count -gt 60) { $retry = $false } - } - exit - } - - } - - function Get-TimingVariable ($sched) { - $atcount = $sched.Count - if ($null -eq $atcount) { $atcount = 1 } - $scheduleTimeSpanSeconds = $artConfig.scheduleTimeSpan.TotalSeconds - $secondsForAllTestsToComplete = $scheduleTimeSpanSeconds - $sleeptime = ($secondsForAllTestsToComplete / $atcount) - 120 - $artConfig.kickOffDelay.TotalSeconds # 1 minute for restart and 1 minute delay for scheduled task and an optional kickoff delay - if ($sleeptime -lt 120) { $sleeptime = 120 } # minimum 2 minute sleep time - return $sleeptime - } - - # Convert OtherArgs to hashtable so we can pass it through to the call to Invoke-AtomicTest - $htvars = @{} - if ($OtherArgs) { - $OtherArgs | ForEach-Object { - if ($_ -match '^-') { - #New parameter - $lastvar = $_ -replace '^-' - $htvars[$lastvar] = $true - } - else { - #Value - $htvars[$lastvar] = $_ - } - } - } - if ($PSBoundParameters.ContainsKey("PauseBetweenAtomics")) { - $timeToPause = $PauseBetweenAtomics - } - else { - $timeToPause = $null - } - $htvars += [Hashtable]$PSBoundParameters - $htvars.Remove('listOfAtomics') | Out-Null - $htvars.Remove('OtherArgs') | Out-Null - $htvars.Remove('Cleanup') | Out-Null - $htvars.Remove('PauseBetweenAtomics') | Out-Null - $htvars.Remove('scheduledTaskCleanup') | Out-Null - - $schedule = Get-Schedule $listOfAtomics - # If the schedule is empty, end process - if (-not $schedule) { - LogRunnerMsg "No test guid's or enabled tests." - return - } - - # timing variables - $SleepTillCleanup = Get-TimingVariable $schedule - - # Perform cleanup, Showdetails or Prereq stuff for all scheduled items and then exit - if ($Cleanup -or $ShowDetails -or $CheckPrereqs -or $ShowDetailsBrief -or $GetPrereqs -or $listOfAtomics) { - $schedule | ForEach-Object { - Invoke-AtomicTestFromScheduleRow $_ $Cleanup - } - return - } - - # exit if file stop.txt is found - If (Test-Path $artConfig.stopFile) { - LogRunnerMsg "exiting script because $($artConfig.stopFile) does exist" - Write-Host -ForegroundColor Yellow "Exiting script because $($artConfig.stopFile) does exist."; Start-Sleep 10; - exit - } - - # Find current test to run - $guid = Get-GuidFromHostName $artConfig.basehostname - if ([string]::IsNullOrWhiteSpace($guid)) { - LogRunnerMsg "Test Guid ($guid) was null, using next item in the schedule" - } - else { - if ($artConfig.verbose) { LogRunnerMsg "Found Test: $guid specified in hostname" } - $sp = [Collections.Generic.List[Object]]$schedule - $currentIndex = $sp.FindIndex( { $args[0].auto_generated_guid -eq $guid }) - if (($null -ne $currentIndex) -and ($currentIndex -ne -1)) { - $tr = $schedule[$currentIndex] - } - - if ($null -ne $tr) { - if ($scheduledTaskCleanup) { - # Cleanup after running test - Write-Host -Fore cyan "Sleeping for $SleepTillCleanup seconds before cleaning up for $($tr.Technique) $($tr.auto_generated_guid) "; Start-Sleep -Seconds $SleepTillCleanup - Invoke-AtomicTestFromScheduleRow $tr $true - } - else { - # run the atomic test and exit - Invoke-AtomicTestFromScheduleRow $tr - Start-Sleep 3; exit - } - } - else { - LogRunnerMsg "Could not find Test: $guid in schedule. Please update schedule to run this test." - } - } - - # Load next scheduled test before renaming computer - $nextIndex += $currentIndex + 1 - if ($nextIndex -ge ($schedule.count)) { - $tr = $schedule[0] - } - else { - $tr = $schedule[$nextIndex] - } - - if ($null -eq $tr) { - LogRunnerMsg "Could not determine the next row to execute from the schedule, Starting from 1st row"; - $tr = $schedule[0] - } - - #Rename Computer and Restart - Rename-ThisComputer $tr $artConfig.basehostname - - } -} +. "$PSScriptRoot\Invoke-RunnerScheduleMethods.ps1" + +function Invoke-AtomicRunner { + [CmdletBinding( + SupportsShouldProcess = $true, + PositionalBinding = $false, + ConfirmImpact = 'Medium')] + Param( + [Parameter(Mandatory = $false)] + [switch] + $ShowDetails, + + [Parameter(Mandatory = $false)] + [switch] + $CheckPrereqs, + + [Parameter(Mandatory = $false)] + [switch] + $GetPrereqs, + + [Parameter(Mandatory = $false)] + [switch] + $Cleanup, + + [Parameter(Mandatory = $false)] + [switch] + $ShowDetailsBrief, + + [Parameter(Mandatory = $false)] + [String] + $LoggingModule, + + [Parameter(Mandatory = $false)] + $ListOfAtomics, + + [Parameter(Mandatory = $false)] + [switch] + $anyOS = $false, + + [parameter(Mandatory = $false)] + [ValidateRange(0, [int]::MaxValue)] + [int] $PauseBetweenAtomics, + + [parameter(Mandatory = $false)] + [switch] $scheduledTaskCleanup, + + [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)] + $OtherArgs + ) + Begin { } + Process { + + function Get-GuidFromHostName( $basehostname ) { + $guid = [System.Net.Dns]::GetHostName() -replace $($basehostname + "-"), "" + + if (!$guid) { + LogRunnerMsg "Hostname has not been updated or could not parse out the Guid: " + $guid + return + } + + # Confirm hostname contains a guid + [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$' + + if ($guid -match $guidRegex) { return $guid } else { return "" } + } + + function Invoke-AtomicTestFromScheduleRow ($tr) { + $theArgs = $tr.InputArgs + if ($theArgs.GetType().Name -ne "Hashtable") { + $tr.InputArgs = ConvertFrom-StringData -StringData $theArgs + } + $sc = $tr.AtomicsFolder + #Run the Test based on if scheduleContext is 'private' or 'public' + if (($sc -eq 'public') -or ($null -eq $sc)) { + Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPublicAtomicsFolder @htvars -supressPathToAtomicsFolder + } + elseif ($sc -eq 'private') { + Invoke-AtomicTest $tr.Technique -TestGuids $tr.auto_generated_guid -InputArgs $tr.InputArgs -TimeoutSeconds $tr.TimeoutSeconds -ExecutionLogPath $artConfig.execLogPath -PathToAtomicsFolder $artConfig.PathToPrivateAtomicsFolder @htvars -supressPathToAtomicsFolder + } + if ($timeToPause -gt 0) { + Write-Host "Sleeping for $timeToPause seconds..." + Start-Sleep $timeToPause + } + elseif ($timeToPause -eq 0) { + Write-Host 'Press any key to continue...'; + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); + } + } + + function Rename-ThisComputer ($tr, $basehostname) { + $hash = $tr.auto_generated_guid + + $newHostName = "$basehostname-$hash" + $shouldRename = $true + if ( $newHostName -eq [System.Net.Dns]::GetHostName()) { $shouldRename = $false } + if ($artConfig.verbose) { LogRunnerMsg "Setting hostname to $newHostName" } + + If (Test-Path $artConfig.stopFile) { + LogRunnerMsg "exiting script because $($artConfig.stopFile) exists" + exit + } + + if ($IsLinux) { + if ($shouldRename) { Invoke-Expression $("hostnamectl set-hostname $newHostName") } + Invoke-Expression $("shutdown -r now") + } + if ($IsMacOS) { + if ($shouldRename) { + Invoke-Expression $("/usr/sbin/scutil --set HostName $newHostName") + Invoke-Expression $("/usr/sbin/scutil --set ComputerName $newHostName") + Invoke-Expression $("/usr/sbin/scutil --set LocalHostName $newHostName") + } + Invoke-Expression $("/sbin/shutdown -r now") + } + else { + if ($debug) { LogRunnerMsg "Debug: pretending to rename the computer to $newHostName"; exit } + if (-not $shouldRename) { Restart-Computer -Force } + $retry = $true; $count = 0 + while ($retry) { + Rename-Computer -NewName $newHostName -Force -Restart + Start-Sleep 120; $count = $count + 1 + LogRunnerMsg "Retrying computer rename $count" + if ($count -gt 60) { $retry = $false } + } + + Start-Sleep -seconds 30 + LogRunnerMsg "uh oh, still haven't restarted - should never get to here" + $retry = $true; $count = 0 + while ($retry) { + $count = $count + 1 + LogRunnerMsg "Rename retry $count" + Restart-Computer -Force + Start-Sleep 300; + if ($count -gt 60) { $retry = $false } + } + exit + } + + } + + function Get-TimingVariable ($sched) { + $atcount = $sched.Count + if ($null -eq $atcount) { $atcount = 1 } + $scheduleTimeSpanSeconds = $artConfig.scheduleTimeSpan.TotalSeconds + $secondsForAllTestsToComplete = $scheduleTimeSpanSeconds + $sleeptime = ($secondsForAllTestsToComplete / $atcount) - 120 - $artConfig.kickOffDelay.TotalSeconds # 1 minute for restart and 1 minute delay for scheduled task and an optional kickoff delay + if ($sleeptime -lt 120) { $sleeptime = 120 } # minimum 2 minute sleep time + return $sleeptime + } + + # Convert OtherArgs to hashtable so we can pass it through to the call to Invoke-AtomicTest + $htvars = @{} + if ($OtherArgs) { + $OtherArgs | ForEach-Object { + if ($_ -match '^-') { + #New parameter + $lastvar = $_ -replace '^-' + $htvars[$lastvar] = $true + } + else { + #Value + $htvars[$lastvar] = $_ + } + } + } + if ($PSBoundParameters.ContainsKey("PauseBetweenAtomics")) { + $timeToPause = $PauseBetweenAtomics + } + else { + $timeToPause = $null + } + $htvars += [Hashtable]$PSBoundParameters + $htvars.Remove('listOfAtomics') | Out-Null + $htvars.Remove('OtherArgs') | Out-Null + $htvars.Remove('PauseBetweenAtomics') | Out-Null + $htvars.Remove('scheduledTaskCleanup') | Out-Null + + $schedule = Get-Schedule $listOfAtomics $true $null (-not $anyOS) + + # If the schedule is empty, end process + if (-not $schedule) { + LogRunnerMsg "No test guid's or enabled tests." + return + } + + # timing variables + $SleepTillCleanup = Get-TimingVariable $schedule + + # Perform cleanup, Showdetails or Prereq stuff for all scheduled items and then exit + if ($Cleanup -or $ShowDetails -or $CheckPrereqs -or $ShowDetailsBrief -or $GetPrereqs -or $listOfAtomics) { + $schedule | ForEach-Object { + Invoke-AtomicTestFromScheduleRow $_ + } + return + } + + # exit if file stop.txt is found + If (Test-Path $artConfig.stopFile) { + LogRunnerMsg "exiting script because $($artConfig.stopFile) does exist" + Write-Host -ForegroundColor Yellow "Exiting script because $($artConfig.stopFile) does exist."; Start-Sleep 10; + exit + } + + # Find current test to run + $guid = Get-GuidFromHostName $artConfig.basehostname + if ([string]::IsNullOrWhiteSpace($guid)) { + LogRunnerMsg "Test Guid ($guid) was null, using next item in the schedule" + } + else { + if ($artConfig.verbose) { LogRunnerMsg "Found Test: $guid specified in hostname" } + $sp = [Collections.Generic.List[Object]]$schedule + $currentIndex = $sp.FindIndex( { $args[0].auto_generated_guid -eq $guid }) + if (($null -ne $currentIndex) -and ($currentIndex -ne -1)) { + $tr = $schedule[$currentIndex] + } + + if ($null -ne $tr) { + if ($scheduledTaskCleanup) { + # Cleanup after running test + Write-Host -Fore cyan "Sleeping for $SleepTillCleanup seconds before cleaning up for $($tr.Technique) $($tr.auto_generated_guid) "; Start-Sleep -Seconds $SleepTillCleanup + $htvars.Add("Cleanup", $true) + Invoke-AtomicTestFromScheduleRow $tr + } + else { + # run the atomic test and exit + Invoke-AtomicTestFromScheduleRow $tr + Start-Sleep 3; exit + } + } + else { + LogRunnerMsg "Could not find Test: $guid in schedule. Please update schedule to run this test." + } + } + + # Load next scheduled test before renaming computer + $nextIndex += $currentIndex + 1 + if ($nextIndex -ge ($schedule.count)) { + $tr = $schedule[0] + } + else { + $tr = $schedule[$nextIndex] + } + + if ($null -eq $tr) { + LogRunnerMsg "Could not determine the next row to execute from the schedule, Starting from 1st row"; + $tr = $schedule[0] + } + + #Rename Computer and Restart + Rename-ThisComputer $tr $artConfig.basehostname + + } +} \ No newline at end of file diff --git a/Public/Invoke-KickoffAtomicRunner.ps1 b/Public/Invoke-KickoffAtomicRunner.ps1 index 284aae2..81bf9f9 100644 --- a/Public/Invoke-KickoffAtomicRunner.ps1 +++ b/Public/Invoke-KickoffAtomicRunner.ps1 @@ -1,57 +1,71 @@ -function Invoke-KickoffAtomicRunner { - - #log rotation function - function Rotate-Log { - Param ($logPath, $max_filesize, $max_age) - $datetime = Get-Date -uformat "%Y-%m-%d-%H%M" - - $log = Get-Item $logPath - if ($log.Length / 1MB -ge $max_filesize) { - Write-Host "file named $($log.name) is bigger than $max_filesize MB" - $newname = "$($log.Name)_${datetime}.arclog" - Rename-Item $log.PSPath $newname - Write-Host "Done rotating file" - } - - $logdir_content = Get-ChildItem $artConfig.atomicLogsPath -filter "*.arclog" - $cutoff_date = (get-date).AddDays($max_age) - $logdir_content | ForEach-Object { - if ($_.LastWriteTime -gt $cutoff_date) { - Remove-Item $_ - Write-Host "Removed $($_.PSPath)" - } - } - } - - #Create log files as needed - $all_log_file = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname).txt" - $all_log_file_cleanup = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname)-cleanup.txt" - New-Item $all_log_file -ItemType file -ErrorAction Ignore - New-Item $all_log_file_cleanup -ItemType file -ErrorAction Ignore - New-Item $artConfig.logFile -ItemType File -ErrorAction Ignore - - #Rotate logs based on FileSize and Date max_filesize - $max_filesize = 200 #in MB - $max_file_age = 30 #in days - Rotate-Log $all_log_file $max_filesize $max_file_age - Rotate-Log $all_log_file_cleanup $max_filesize $max_file_age - - Rotate-Log $artConfig.logFile $max_filesize $max_file_age #no need to repeat this. Can reduce further. - - # Optional additional delay before starting - Start-Sleep $artConfig.kickOffDelay.TotalSeconds - - $WorkingDirectory = if ($IsLinux -or $IsMacOS) { "/tmp" } else { $env:TEMP } - $FileName = if ($IsLinux -or $IsMacOS) { "pwsh" } else { "powershell.exe" } - if ($artConfig.debug) { $Arguments = "-Command Invoke-AtomicRunner *>> $all_log_file" } else { $Arguments = "-Command Invoke-AtomicRunner" } - # Invoke the atomic as its own process because we don't want to skip the cleanup and rename process in the event that AV kills the process running the atomic - Start-Process -FilePath $FileName -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory - # Run the cleanup commmands - if ($artConfig.debug) { Invoke-AtomicRunner -scheduledTaskCleanup *>> $all_log_file_cleanup } else { Invoke-AtomicRunner -scheduledTaskCleanup } -} - -function LogRunnerMsg ($message) { - $now = "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) - Write-Host -fore cyan $message - Add-Content $artConfig.logFile "$now`: $message" -} +function Invoke-KickoffAtomicRunner { + + #log rotation function + function Rotate-Log { + Param ($logPath, $max_filesize, $max_age) + $datetime = Get-Date -uformat "%Y-%m-%d-%H%M" + + $log = Get-Item $logPath + if ($log.Length / 1MB -ge $max_filesize) { + Write-Host "file named $($log.name) is bigger than $max_filesize MB" + $newname = "$($log.Name)_${datetime}.arclog" + Rename-Item $log.PSPath $newname + Write-Host "Done rotating file" + } + + $logdir_content = Get-ChildItem $artConfig.atomicLogsPath -filter "*.arclog" + $cutoff_date = (get-date).AddDays($max_age) + $logdir_content | ForEach-Object { + if ($_.LastWriteTime -gt $cutoff_date) { + Remove-Item $_ + Write-Host "Removed $($_.PSPath)" + } + } + } + + #Create log files as needed + $all_log_file = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname).txt" + $all_log_file_cleanup = Join-Path $artConfig.atomicLogsPath "all-out-$($artConfig.basehostname)-cleanup.txt" + New-Item $all_log_file -ItemType file -ErrorAction Ignore + New-Item $all_log_file_cleanup -ItemType file -ErrorAction Ignore + New-Item $artConfig.logFile -ItemType File -ErrorAction Ignore + + #Rotate logs based on FileSize and Date max_filesize + $max_filesize = 200 #in MB + $max_file_age = 30 #in days + Rotate-Log $all_log_file $max_filesize $max_file_age + Rotate-Log $all_log_file_cleanup $max_filesize $max_file_age + + Rotate-Log $artConfig.logFile $max_filesize $max_file_age #no need to repeat this. Can reduce further. + + # Optional additional delay before starting + Start-Sleep $artConfig.kickOffDelay.TotalSeconds + + $WorkingDirectory = if ($IsLinux -or $IsMacOS) { "/tmp" } else { $env:TEMP } + $FileName = if ($IsLinux -or $IsMacOS) { "pwsh" } else { "powershell.exe" } + if ($artConfig.debug) { $Arguments = "-Command Invoke-AtomicRunner *>> $all_log_file" } else { $Arguments = "-Command Invoke-AtomicRunner" } + # Invoke the atomic as its own process because we don't want to skip the cleanup and rename process in the event that AV kills the process running the atomic + $p1 = Start-Process -FilePath $FileName -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory -PassThru + + if ($artConfig.debug) { $Arguments = "-Command Invoke-AtomicRunner -scheduledTaskCleanup *>> $all_log_file_cleanup" } else { $Arguments = "-Command Invoke-AtomicRunner -scheduledTaskCleanup" } + $p2 = Start-Process -FilePath $FileName -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory -PassThru + + return $p1,$p2 + +} + +function LogRunnerMsg ($message) { + $mutexName = 'AtomicRunnerLoggingMutex' + $mutex = New-Object 'Threading.Mutex' $false, $mutexName + # Grab the mutex. Will block until this process has it. + $mutex.WaitOne() | Out-Null + try { + # OK. Now it is safe to write to your log file + $now = "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) + Write-Host -fore cyan $message + Add-Content $artConfig.logFile "$now`: $message" + } + finally { + $mutex.ReleaseMutex() + } +} \ No newline at end of file diff --git a/Private/Invoke-KillProcessTree.ps1 b/Public/Invoke-KillProcessTree.ps1 similarity index 100% rename from Private/Invoke-KillProcessTree.ps1 rename to Public/Invoke-KillProcessTree.ps1 diff --git a/Public/Invoke-RunnerScheduleMethods.ps1 b/Public/Invoke-RunnerScheduleMethods.ps1 index fbbedf4..ac133bc 100644 --- a/Public/Invoke-RunnerScheduleMethods.ps1 +++ b/Public/Invoke-RunnerScheduleMethods.ps1 @@ -96,7 +96,7 @@ function Get-ScheduleRefresh() { } -function Get-Schedule($listOfAtomics, $filtered = $true, $testGuids = $null) { +function Get-Schedule($listOfAtomics, $filterByEnabled = $true, $testGuids = $null, $filterByPlatform = $true) { if ($listOfAtomics -or (Test-Path($artConfig.scheduleFile))) { if ($listOfAtomics) { $schedule = Import-Csv $listOfAtomics @@ -111,9 +111,15 @@ function Get-Schedule($listOfAtomics, $filtered = $true, $testGuids = $null) { ($Null -ne $TestGuids -and $TestGuids -contains $_.auto_generated_guid) } } - elseif ($filtered) { - $schedule = $schedule | Where-Object { - ($_.enabled -eq $true -and ($_.supported_platforms -like "*" + $artConfig.OS + "*" )) + else { + if ($filterByEnabled -and $filterByPlatform) { + $schedule = $schedule | Where-Object { ($_.enabled -eq $true -and ($_.supported_platforms -like "*" + $artConfig.OS + "*" )) } + } + elseif ($filterByEnabled) { + $schedule = $schedule | Where-Object { $_.enabled -eq $true } + } + elseif ($filterByPlatform) { + $schedule = $schedule | Where-Object { $_.supported_platforms -like "*" + $artConfig.OS + "*" } } } diff --git a/Public/Invoke-SetupAtomicRunner.ps1 b/Public/Invoke-SetupAtomicRunner.ps1 index 45a9deb..bcf52b8 100755 --- a/Public/Invoke-SetupAtomicRunner.ps1 +++ b/Public/Invoke-SetupAtomicRunner.ps1 @@ -1,137 +1,197 @@ -function Invoke-SetupAtomicRunner { - - # ensure running with admin privs - if ($artConfig.OS -eq "windows") { - # auto-elevate on Windows - $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) - $testadmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) - if ($testadmin -eq $false) { - Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition)) - exit $LASTEXITCODE - } - } - else { - # linux and macos check - doesn't auto-elevate - if ((id -u) -ne 0 ) { - Throw "You must run the Invoke-SetupAtomicRunner script as root" - exit - } - } - - if ($artConfig.basehostname.length -gt 15) { Throw "The hostname for this machine (minus the GUID) must be 15 characters or less. Please rename this computer." } - - #create AtomicRunner-Logs directories if they don't exist - New-Item -ItemType Directory $artConfig.atomicLogsPath -ErrorAction Ignore - New-Item -ItemType Directory $artConfig.runnerFolder -ErrorAction Ignore - - if ($artConfig.gmsaAccount) { - Start-Service WinRM - $path = Join-Path $env:ProgramFiles "WindowsPowerShell\Modules\RenameRunner\RoleCapabilities" - New-Item -ItemType Directory $path -ErrorAction Ignore - New-PSSessionConfigurationFile -SessionType RestrictedRemoteServer -GroupManagedServiceAccount $artConfig.gmsaAccount -RoleDefinitions @{ "$($artConfig.user)" = @{ 'RoleCapabilities' = 'RenameRunner' } } -path "$env:Temp\RenameRunner.pssc" - New-PSRoleCapabilityFile -VisibleCmdlets @{ 'Name' = 'Rename-Computer'; 'Parameters' = @{ 'Name' = 'NewName'; 'ValidatePattern' = 'ATOMICSOC.*' }, @{ 'Name' = 'Force' }, @{ 'Name' = 'restart' } } -path "$path\RenameRunner.psrc" - $null = Register-PSSessionConfiguration -name "RenameRunnerEndpoint" -path "$env:Temp\RenameRunner.pssc" -force - Add-LocalGroupMember "administrators" "$($artConfig.gmsaAccount)$" -ErrorAction Ignore - # Make sure WinRM is enabled and set to Automic start (not delayed) - Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name Start -Value 2 - Set-ItemProperty hklm:\\SYSTEM\CurrentControlSet\Services\WinRM -Name DelayedAutostart -Value 0 # default is delayed start and that is too slow given our 1 minute delay on our kickoff task - # this registry key must be set to zero for things to work get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service\ - $hklmKey = (get-itemproperty hklm:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs - $hkcuKey = (get-itemproperty hkcu:\Software\Policies\Microsoft\Windows\WinRM\Service -name DisableRunAs -ErrorAction ignore).DisableRunAs - if ((1 -eq $hklmKey) -or (1 -eq $hkcuKey)) { Write-Host -ForegroundColor Red "DisableRunAs registry Key will not allow use of the JEA endpoint with a gmsa account" } - if ((Get-ItemProperty hklm:\System\CurrentControlSet\Control\Lsa\ -name DisableDomainCreds).DisableDomainCreds) { Write-Host -ForegroundColor Red "Do not allow storage of passwords and credentials for network authentication must be disabled" } - } - - if ($artConfig.OS -eq "windows") { - - if (Test-Path $artConfig.credFile) { - Write-Host "Credential File $($artConfig.credFile) already exists, not prompting for creation of a new one." - $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) - } - else { - # create credential file for the user since we aren't using a group managed service account - $cred = Get-Credential -UserName $artConfig.user -message "Enter password for $($artConfig.user) in order to create the runner scheduled task" - $cred.Password | ConvertFrom-SecureString | Out-File $artConfig.credFile - - } - - # setup scheduled task that will start the runner after each restart - # local security policy --> Local Policies --> Security Options --> Network access: Do not allow storage of passwords and credentials for network authentication must be disabled - $taskName = "KickOff-AtomicRunner" - Unregister-ScheduledTask $taskName -confirm:$false -ErrorAction Ignore - # Windows scheduled task includes a 20 minutes sleep then restart if the call to Invoke-KickoffAtomicRunner fails - # this occurs occassionally when Windows has issues logging into the runner user's account and logs in as a TEMP user - $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-exec bypass -Command Invoke-KickoffAtomicRunner; Start-Sleep 1200; Restart-Computer -Force" - $taskPrincipal = New-ScheduledTaskPrincipal -UserId $artConfig.user - $delays = @(1, 2, 4, 8, 16, 32, 64) # using multiple triggers as a retry mechanism because the built-in retry mechanism doesn't work when the computer renaming causes AD replication delays - $triggers = @() - foreach ($delay in $delays) { - $trigger = New-ScheduledTaskTrigger -AtStartup - $trigger.Delay = "PT$delay`M" - $triggers += $trigger - } - $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Trigger $triggers -Description "A task that runs 1 minute or later after boot to start the atomic test runner script" - try { - $null = Register-ScheduledTask -TaskName $taskName -InputObject $task -User $artConfig.user -Password $($cred.GetNetworkCredential().password) -ErrorAction Stop - } - catch { - if ($_.CategoryInfo.Category -eq "AuthenticationError") { - # remove the credential file if the password didn't work - Write-Error "The credentials you entered are incorrect. Please run the setup script again and double check the username and password." - Remove-Item $artConfig.credFile - } - else { - Throw $_ - } - } - } - else { - # sets cronjob string using basepath from config.ps1 - $pwshPath = which pwsh - $job = "@reboot root sleep 60;$pwshPath -Command Invoke-KickoffAtomicRunner" - $exists = cat /etc/crontab | Select-String -Quiet "KickoffAtomicRunner" - #checks if the Kickoff-AtomicRunner job exists. If not appends it to the system crontab. - if ($null -eq $exists) { - $(Write-Output "$job" >> /etc/crontab) - write-host "setting cronjob" - } - else { - write-host "cronjob already exists" - } - } - - # Add Import-Module statement to the PowerShell profile - $root = Split-Path $PSScriptRoot -Parent - $pathToPSD1 = Join-Path $root "Invoke-AtomicRedTeam.psd1" - $importStatement = "Import-Module ""$pathToPSD1"" -Force" - New-Item $PROFILE -ErrorAction Ignore - $profileContent = Get-Content $profile - $line = $profileContent | Select-String ".*import-module.*invoke-atomicredTeam.psd1" | Select-Object -ExpandProperty Line - if ($line) { - $profileContent | ForEach-Object { $_.replace( $line, "$importStatement") } | Set-Content $profile - } - else { - Add-Content $profile $importStatement - } - - # Install the Posh-SYLOG module if we are configured to use it and it is not already installed - if ((-not (Get-Module -ListAvailable "Posh-SYSLOG")) -and [bool]$artConfig.syslogServer -and [bool]$artConfig.syslogPort) { - write-verbose "Posh-SYSLOG" - Install-Module -Name Posh-SYSLOG -Scope CurrentUser -Force - } - - # create the CSV schedule of atomics to run if it doesn't exist - if (-not (Test-Path $artConfig.scheduleFile)) { - Invoke-GenerateNewSchedule - } - - $schedule = Get-Schedule - if ($null -eq $schedule) { - Write-Host -ForegroundColor Yellow "There are no tests enabled on the schedule, set the 'Enabled' column to 'True' for the atomic test that you want to run. The schedule file is found here: $($artConfig.scheduleFile)" - Write-Host -ForegroundColor Yellow "Rerun this setup script after updating the schedule" - } - else { - # Get the prereqs for all of the tests on the schedule - Invoke-AtomicRunner -GetPrereqs - } -} +function Invoke-SetupAtomicRunner { + + [CmdletBinding( + SupportsShouldProcess = $true, + PositionalBinding = $false, + ConfirmImpact = 'Medium')] + Param( + [Parameter(Mandatory = $false)] + [switch] + $SkipServiceSetup, + + [Parameter(Mandatory = $false)] + [switch] + $asScheduledtask + ) + + # ensure running with admin privs + if ($artConfig.OS -eq "windows") { + # auto-elevate on Windows + $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) + $testadmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + if ($testadmin -eq $false) { + Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition)) + exit $LASTEXITCODE + } + } + else { + # linux and macos check - doesn't auto-elevate + if ((id -u) -ne 0 ) { + Throw "You must run the Invoke-SetupAtomicRunner script as root" + exit + } + } + + if ($artConfig.basehostname.length -gt 15) { Throw "The hostname for this machine (minus the GUID) must be 15 characters or less. Please rename this computer." } + + #create AtomicRunner-Logs directories if they don't exist + New-Item -ItemType Directory $artConfig.atomicLogsPath -ErrorAction Ignore + New-Item -ItemType Directory $artConfig.runnerFolder -ErrorAction Ignore + + if ($artConfig.OS -eq "windows") { + if ($asScheduledtask) { + if (Test-Path $artConfig.credFile) { + Write-Host "Credential File $($artConfig.credFile) already exists, not prompting for creation of a new one." + $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $artConfig.user, (Get-Content $artConfig.credFile | ConvertTo-SecureString) + } + else { + # create credential file for the user since we aren't using a group managed service account + $cred = Get-Credential -UserName $artConfig.user -message "Enter password for $($artConfig.user) in order to create the runner scheduled task" + $cred.Password | ConvertFrom-SecureString | Out-File $artConfig.credFile + } + # setup scheduled task that will start the runner after each restart + # local security policy --> Local Policies --> Security Options --> Network access: Do not allow storage of passwords and credentials for network authentication must be disabled + $taskName = "KickOff-AtomicRunner" + Unregister-ScheduledTask $taskName -confirm:$false -ErrorAction Ignore + # Windows scheduled task includes a 20 minutes sleep then restart if the call to Invoke-KickoffAtomicRunner fails + # this occurs occassionally when Windows has issues logging into the runner user's account and logs in as a TEMP user + $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-exec bypass -Command Invoke-KickoffAtomicRunner; Start-Sleep 1200; Restart-Computer -Force" + $taskPrincipal = New-ScheduledTaskPrincipal -UserId $artConfig.user + $delays = @(1, 2, 4, 8, 16, 32, 64) # using multiple triggers as a retry mechanism because the built-in retry mechanism doesn't work when the computer renaming causes AD replication delays + $triggers = @() + foreach ($delay in $delays) { + $trigger = New-ScheduledTaskTrigger -AtStartup + $trigger.Delay = "PT$delay`M" + $triggers += $trigger + } + $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Trigger $triggers -Description "A task that runs 1 minute or later after boot to start the atomic test runner script" + try { + $null = Register-ScheduledTask -TaskName $taskName -InputObject $task -User $artConfig.user -Password $($cred.GetNetworkCredential().password) -ErrorAction Stop + } + catch { + if ($_.CategoryInfo.Category -eq "AuthenticationError") { + # remove the credential file if the password didn't work + Write-Error "The credentials you entered are incorrect. Please run the setup script again and double check the username and password." + Remove-Item $artConfig.credFile + } + else { + Throw $_ + } + } + + # remove the atomicrunnerservice now that we are using a scheduled task instead + . "$PSScriptRoot\AtomicRunnerService.ps1" -Remove + } + elseif (-not $SkipServiceSetup) { + # create the service that will start the runner after each restart + # The user must have the "Log on as a service" right. To add that right, open the Local Security Policy management console, go to the + # "\Security Settings\Local Policies\User Rights Assignments" folder, and edit the "Log on as a service" policy there. + . "$PSScriptRoot\AtomicRunnerService.ps1" -Remove + . "$PSScriptRoot\AtomicRunnerService.ps1" -UserName $artConfig.user -installDir $artConfig.serviceInstallDir -Setup + Add-EnvPath -Container Machine -Path $artConfig.serviceInstallDir + # set service start retry options + $ServiceDisplayName = "AtomicRunnerService" + $action1, $action2, $action3 = "restart" + $time1 = 600000 # 10 minutes in miliseconds + $action2 = "restart" + $time2 = 600000 # 10 minutes in miliseconds + $actionLast = "restart" + $timeLast = 3600000 # 1 hour in miliseconds + $resetCounter = 86400 # 1 day in seconds + $services = Get-CimInstance -ClassName 'Win32_Service' | Where-Object { $_.DisplayName -imatch $ServiceDisplayName } + $action = $action1 + "/" + $time1 + "/" + $action2 + "/" + $time2 + "/" + $actionLast + "/" + $timeLast + foreach ($service in $services) { + # https://technet.microsoft.com/en-us/library/cc742019.aspx + $output = sc.exe failure $($service.Name) actions= $action reset= $resetCounter + } + # set service to delayed auto-start (doesn't reflect in the services console until after a reboot) + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\AtomicRunnerService" -Name Start -Value 2 + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\AtomicRunnerService" -Name DelayedAutostart -Value 1 + + # remove scheduled task now that we are using a service instead + Unregister-ScheduledTask "KickOff-AtomicRunner" -confirm:$false -ErrorAction Ignore + } + } + else { + # sets cronjob string using basepath from config.ps1 + $pwshPath = which pwsh + $job = "@reboot root sleep 60;$pwshPath -Command Invoke-KickoffAtomicRunner" + $exists = cat /etc/crontab | Select-String -Quiet "KickoffAtomicRunner" + #checks if the Kickoff-AtomicRunner job exists. If not appends it to the system crontab. + if ($null -eq $exists) { + $(Write-Output "$job" >> /etc/crontab) + write-host "setting cronjob" + } + else { + write-host "cronjob already exists" + } + } + + # Add Import-Module statement to the PowerShell profile + $root = Split-Path $PSScriptRoot -Parent + $pathToPSD1 = Join-Path $root "Invoke-AtomicRedTeam.psd1" + $importStatement = "Import-Module ""$pathToPSD1"" -Force" + $profileFolder = Split-Path $profile + New-Item -ItemType Directory -Force -Path $profileFolder | Out-Null + New-Item $PROFILE -ErrorAction Ignore + $profileContent = Get-Content $profile + $line = $profileContent | Select-String ".*import-module.*invoke-atomicredTeam.psd1" | Select-Object -ExpandProperty Line + if ($line) { + $profileContent | ForEach-Object { $_.replace( $line, "$importStatement") } | Set-Content $profile + } + else { + Add-Content $profile $importStatement + } + + # Install the Posh-SYLOG module if we are configured to use it and it is not already installed + if ((-not (Get-Module -ListAvailable "Posh-SYSLOG")) -and [bool]$artConfig.syslogServer -and [bool]$artConfig.syslogPort) { + write-verbose "Posh-SYSLOG" + Install-Module -Name Posh-SYSLOG -Scope CurrentUser -Force + } + + # create the CSV schedule of atomics to run if it doesn't exist + if (-not (Test-Path $artConfig.scheduleFile)) { + Invoke-GenerateNewSchedule + } + + $schedule = Get-Schedule + if ($null -eq $schedule) { + Write-Host -ForegroundColor Yellow "There are no tests enabled on the schedule, set the 'Enabled' column to 'True' for the atomic test that you want to run. The schedule file is found here: $($artConfig.scheduleFile)" + Write-Host -ForegroundColor Yellow "Rerun this setup script after updating the schedule" + } + else { + # Get the prereqs for all of the tests on the schedule + Invoke-AtomicRunner -GetPrereqs + } +} + +# Add-EnvPath from https://gist.github.com/mkropat/c1226e0cc2ca941b23a9 +function Add-EnvPath { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + + [ValidateSet('Machine', 'User', 'Session')] + [string] $Container = 'Session' + ) + + if ($Container -ne 'Session') { + $containerMapping = @{ + Machine = [EnvironmentVariableTarget]::Machine + User = [EnvironmentVariableTarget]::User + } + $containerType = $containerMapping[$Container] + + $persistedPaths = [Environment]::GetEnvironmentVariable('Path', $containerType) -split ';' + if ($persistedPaths -notcontains $Path) { + $persistedPaths = $persistedPaths + $Path | Where-Object { $_ } + [Environment]::SetEnvironmentVariable('Path', $persistedPaths -join ';', $containerType) + } + } + + $envPaths = $env:Path -split ';' + if ($envPaths -notcontains $Path) { + $envPaths = $envPaths + $Path | Where-Object { $_ } + $env:Path = $envPaths -join ';' + } +} \ No newline at end of file diff --git a/Public/config.ps1 b/Public/config.ps1 index 9f5cc99..2a92005 100644 --- a/Public/config.ps1 +++ b/Public/config.ps1 @@ -21,9 +21,6 @@ $artConfig = [PSCustomObject]@{ kickOffDelay = New-TimeSpan -Minutes 0 # an additional delay before Invoke-KickoffAtomicRunner calls Invoke-AtomicRunner scheduleFileName = "AtomicRunnerSchedule.csv" - # [optional] If you need to use a group managed service account in order to rename the computer, enter it here - gmsaAccount = $null - # [optional] Logging Module, uses Syslog-ExecutionLogger if left blank and the syslogServer and syslogPort are set, otherwise it uses the Default-ExecutionLogger LoggingModule = '' @@ -41,6 +38,9 @@ $artConfig = [PSCustomObject]@{ # amsi bypass script block (applies to Windows only) absb = $null + # AtomicRunnerService install directory + ServiceInstallDir = "${ENV:windir}\System32" + } # If you create a file called privateConfig.ps1 in the same directory as you installed Invoke-AtomicRedTeam you can overwrite any of these settings with your custom values