Logging PowerShell activity

Recent versions of Windows PowerShell provide several features for logging activity from PowerShell sessions. NXLog Agent can be configured to collect and parse PowerShell logs.

In addition to the sections below, see Securing PowerShell in the Enterprise, Greater Visibility Through PowerShell Logging, and PowerShell ♥ the Blue Team. Also see the Command line process auditing article on Microsoft Docs, the Windows Command Line Auditing and Sysmon chapters, which can be used to generate events for command line process creation (but not for commands executed through the PowerShell engine).

Module logging with PowerShell

Module logging, available since PowerShell 3, logs pipeline execution events for specified PowerShell modules. This feature writes Event ID 4103 events to the Microsoft-Windows-PowerShell/Operational channel.

Module logging can be enabled by setting the LogPipelineExecutionDetails property of a module to TRUE. Otherwise, this property can be enabled for selected modules through Group Policy as follows.

  1. Open the Group Policy MMC snapin (gpedit.msc).

  2. Go to Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell and open the Turn on Module Logging setting.

    Finding the Module Logging policy
  3. Select Enabled. Then click the Show… button and enter the modules for which to enable logging. Use an asterisk (*) to enable logging for all modules.

    Configuring the Module Logging policy
Example 1. Collecting module logging events

This configuration collects all events with ID 4103 from the Windows PowerShell Operational channel. First, the key-value pairs from the ContextInfo field are parsed to remove the \n and \r\n characters where required, after that, the ContextInfo_ prefix is added to enhance visibility. In addition, the original Message and ContextInfo fields are removed with their corresponding content as they are available elsewhere in the output. Finally, the logs are converted to JSON.

nxlog.conf
<Extension kvp>
    Module          xm_kvp
    KVPDelimiter    ,
    KVDelimiter     =
</Extension>

<Extension json>
    Module          xm_json
</Extension>

<Input in>
    Module          im_msvistalog
    <QueryXML>
        <QueryList>
            <Query Id="0" Path="Microsoft-Windows-PowerShell/Operational">
                <Select Path="Microsoft-Windows-PowerShell/Operational">
                    *[System[EventID=4103]]</Select>
            </Query>
        </QueryList>
    </QueryXML>
    <Exec>
        if defined($ContextInfo)
        {
            $ContextInfo = replace($ContextInfo, "\r\n", ",");
            $ContextInfo = replace($ContextInfo, "\n", ",");
            $ContextInfo = replace($ContextInfo, "        ", "");
            kvp->parse_kvp($ContextInfo, "ContextInfo_");
            delete($ContextInfo);
            delete($Message);
        }
        json->to_json();
    </Exec>        
</Input>
Output sample
{
  "EventTime": "2020-01-29T05: 30: 45.727799-08: 00",
  "Hostname": "NXLog-Server",
  "Keywords": "0",
  "EventType": "INFO",
  "SeverityValue": 2,
  "Severity": "INFO",
  "EventID": 4103,
  "SourceName": "Microsoft-Windows-PowerShell",
  "ProviderGuid": "{A0C1853B-5C40-4B15-8766-3CF1C58F985A}",
  "Version": 1,
  "TaskValue": 106,
  "OpcodeValue": 20,
  "RecordNumber": 170,
  "ActivityID": "{9C1FE60B-D6F2-0000-3316-209CF2D6D501}",
  "ExecutionProcessID": 3648,
  "ExecutionThreadID": 1060,
  "Channel": "Microsoft-Windows-PowerShell/Operational",
  "Domain": "NXLog-Server",
  "AccountName": "Administrator",
  "UserID": "S-1-5-21-2463765617-934790487-2583750676-500",
  "AccountType": "User",
  "Category": "Executing Pipeline",
  "Opcode": "To be used when operation is just executing a method",
  "Payload": "Update-Help has completed successfully.",
  "EventReceivedTime": "2020-01-29T05: 30: 47.161585-08: 00",
  "SourceModuleName": "in",
  "SourceModuleType": "im_msvistalog",
  "ContextInfo_Severity": "Informational",
  "ContextInfo_Host Name": "ConsoleHost",
  "ContextInfo_Host Version": "5.1.17763.592",
  "ContextInfo_Host ID": "67d049eb-f3d6-4718-8cd2-b9dae30c4c7b",
  "ContextInfo_Host Application": "C: \\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
  "ContextInfo_Engine Version": "5.1.17763.592",
  "ContextInfo_Runspace ID": "3145a9e1-18e3-4fa1-8700-fc78c783684b",
  "ContextInfo_Pipeline ID": 6,
  "ContextInfo_Command Name": "Update-Help",
  "ContextInfo_Command Type": "Cmdlet",
  "ContextInfo_Script Name": null,
  "ContextInfo_Command Path": null,
  "ContextInfo_Sequence Number": 79,
  "ContextInfo_User": "NXLog-Server\\Administrator",
  "ContextInfo_Connected User": null,
  "ContextInfo_Shell ID": "Microsoft.PowerShell"
}

Script block logging

PowerShell 5 introduces script block logging, which records the content of all script blocks that are processed. Events with event ID 4104 are written to the Microsoft-Windows-PowerShell/Operational channel. Start and stop events can also be enabled; these events have event ID 4105 and event ID 4106 respectively.

Script block logging can be configured through Group Policy as follows.

  1. Open the Group Policy MMC snapin (gpedit.msc).

  2. Go to Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell and open the Turn on PowerShell Script Block Logging setting.

    Finding the Script Block Logging policy
  3. Select Enabled. Optionally, check the Log script block invocation start/stop events option (this will generate a high volume of event logs).

    Configuring the Script Block Logging policy
Example 2. Collecting script block logging events

The following configuration collects events with IDs 4104, 4105, and 4106 from the Windows PowerShell Operational channel. Verbose-level events are excluded.

nxlog.conf
<Input script_block_logging>
    Module  im_msvistalog
    <QueryXML>
        <QueryList>
            <Query Id="0" Path="Microsoft-Windows-PowerShell/Operational">
                <Select Path="Microsoft-Windows-PowerShell/Operational">
                    *[System[(Level=0 or Level=1 or Level=2 or Level=3 or Level=4)
                             and ((EventID &gt;= 4104 and EventID &lt;= 4106))]]
                </Select>
            </Query>
        </QueryList>
    </QueryXML>
</Input>

Transcription

PowerShell provides "over-the-shoulder" transcription of PowerShell sessions with the Start-Transcript cmdlet. With PowerShell 5, system-wide transcription can be enabled via Group Policy; this is equivalent to calling the Start-Transcript cmdlet on each PowerShell session. Transcriptions are written to the current user’s Documents directory unless a system-level output directory is set in the policy settings.

Log sample (with invocation headers enabled)
**********************
Windows PowerShell transcript start
Start time: 20171030223248
Username: WIN-FT17VBNL4B2\Administrator
RunAs User: WIN-FT17VBNL4B2\Administrator
Machine: WIN-FT17VBNL4B2 (Microsoft Windows NT 10.0.14393.0)
Host Application: C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe
Process ID: 4268
PSVersion: 5.1.14393.1770
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.14393.1770
BuildVersion: 10.0.14393.1770
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
**********************
Command start time: 20171030223255
**********************
PS C:\Users\Administrator> echo test
test
**********************
Command start time: 20171030223256
**********************
PS C:\Users\Administrator> exit
**********************
Windows PowerShell transcript end
End time: 20171030223256
**********************

System-wide transcription can be enabled through Group Policy as follows.

If system-wide transcription to a shared location is enabled, access to that directory should be limited to prevent users from viewing the transcripts of other users or computers.
  1. Open the Group Policy MMC snapin (gpedit.msc).

  2. Go to Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell and open the Turn on PowerShell Transcription setting.

  3. Select Enabled. Set a system-wide transcript output directory if required. Check the Include invocation headers option (this setting generates a timestamp for each command and is recommended).

    Configuring the Transcription policy
Example 3. Parsing PowerShell transcriptions

This configuration reads and parses transcript files written to the TRANSCRIPTS_DIR directory (which should be set appropriately). Headers, footers, and commands are parsed as separate events. $File and $EventTime fields are set for each event (invocation headers must be enabled for command timestamps). $Command and $Output fields are added for command events. Fields from the header entries are parsed with xm_kvp and added to the event record. Finally, the logs are converted to JSON format and forwarded via TCP.

The HeaderLine below must be changed if invocation headers are not enabled. See the comment in the configuration.
nxlog.conf
define TRANSCRIPTS_DIR C:\powershell

<Extension transcript_parser>
    Module          xm_multiline
    # Use this if invocation headers are ON (recommended)
    HeaderLine      /^\*{22}$/
    # Use this if invocation headers are OFF (not recommended)
    #HeaderLine  /^(\*{22}$|PS[^>]*>)/
    <Exec>
        $raw_event =~ s/^\xEF\xBB\xBF//;
        if get_var('include_next_record') and $raw_event =~ /^\*{22}$/
        {
            set_var('include_next_record', FALSE);
            $raw_event =~ s/^\*//;
        }
        else if $raw_event =~ /^Command start time: \d{14}$/
            set_var('include_next_record', TRUE);
    </Exec>
</Extension>

<Extension transcript_header_parser>
    Module          xm_kvp
    KVPDelimiter    \n
</Extension>

<Input transcription>
    Module          im_file
    File            '%TRANSCRIPTS_DIR%\\*PowerShell_transcript.*'
    InputType       transcript_parser
    Recursive       TRUE
    <Exec>
        # Add file name as field to event record
        $File = file_name();

        # Drop extra separator line events
        if $raw_event =~ /^\*{22}$/
            drop();

        # Parse header and footer entries
        else if $raw_event =~ /(?x)^\*{22}\r\n
            (?<Message>Windows\ PowerShell\ transcript\ (start|end)\r\n
            (?<KVP>.*))$/s
        {
            $KVP =~ s/\r//g;
            transcript_header_parser->parse_kvp($KVP);
            delete($KVP);
            if ${Start time}
                $EventTime = strptime(string(${Start time}), '%Y%m%d%H%M%S');
            else
                $EventTime = strptime(string(${End time}), '%Y%m%d%H%M%S');
        }

        # Parse command entries with invocation headers
        else if $raw_event =~ /(?x)^\*{22}\r\n
            (?<Part1>Command\ start\ time:\ (?<EventTime>\d{14})\r\n)
            (?<Part2>\*{21}\r\nPS[^>]*>\ ?
                     (?<Command>[^\r\n]+)(\r\n(?<Output>.+))?)$/s
        {
            $Message = $Part1 + '*' + $Part2;
            delete($Part1);
            delete($Part2);
            $EventTime = strptime(string($EventTime) + '', '%Y%m%d%H%M%S');
        }

        # Parse command entries without invocation headers
        else if $raw_event =~ /(?x)^PS[^>]*>\ ?
             (?<Command>[^\r\n]+)(\r\n(?<Output>.+))?$/s
            $Message = $raw_event;
        else log_warning('Failed to parse transcript entry');
    </Exec>
</Input>

The following output shows the first two events of the log sample above.

Output sample
{
  "EventReceivedTime": "2017-10-30 22:32:49",
  "SourceModuleName": "transcription",
  "SourceModuleType": "im_file",
  "File": "C:\\powershell\\\\20171030\\PowerShell_transcript.WIN-FT17VBNL4B2.LcxuCZbr.20171030223248.txt",
  "Message": "Windows PowerShell transcript start\r\nStart time: 20171030223248\r\nUsername: WIN-FT17VBNL4B2\\Administrator\r\nRunAs User: WIN-FT17VBNL4B2\\Administrator\r\nMachine: WIN-FT17VBNL4B2 (Microsoft Windows NT 10.0.14393.0)\r\nHost Application: C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\PowerShell.exe\r\nProcess ID: 4268\r\nPSVersion: 5.1.14393.1770\r\nPSEdition: Desktop\r\nPSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.14393.1770\r\nBuildVersion: 10.0.14393.1770\r\nCLRVersion: 4.0.30319.42000\r\nWSManStackVersion: 3.0\r\nPSRemotingProtocolVersion: 2.3\r\nSerializationVersion: 1.1.0.1",
  "Start time": "20171030223248",
  "Username": "WIN-FT17VBNL4B2\\Administrator",
  "RunAs User": "WIN-FT17VBNL4B2\\Administrator",
  "Machine": "WIN-FT17VBNL4B2 (Microsoft Windows NT 10.0.14393.0)",
  "Host Application": "C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\PowerShell.exe",
  "Process ID": "4268",
  "PSVersion": "5.1.14393.1770",
  "PSEdition": "Desktop",
  "PSCompatibleVersions": "1.0, 2.0, 3.0, 4.0, 5.0, 5.1.14393.1770",
  "BuildVersion": "10.0.14393.1770",
  "CLRVersion": "4.0.30319.42000",
  "WSManStackVersion": "3.0",
  "PSRemotingProtocolVersion": "2.3",
  "SerializationVersion": "1.1.0.1",
  "EventTime": "2017-10-30 22:32:48"
}
{
  "EventReceivedTime": "2017-10-30 22:32:56",
  "SourceModuleName": "transcription",
  "SourceModuleType": "im_file",
  "File": "C:\\powershell\\\\20171030\\PowerShell_transcript.WIN-FT17VBNL4B2.LcxuCZbr.20171030223248.txt",
  "Command": "echo test",
  "EventTime": "2017-10-30 22:32:55",
  "Output": "test",
  "Message": "Command start time: 20171030223255\r\n**********************\r\nPS C:\\Users\\Administrator> echo test\r\ntest"
}
Disclaimer

While we endeavor to keep the information in our guides up to date and correct, NXLog makes no representations or warranties of any kind, express or implied about the completeness, accuracy, reliability, suitability, or availability of the content represented here. We update our screenshots and instructions on a best-effort basis.

Last revision: 8 May 2020