powershellpowershell-cmdletwindows-task-schedulerpowershell-modulepowershell-7.3

How to run my PowerShell module cmdlet as SYSTEM?


Created a PowerShell module, it has a function and exposes a cmdlet for that function. the built-in PowerShell 5.1 and pwsh.exe 7.3.1 (Installed using MSI installer) can detect and run the cmdlet without problem.

now I need that cmdlet to "run whether the user is logged on or not" in Windows task scheduler.

the problem arises when I try to run my PowerShell module's cmdlet as NT AUTHORITY\SYSTEM.

Which I need to do because in task scheduler, that appears to be the only way to get scheduled task "run whether the user is logged on or not". (I don't want to manually enter username or password of any Windows user account)

enter image description here

Ideally, I'd rather use built in administrators security group but as you can see then i won't be able to run the task if the user is not logged on.

enter image description here

so I'm really stuck here not sure what to do. I assume this is one of the edge cases I'm encountering.

I need to find a way so that when PowerShell is running as SYSTEM, it will still be able to detect my module's cmdlet.

I know my cmdlet isn't detected when PowerShell is running as SYSTEM because I tested it with PsExec64.

I put my PowerShell module in here (that's where they get installed by default from online galleries):

C:\Users\<UserName>\OneDrive\Documents\PowerShell\Modules\ModuleFolder

This is the entire block of the PowerShell script I use to create my task.

$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-command 'myCmdLet -parameter1 $variable1"

# First thing I tried
$TaskPrincipal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Administrators" -RunLevel Highest

# Second thing I tried
$TaskPrincipal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest

$trigger = New-ScheduledTaskTrigger -AtStartup

Register-ScheduledTask -Action $action -Trigger $trigger -Principal $TaskPrincipal -TaskName "Name" -Description "Description"

$TaskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Compatibility Win8 -StartWhenAvailable

Set-ScheduledTask -TaskName "Name" -Settings $TaskSettings 

UPDATE:

I found a way:

$TaskPrincipal = New-ScheduledTaskPrincipal -LogonType S4U -UserId $env:USERNAME -RunLevel Highest

which runs the task as administrator (since the module cmdlet won't even work without administrator privileges) and also checks these 2 boxes which I really wanted to do. however, still looking for a way to make my module's cmdlet known to PowerShell when it runs as SYSTEM, it will provide 1 benefit, AFAIK, which is to remove the dependency on a specific user account existing on the computer.

enter image description here


Solution

  • To summarize:


    The following self-contained example:

    #requires -RunAsAdministrator
    
    # Abort on any error.
    # Note: Since the *-ScheduledTask* cmdlets are implemented as 
    #       CDXML-based *script* modules, they only see preference vars.
    #       in the *global* scope - see https://github.com/PowerShell/PowerShell/issues/4568
    $prevEaPref = $global:ErrorActionPreference
    $global:ErrorActionPreference = 'Stop'
    
    try {
    
        # Create a simple script module that exports function Get-Foo,
        # in the current user's home dir., named '_test.psm1'
        @'
    function Get-Foo { "Hi, I'm running at $(Get-Date)`n * as $env:USERNAME (whose home dir. is '$HOME')`n * in '$PWD'`n * $(('NON-elevated', 'ELEVATED')[[bool] (net session 2>$null)])." }
    '@ > ~/_test.psm1
    
        # Set up the scheduled task:
    
        # The command (program) to run.
        # Import the test-module via its full path, call Get-Foo, and redirect all output streams
        # to file '_test.txt' in the current users' home dir.
        $action = New-ScheduledTaskAction -Execute powershell -Argument "-ExecutionPolicy Bypass -NoProfile -Command & { Import-Module `"$((Get-Item ~/_test.psm1).FullName)`"; Get-Foo } *> `"$((Get-Item ~).FullName)\_test.txt`""
    
        # Run as 'NT AUTHORITY\SYSTEM', which runs:
        #  * invisibly
        #  * whether or not someone is logged on
        #  * implicitly with elevation
        $user = New-ScheduledTaskPrincipal -UserID 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount
    
        # # Advanced settings such as whether to allow start on demand, not running when on batter power, ... 
        # $settings = New-ScheduledTaskSettingsSet
    
        # When to run it: Run a few seconds from now, once.
        $secsFromNow = 5
        $when = (Get-Date).AddSeconds($secsFromNow)
        $trigger = New-ScheduledTaskTrigger -Once -At $when
    
        # Create the task from the above.
        $newTask = New-ScheduledTask -Action $action -Principal $user -Trigger $trigger
    
        # Register the task with name '_Test'
        Write-Verbose -Verbose "Creating task..."
        Register-ScheduledTask '_Test' -InputObject $newTask -Force
    
        Write-Verbose -Verbose "Task will execute invisibly in $secsFromNow seconds, running as 'NT AUTHORITY\SYSTEM'. Waiting (plus a few extra seconds)..."
        Start-Sleep ($secsFromNow + 5) # Wait an extra few seconds to give the task time to complete.
    
        Write-Verbose -Verbose "Task is assumed to have run. Output logged:"
        Get-Content ~/_test.txt
    }
    finally {
        # Clean up.
        Remove-Item -ErrorAction Ignore ~/_test.psm1, ~/_test.txt
        Unregister-ScheduledTask -ErrorAction Ignore -TaskName _Test -Confirm:$false
        $global:ErrorActionPreference = $prevEaPref
    }
    

    Sample output:

    VERBOSE: Creating task...
    
    TaskPath                                       TaskName                          State
    --------                                       --------                          -----
    \                                              _Test                             Ready
    VERBOSE: Task will execute invisibly in 5 seconds, running as 'NT AUTHORITY\SYSTEM'. Waiting (plus a few extra seconds)...
    VERBOSE: Task is assumed to have run. Output logged:
    Hi, I'm running at 01/11/2023 17:06:04
     * as WORKSTATION1$ (whose home dir. is 'C:\Windows\system32\config\systemprofile')
     * in 'C:\Windows\system32'
     * ELEVATED.
    

    The last 4 lines are the module function's (Get-Foo's) output, proving that the module was successfully imported in the context of the task.