2

This is what I've gathered so far. I need a scheduled task that runs only 1 time, at the next startup (whenever that is) and after it runs it should delete itself.

Register-ScheduledTask -TaskName "Test 1" -InputObject (
    (
        New-ScheduledTask -Action (
            New-ScheduledTaskAction -Execute "PowerShell.exe"
        ) -Trigger (
            New-ScheduledTaskTrigger -Once -AtStartup # both of these can't be used
        ) -Settings (
            New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter 00:00:01 
        ) 
    ) | ForEach-Object { $_.Triggers[0].EndBoundary = <# 1 second after task is run #> ; $_ }
)

I'm not sure if it's possible, if it's not, please let me know so I can explore other options, but if it is, I need help figuring out how I can make it happen. The EndBoundary looks like it needs a date but I don't have any to give it, because it's unknown when the next Startup is.

I'm doing this because I need to run a PowerShell code at the next startup (time unknown) and only 1 time.

stackprotector
  • 10,498
  • 4
  • 35
  • 64
SpyNet
  • 323
  • 8
  • Not sure if there's a solution based on scheduled tasks, but you can create a registry value in key `registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce` – mklement0 May 31 '23 at 02:10
  • 1
    @mklement0 Thank you, found the documentation for that and created a perfect working method based on it. https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys – SpyNet May 31 '23 at 20:08

3 Answers3

2

-Once will only work with calendar-based triggers, such as "monthly", "weekly", etc. You cannot combine that with -AtStartup so that it runs only for one startup.

-DeleteExpiredTaskAfter will only work with an expiry date. But an expiry date does not work well with -AtStartup, as the task could expire before the machine boots.

An easy way to self delete the task after execution is to add the removal to the PowerShell script that you are executing anyway. Just add the following line to your PowerShell script:

Unregister-ScheduledTask -TaskName 'Test 1' -Confirm:$false

You can also just disable it so that it does not run again, but you can get info about when it did run and what the result was:

Disable-ScheduledTask -TaskName 'Test 1'

If you do not want to modify your main action, you can also add a second action, that is defined like this:

New-ScheduledTaskAction -Execute powershell -Argument "-Command `"Unregister-ScheduledTask -TaskName 'Test 1' -Confirm:`$false`""

So in total, your task definition could look like this:

Register-ScheduledTask -TaskName "Test 1" -InputObject (
    (
        New-ScheduledTask -Action (
            (New-ScheduledTaskAction -Execute "PowerShell.exe"),
            (New-ScheduledTaskAction -Execute powershell -Argument "-ExecutionPolicy ByPass -Command `"Unregister-ScheduledTask -TaskName 'Test 1' -Confirm:`$false`"")
        ) -Trigger (
            New-ScheduledTaskTrigger -AtStartup
        )
    )
)
stackprotector
  • 10,498
  • 4
  • 35
  • 64
  • I can't rely on the same PowerShell code that creates the task to delete it as well, what I need is a standalone feature that runs on its own and deletes itself, without me doing anything extra, that way it doesn't need clean up. From what I'm seeing, it's not possible to do it via scheduled task, but it is possible via this: https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys – SpyNet May 31 '23 at 20:14
  • If I add a 2nd action, then I'll need a 3rd action to delete the 2nd action and so on. – SpyNet May 31 '23 at 20:16
  • @SpyNet You are mixing up terms here. An action is not a task. A task can consist of multiple actions. And one of those actions can delete the whole task. – stackprotector Jun 01 '23 at 04:43
  • I added another example for better understanding. – stackprotector Jun 01 '23 at 04:54
1

Thanks to mklement0 comment on this question, I found a way to achieve what I needed, It's not using Scheduled tasks, it uses this: https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys

Since my module performs sensitive task, I need a perfect workflow for it so that even if something unexpected like power outage happens, the code integrity policy will return to enforced mode. This Audit/Enforced CI policy swap only lasts for few seconds but it's important to account for unexpected events during that time.

Here I'm setting a snapback guarantee using RunOnce, machine-wide

################# Snap back guarantee #################
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
$command = @"
CiTool --update-policy "$((Get-Location).Path)\$PolicyID.cip" -json; Remove-Item -Path "$((Get-Location).Path)\$PolicyID.cip" -Force
"@
$command | Out-File "C:\EnforcedModeSnapBack.ps1"
New-ItemProperty -Path $registryPath -Name '*CIPolicySnapBack' -Value "powershell.exe -WindowStyle `"Hidden`" -ExecutionPolicy `"Bypass`" -Command `"& {&`"C:\EnforcedModeSnapBack.ps1`";Remove-Item -Path 'C:\EnforcedModeSnapBack.ps1' -Force}`"" -PropertyType String

Then I perform some module related tasks.

Once the module reaches to a point where it's successfully completed its tasks and re-deployed the CI policy in enforced mode, then I remove the snapback guarantee in a finally block since it's no longer needed.

finally {                                          
    # Deploy Enforced mode CIP
    Write-Debug -Message "Finally Block Running"
    Update-BasePolicyToEnforced
    # Enforced Mode Snapback removal after base policy has already been successfully re-enforced
    Remove-Item -Path "C:\EnforcedModeSnapBack.ps1" -Force                     
    Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -Name "*CIPolicySnapBack" -Force
}

This way no trace of this will be left on the system and this feature works transparently, also works when user boots into safe mode.

SpyNet
  • 323
  • 8
  • 1
    Nicely done; suggestions re the PowerShell CLI call. It's simpler to change the execution policy by placing `-ExecutionPolicy Bypass` before the `-Command` parameter. There's no reason to use `& { ... }` in the `-Command` argument - just use `...` directly. (Older versions of the [CLI documentation](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh) erroneously suggested that `& { ... }` is required, but this has since been corrected.) – mklement0 May 31 '23 at 20:39
  • Thank you very much, I fixed the Execution policy but when I remove the `&` it won't work correctly. I read the documentation you linked to, it's for `pwsh.exe` but I use PowerShell.exe` because even though my module requires PowerShell 7.3.4, I'm not sure if using `pwsh.exe` in CMD like that will work if a user installed PowerShell through Microsoft Store, I tired installing it from there before but if i remember correctly there was an issue calling `pwsh.exe` like that. If I'm wrong please let me know. – SpyNet May 31 '23 at 21:56
  • 1
    It's not just `&` that you can remove but the `& { ... }` _enclosure_ - that is, replace the later with `...` (representing whatever commands you want to execute). In this respect, the CLIs of both editions behave the same. Yes, it makes sense to rely on `powershell.exe`, given that it is guaranteed to be present. – mklement0 May 31 '23 at 22:01
0

Here is another answer, this one does use Scheduled tasks only.

As explained in my first answer, I do this for a guaranteed CI policy enforced mode Snapback.

First I create a scheduled task

$taskAction = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '/c c:\cmd.cmd'
$taskTrigger = New-ScheduledTaskTrigger -AtLogOn
$principal = New-ScheduledTaskPrincipal -GroupId 'BUILTIN\Administrators' -RunLevel Highest
$TaskSettings = New-ScheduledTaskSettingsSet -Hidden -Compatibility Win8 -DontStopIfGoingOnBatteries -Priority 0 -AllowStartIfOnBatteries
Register-ScheduledTask -TaskName 'EnforcedModeSnapBack' -Action $taskAction -Trigger $taskTrigger -Principal $principal -Settings $TaskSettings
                                
Set-Content "c:\cmd.cmd" -Value @"
CiTool --update-policy "$((Get-Location).Path)\$PolicyID.cip" -json
schtasks /Delete /TN EnforcedModeSnapBack /F
del "%~f0"
"@

Then I do some module related tasks

then there is finally block that runs if everything was completed successfully

Unregister-ScheduledTask -TaskName 'EnforcedModeSnapBack' -Confirm:$false
Remove-Item -Path 'c:\cmd.cmd' -Force

This method also deletes itself as well as the task that was created, so leaves no trace.

P.S -AtLogOn is important here, -AtStartup wouldn't work with this method.

https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtasksettingsset

SpyNet
  • 323
  • 8