Public/Invoke-SetupAtomicRunner.ps1
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 ';' } } |