Last active
August 13, 2018 18:50
-
-
Save neu5ron/450289373db61d5c8d7378e79455ef07 to your computer and use it in GitHub Desktop.
Windows PowerShell Logstash Parser. Parses EventID's 4103 and 4104. Hash Script Block Text ---- useful for finding reoccuring scripts we want to whitelist/blacklist. Hash Script Block Text and UserID ---- because sometimes certain accounts should not run certain scripts, so filtering just by hash could be a problem.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
filter { | |
if [@meta][log][type] == "windows-wef" { | |
# PowerShell Operational Only | |
if [Channel] == "Microsoft-Windows-PowerShell/Operational" { | |
# EventID 4103 | |
if [EventID] == 4103 { | |
mutate { | |
add_field => { | |
"PayLoadInvocation" => "%{Payload}" | |
"PayLoadParams" => "%{Payload}" | |
} | |
gsub => [ | |
# Normalize ContextInfo | |
"ContextInfo", " ", "", | |
"ContextInfo", " = ", "=" | |
] | |
} | |
# Parse ContextInfo | |
kv { | |
source => "ContextInfo" | |
field_split => "\r\n" | |
value_split => "=" | |
remove_char_key => " " | |
allow_duplicate_values => false | |
# Set only allowed keys/fields incase ever an error parsing where something could contain a similar value_split of "=" | |
include_keys => [ "Severity", "HostName", "HostVersion", "HostID", "HostApplication", "EngineVersion", "RunspaceID", "PipelineID", "CommandName", "CommandType", "ScriptName", "CommandPath", "SequenceNumber", "User", "ConnectedUser", "ShellID" ] | |
} | |
mutate { | |
gsub => [ | |
# Prepare Payload CommandInvocation parsing | |
"PayLoadInvocation", "CommandInvocation\(.*\)", "CommandInvocation", | |
"PayLoadInvocation", "ParameterBinding.*\r\n", "", | |
# Prepare Payload ParameterBinding parsing | |
"PayLoadParams", "CommandInvocation.*\r\n", "", | |
"PayLoadParams", "ParameterBinding\(\S+\): ", "|||SPLITMEHEHE|||", | |
# Remove any commandinvocation and parameterbinding and any other known fields/keys and leave a remaining Payload field | |
"Payload", "CommandInvocation.*\r\n", "", | |
"Payload", "ParameterBinding.*\r\n", "" | |
] | |
} | |
# Parse payload field for all CommandInvocations | |
kv { | |
source => "PayLoadInvocation" | |
field_split => "\n" | |
value_split => ":" | |
allow_duplicate_values => false | |
target => "[ps]" | |
include_keys => [ "CommandInvocation" ] | |
} | |
ruby { | |
code => " | |
params_split = event.get('PayLoadParams').split('|||SPLITMEHEHE|||') | |
params_split = params_split.drop(1) | |
params_split_length = params_split.length | |
all_names = Array.new | |
all_values = Array.new | |
all_values_non_alphanumeric = Array.new | |
for param in params_split | |
slice_and_dice = param.index('; value=') | |
name = param.slice(6..slice_and_dice-2) | |
value = param.slice(param.index('value=')..-1)[6..-1] | |
value = value.strip | |
value[0] = '' | |
value[-1] = '' | |
value_non_alphanumeric = value.gsub(/[A-Za-z0-9\s]+/i, '') | |
all_names.push(name) | |
all_values.push(value) | |
all_values_non_alphanumeric.push(value_non_alphanumeric) | |
end | |
all_names = all_names.uniq | |
all_values = all_values.uniq | |
event.set('[ps][param][name]', all_names) | |
event.set('[ps][param][value]', all_values) | |
event.set('[ps][param][value_nonalphanumeric]', all_values_non_alphanumeric) | |
" | |
} | |
# Cleanup and Conversions | |
mutate { | |
# Normalize ContextInfo field names | |
rename => { | |
"CommandName" => "[ps][command][name]" | |
"CommandPath" => "[ps][command][path]" | |
"CommandType" => "[ps][command][type]" | |
"ConnectedUser" => "[ps][connected_user][full]" | |
"EngineVersion" => "[ps][version][full]" | |
"HostApplication" => "[ps][src][application]" | |
"HostID" => "[ps][src][host_id]" | |
"HostName" => "[ps][src][name]" | |
"HostVersion" => "[ps][src][version]" | |
"PipelineID" => "[ps][pipeline_id]" | |
"RunspaceID" => "[ps][runspace_id]" | |
"ScriptName" => "[file][name]" | |
"SequenceNumber" => "[ps][seq_num]" | |
"ShellID" => "[ps][src][id]" | |
"User" => "[ps][user][full]" | |
"[ps][CommandInvocation]" => "[ps][invocation]" | |
"Payload" => "[ps][remaining_payload]" | |
} | |
# Remove unwanted fields | |
remove_field => [ | |
"Severity", | |
"EventType", | |
"Keywords", | |
"message", | |
"Message", | |
"Opcode", | |
"port", | |
"SeverityValue", | |
"SourceModuleName", | |
"SourceModuleType", | |
"Version", | |
"ContextInfo", | |
"PayLoadInvocation", | |
"PayLoadParams" | |
] | |
# Set correct value types | |
convert => { "[ps][pipeline_id]" => "integer" } | |
convert => { "[ps][seq_num]" => "integer" } | |
lowercase => [ | |
"[ps][command][name]", | |
"[ps][command][type]", | |
"[ps][src][application]", | |
"[ps][src][id]", | |
"[ps][src][name]", | |
"[ps][user][full]" | |
] | |
} | |
} | |
# EventID 4104 | |
else if [EventID] == 4104 { | |
# Sometimes ScriptBlockText will not be parsed from the Message field. When this happens the other parameters (appear) to also never be parsed (ie: ScriptBlockId etc) | |
# So check if ScriptBlockText exists and if it does not then we will want to parse the parameters from the Message field | |
if [ScriptBlockText] { | |
mutate { | |
remove_field => [ | |
"Message" | |
] | |
} | |
} | |
else { | |
# Lets use GSUB to make sure we can get things to split on / make it easier more efficient to split on | |
grok { | |
match => { | |
"Message" => "^Creating Scriptblock text \(%{INT:MessageNumber} of %{INT:MessageTotal}\):\r\n%{GREEDYDATA:ScriptBlockText}\r\n\r\nScriptBlock ID: %{UUID:ScriptBlockId}\r\nPath: %{DATA:Path}$" | |
} | |
break_on_match => true | |
keep_empty_captures => false | |
named_captures_only => true | |
tag_on_failure => [ "_grokparsefailure", "_parsefailure" ] | |
tag_on_timeout => "_groktimeout" | |
# Timeout 1.5 seconds | |
timeout_millis => 1500 | |
remove_field => [ "Message" ] | |
} | |
} | |
mutate { | |
rename => { | |
"Path" => "[file][name]" | |
"ScriptBlockText" => "[ps][script_block][text]" | |
"ScriptBlockId" => "[ps][script_block][id]" | |
"MessageNumber" => "[ps][script_block][msg_num]" | |
"MessageTotal" => "[ps][script_block][msg_total]" | |
} | |
copy => { "Domain" => "[src_user][domain]"} | |
} | |
# Fingerprint the Script Block Text ---- useful for finding reoccuring scripts we want to exclude | |
fingerprint { | |
source => [ "[ps][script_block][text]" ] | |
method => "SHA1" | |
target => "[@meta][fp][ps][script_block][sha1]" | |
key => "logstash" | |
} | |
# Fingerprint Script Block Text and UserID ---- because sometimes certain accounts should not run certain scripts, so filtering just Script Block Text could be a problem. Also, don't want to use AccountName because a local user with $X name could have same name as a domain user! | |
fingerprint { | |
source => [ "[ps][script_block][text]", "UserID" ] | |
concatenate_sources => true | |
method => "SHA1" | |
target => "[@meta][fp][ps][script_block_and_sid][sha1]" | |
key => "logstash" | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment