Jump to content

Powershell 2: If [piped string] does not contain X, drop. If it does, pipe out.


Recommended Posts

Posted (edited)

Furthering my quest to automate my many repeated tasks, I'm trying to write a script to query the event log of remote machines and pull back a list of users that have logged on (mainly because the machine has been vandalised and the staff like to know the last few people to use it).. I've managed to narrow it down pretty well, it only pulls back Logon events, exactly as I tell it to. Thing is, it pulls back 'non-logon' logon events, mainly those caused by Services, such as our AV.

 

So, what I need to do is now tell it 'If the Message does not contain the word 'winlogon', ignore the entire thing.', because the events for the AV logon, for example, come from the AV exe and not the winlogon exe. This should, in theory, make my script return only the actual user logons. However, after trying for a considerable amount of time, I'm pretty stumped. I've gotten this far with intuition and google-fu, but now even those are letting me down :(

 

For those curious, my code is as follows:

[color="#4B0082"]$a[/color] [color="#FF0000"]=[/color] [color="#008080"]read-host[/color] [color="#A52A2A"]"Computername?"[/color]
[color="#008000"]# Set name of remote machine to query.[/color]
[color="#4B0082"]$b[/color] [color="#FF0000"]=[/color] [color="#008080"]Read-Host[/color] [color="#A52A2A"]"MaxEvents? (Default: 3)"[/color]
[color="#0000FF"]if[/color] ($b [color="#FF0000"]-eq[/color] [color="#A52A2A"]""[/color]) {([color="#4B0082"]$b[/color] [color="#FF0000"]=[/color] 3)}
[color="#008000"]# Set amount of logs to return. If null, set to 3.[/color]

[color="#008080"]Get-WinEvent -ComputerName[/color] [color="#A52A2A"]"$a.domain.local"[/color] [color="#008080"]-FilterHashtable[/color] @{ProviderName[color="#FF0000"]=[/color][color="#A52A2A"]"Microsoft-Windows-Security-Auditing"[/color]; ID[color="#FF0000"]=[/color][color="#A52A2A"]"4624"[/color]} [color="#008080"]-MaxEvents[/color] [color="#4B0082"]$b[/color] | [color="#008080"]Select[/color] [color="#A52A2A"]Message[/color] | [color="#008080"]ForEach-Object[/color] {[color="#4B0082"]$_[/color] [color="#FF0000"]-replace[/color] [color="#A52A2A"]"(?s)Detailed Authentication .*? session key was requested.}"[/color], [color="#A52A2A"]"------------------------------`r`n"[/color]} | [color="#008080"]Format-Table -Wrap -Auto[/color] | [color="#008080"]Out-File[/color] [color="#A52A2A"]"M:\$aLog.txt"[/color]
[color="#008000"]# Query eventlog of $a, return $b instances of Microsoft Windows Security Auditing logs with ID of 4624 (Logon). Strip out unnecessary description, replace with separator. Save to file.[/color]
[color="#008080"]Start-Process[/color] [color="#A52A2A"]"M:\$aLog.txt"[/color] [color="#008080"]-wait[/color]
[color="#008000"]# Open file. Wait for file to close.[/color]
[color="#008080"]Remove-Item[/color] [color="#A52A2A"]"M:\$aLog.txt"[/color]
[color="#008000"]# On file close, delete file.[/color]

 

Edit: Point.. Has to be ran in Powershell 2.0, not 3.0. Powershell 3.0 always returns a null 'Message' column, a bug complained about by a lot of people online.

Edited by Garacesh
Posted (edited)

I think this will do what you want, though i'm no powershell guru myself so it's probably not the best way!

 

# Set computer name and add domain suffix
$a = read-host "Computername?"
$a += ".domain.local"

# Set amount of logs to return. If null, set to 3.
$b = Read-Host "MaxEvents? (Default: 3)"
if ($b -eq "") {($b = 3)}

# Set Logfile Location
$logFile = "M:\" + $a + "Log.txt"

# Get Logon Events and create array of messages
$messages = @(Get-WinEvent -ComputerName "$a" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $b | % {($_.message)})

# Loop through messages and find ones with logon type 2 (interactive logon)
foreach ($message in $messages){
   if ($message.contains("Logon Type:			2")){ 
       # Replace unwanted parts of the message and output the message to the logfile
       $message -replace "(?s)(Detailed)(.*)", "------------------------------`r`n" >> $logfile
   }
}

# Open file. Wait for file to close.
Start-Process $logFile -wait

# On file close, delete file.
Remove-Item $logFile

Edited by pleach85
Edited to reflect changes made in comment below
Posted (edited)

Aha! Certainly a step in the right direction. Thank you very much!

 

So, since I'd like to understand it rather than simply use it.. Set hostname ($a) and append domain suffix (Didn't know you could do that, thanks!). I got that. Set amount of MaxEvents ($b) and if nul, set to 3. Got that.

 

I take it what happens next is the '$messages' array is created from the output of the Get-WinEvent cmdlet, with the "% {($_.message)})" categorising them each as 'message'?

'foreach ($message in $messages)' then checks each message (as defined by % {($_.message)})) in $messages for Logon Type: 2 and if it finds it, removes the unwanted bits and creates the log file (as specified via $logfile)

 

However, due to '>' being Out-File (didn't know about that, nor % being ForEach-Object, so thanks!) it only gives me the last log if finds that meets the criteria, and not all the ones before it, because Out-File overwrites. Should I give it a large number or events to check, say, 100, it ought to return quite a few login records, but it doesn't.. So I tried changing > to '| Add-Content' and that gives me a lovely string of chinese-looking text, hardly ideal. Still, a definite step in the right direction.

 

Aha! Figured it out! By creating another array ($logs) from the output of 'foreach [...]' and then '$logs | Out-File' it collects them all, and then writes the file, giving me the full record! Woohoo!

 

A few adjustments have been made ("Logon Type: 2" changed to "C:\Windows\System32\winlogon.exe" as it was still returning irrelevant logs sometimes, and I've changed the $logfile = "M:\$aLog.txt" as it doesn't seem to like $aLog and simply creates the filed called .txt, so just 'Log.txt' will have to do) - not that I'm ungrateful. To learn, one has to adapt code rather than simply splice it.

 

[color="#008000"]# Set name of remote machine to query, add domain suffix.[/color]
[color="#4B0082"]$a[/color] [color="#FF0000"]=[/color] [color="#008080"]read-host[/color] [color="#800000"]"Computername?"[/color]
[color="#4B0082"]$a[/color] [color="#FF0000"]+=[/color] [color="#800000"]".domain.local"[/color]
[color="#008000"]# Set amount of logs to return. If null, set to 3.[/color]
[color="#4B0082"]$b[/color] [color="#FF0000"]=[/color] [color="#008080"]Read-Host[/color] [color="#800000"]"MaxEvents? (Default: 3)"[/color]
[color="#0000FF"]if[/color] ([color="#4B0082"]$b[/color] [color="#FF0000"]-eq[/color] [color="#800000"]""[/color]) {([color="#4B0082"]$b[/color] [color="#FF0000"]=[/color] 3)}

[color="#008000"]# Query eventlog of $a, return $b instances of Microsoft Windows Security Auditing logs with ID of 4624 (Logon). Create array.[/color]
[color="#4B0082"]$messages[/color] = @([color="#008080"]Get-WinEvent -ComputerName[/color] [color="#800000"]$a[/color] [color="#008080"]-FilterHashtable[/color] @{ProviderName=[color="#800000"]"Microsoft-Windows-Security-Auditing"[/color]; ID=[color="#800000"]"4624"[/color]} [color="#008080"]-MaxEvents[/color] [color="#4B0082"]$b[/color] | [color="#008080"]%[/color] {([color="#4B0082"]$_[/color].message)})

[color="#008000"]# Loop through messages and find ones containing C:\...\winlogon.exe. Build new array.[/color]
[color="#4B0082"]$logs[/color] = [color="#0000FF"]foreach[/color] ([color="#4B0082"]$message[/color] [color="#0000FF"]in[/color] [color="#4B0082"]$messages[/color]){
   [color="#0000FF"]if[/color] ([color="#4B0082"]$message[/color].contains([color="#800000"]"C:\Windows\System32\winlogon.exe"[/color])){ 
       [color="#008000"]# Replace unwanted parts of the message and output the message to the logfile[/color]
       [color="#4B0082"]$message[/color] [color="#FF0000"]-replace[/color] [color="#800000"]"(?s)(Detailed)(.*)"[/color], [color="#800000"]"------------------------------`r`n`r`n`r`n------------------------------"[/color]
   }
}

[color="#008000"]# Write logs to file[/color]
[color="#4B0082"]$logs[/color] | [color="#008080"]Out-File[/color] [color="#800000"]"M:\Log.txt"[/color]
[color="#008000"]# Open file. Wait for file to close.[/color]
[color="#008080"]Start-Process[/color] [color="#800000"]"M:\Log.txt"[/color] [color="#008080"]-wait[/color]
[color="#008000"]# On file close, delete both files.[/color]
[color="#008080"]Remove-Item[/color] [color="#800000"]"M:\Log.txt"[/color] 

Edited by Garacesh
Posted

I agree, its how I learn most of my coding!

 

Sounds like you've understood it perfectly, I've just made a few mistakes :doh: if you add >> instead of > that will append to the txt file instead of overwrite it. Also for the log file (didn't even notice it wasn't creating the proper text file name) you could use

 

$logFile = "M:\" + $a + "Log.txt"

 

This will build a proper string for the log file path.

Posted

I take it what happens next is the '$messages' array is created from the output of the Get-WinEvent cmdlet, with the "% {($_.message)})" categorising them each as 'message'?

'foreach ($message in $messages)' then checks each message (as defined by % {($_.message)})) in $messages for Logon Type: 2 and if it finds it, removes the unwanted bits and creates the log file (as specified via $logfile)

 

That's not exactly how it works... the messages array is created from the output of the Get-WinEvent cmdlet but it is not categorising them each as 'message' the % {($_.message)}) means foreach item passed into the pipe get the "message" part of the Get-WinEvent object.

 

foreach ($message in $messages) could have easily have been foreach ($item in $messages) you are just creating a variable name for the value in the array.

Posted

Yeah, I figured similar when I attempted to tidy things up a bit more.. $a became $cn, $fqdn was created as $cn+domain (to keep the domain extension out of the log file name), $b became $me..

 

However, When I changed the $_.message to $_.event and $messages to $logs (since I no longer needed the array $logs) the script broke. Seemed like it didn't want to accept different names.. I figured this might have been because the contents of the event log are referred to as 'Message' by Get-WinEvent so it had to be fixed (since there's no Select Message or similar in there), but once I'd changed them back, it worked again.

Posted (edited)

Sorry to be a nag - I'm trying to make it include the TimeCreated as well as the Message, but hitting a stump, since TimeCreated won't contain any filterable information like Message does.

 

Is there a way I could link each instance of TimeCreated to Message before filtering them out (by the events that contain C:\...\Winlogon.exe)?

 

$messages = @(Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $me | % {($_.message)})
$times = @(Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $me | % {($_.timecreated)})
$fulllog = $messages + $times

 

If I've gotten that right, that would end up with an array ($fulllog) of: Login; Login; Login; Timestamp; Timestamp; Timestamp; (x150) rather than Login+Timestamp; Login+Timestamp.

I'm guessing I'd need to use some kind of foreach ($message in $messages) {combine with timecreated entry)..?

Edited by Garacesh
Posted

Rather than extracting them both separately and trying to merge them back together you'll be better off making an array of events then using a foreach to extract all the information you want from that event something like this...

 


$events = @(Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $me)

foreach ($event in $events){

   #You can do all the manipulation of the event here using things like
   # if ($event.message -contains "winlogon.exe"){ DO THIS}

   $fulllog += "----------" + $event.timecreated + "----------`r`n"
   $fulllog += $event.message + "`r`n`r`n"
}

 

This way you're only running the Get-WinEvent once and you are able to extract and modify all the information you want from the event then add it to your log in the foreach loop.

 

Hope this helps!

Posted

Ohh.. So if multiple (I don't really know what to call them, so I'll call them) 'categories' of data are written into an array, they're carried into the array by their said category? (In this case, TimeCreated, Message, etc).. They keep that info?

 

Good thing I don't have to calculate them both separately, that would've doubled the time the script takes! I'll play around with what you've given me and get back to you. Thanks! :D

Posted
Yeah, an array is a collection of items. The items in your array ($events) will be WinEvent-Objects as that's what you are putting in. Its these WinEvent objects that have all the information you need in them (TimeCreated, Messages etc). You can put any object into an array (string, int, another array, ...) it just so happens you're putting WinEvents in which contain alot of information you can extract later in your script. :)
Posted

Okay, I think I've gotten the theory of it.. But I seem to have gotten myself into a position where $FullLog is counting it all as one item, rather than multiple items of TimeCreated + Message and it's only pulling back one log..

 

# Query eventlog of $fqdn, return $me instances of Microsoft Windows Security Auditing logs with ID of 4624 (Logon). Create array.
$events = @(Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $me)

# Combine each message with its relevant timestamp
foreach ($event in $events){
   $fulllogs += "----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n"
}

$fulllogs | Out-File M:\TESTFullLog1.txt

# Loop through messages and find ones containing C:\...\winlogon.exe. Build new array.
$logins = foreach ($fulllog in $fulllogs) {
   if ($fulllog.contains("C:\Windows\System32\winlogon.exe")){ 
       # Replace unwanted parts of the message and output the message to the logfile
       $fulllog -replace "(?s)(Detailed)(.*) key was requested.", "------------ End of event log ------------`r`n`r`n" 
	$count = ([int]$count + 1)
   }
}

$fulllogs | Out-File M:\TESTFullLog2.txt

# Write 
"$count valid records within $me queried logon events. `r`n`r`n$logins" | Out-File $logFile

 

Currently returns:

 

1 valid records within 150 queried logon events. 

----------09/19/2013 11:33:35----------
An account was successfully logged on.

Subject:
Security ID:		S-1-5-18
Account Name:		BL07-002$
Account Domain:		SMSC
Logon ID:		0x3e7

Logon Type:			5

New Logon:
Security ID:		S-1-5-21-2913182392-3483331520-2670013734-1004
Account Name:		SophosSAUBL07-0020
Account Domain:		BL07-002
Logon ID:		0x25f6157
Logon GUID:		{00000000-0000-0000-0000-000000000000}

Process Information:
Process ID:		0x2bc
Process Name:		C:\ProgramData\Sophos\AutoUpdate\Cache\sophos_autoupdate1.dir\ALUpdate.exe

Network Information:
Workstation Name:	BL07-002
Source Network Address:	-
Source Port:		-

------------ End of event log ------------

 

That log should have been filtered out, because it doesn't contain the string "C:\Windows\System32\winlogon.exe", which is part of the reason I'm working on this.. Now, I've tried changing the replace from 'Detailed (to) (end of entry)' to 'Detailed (to) key requested' because I thought it was deleting all of the entries after the first one because it was all one item, but that doesn't seem to be doing the trick either.

 

So, this is where M:\TESTFullLog1.txt and M:\TESTFullLog2.txt came in, one before and one after the filtering to ensure it was happening correctly, and right now they're both identical 579kb files, without the Detailed+ removed. So it's not working all of a sudden. But if it's not filtering them out properly, why am I getting one incorrect log and none of the rest?

 

GAAHHH, why do I do this to myself?

Posted

There is only one object in fulllogs, a massive string containing all the events. The line :

 

$fulllogs += "----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n"

 

Is just appending each time and message to that string. So your later code will find that that massive string does indeed contain "C:\Windows\System32\winlogon.exe".

Posted

Yeah, I figured that's what was happening D:

It formats them correctly, one timestamp, one message, one timestamp, one message.. but the fact that they're all one string doth complicate.

Posted

It's useful to look at the events. In the ISE

 

If you do something like :

$events | export-csv c:\temp\test.csv

 

You can then see each event object and it's properties.

 

$events | get-member will show you any public methods

 

All of which can be hehlpful in debugging and understanding what your code is actually doing.

Posted

Just filter them when you filter the events into that string.

 

foreach ($event in $events){

if ($event.message.contains("blah")) {

$fulllogs += "----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n"

}

}

Posted (edited)

I'm right in thinking that '$array +=' means "Add the data to this array" rather than "This is a definitive allocation of that array's contents" (which would remove all current contents)

Therefore doing it your suggested way, @pcstru, $fulllogs would still be one item rather than multiple, but that wouldn't actually matter because it would only contain the filtered logs?

 

Edit: Aha, half-working. It's only pulling back the winlogon events now, but still including the text I wanted stripping out. Whilst not strictly a requirement, it would be helpful.

Edit: Aha, got it!

foreach ($event in $events){
    if ($event.message.contains("C:\Windows\System32\winlogon.exe")){ 
 	# Combine each message with its relevant timestamp, strip out junk data.
    	$fulllogs += ([string]"----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n" -replace [string]"(?s)Detailed.*key was requested.", "---------- Event message end ----------`r`n`r`n")
	$count = ([int]$count + 1)
}
}

Edited by Garacesh
Posted
I'm right in thinking that '$array +=' means "Add the data to this array" rather than "This is a definitive allocation of that array's contents" (which would remove all current contents)

I think += mostly means add to/append. It rather depends what is being added to what as to how it will actually behave. Fulllog is a string so that's what will happen. events is a (pointer to) a collection of objects - not quite the same thing as an array (in that a collection of objects could be described as an array but an array is not a collection of objects!).

 

Therefore doing it your suggested way, @pcstru, $fulllogs would still be one item rather than multiple, but that wouldn't actually matter because it would only contain the filtered logs?

 

Yes. If that's what you want as an output.

Posted
I think += mostly means add to/append. It rather depends what is being added to what as to how it will actually behave. Fulllog is a string so that's what will happen. events is a (pointer to) a collection of objects - not quite the same thing as an array (in that a collection of objects could be described as an array but an array is not a collection of objects!).

Ah. Muhbad. Thanks for the clarification.

 

I was having a little difficulty but figured it out pretty much right as you posted (see 2 posts up). My log output is now timestamped. Woo!

Now.. Do I attempt a further 'upgrade' or be happy it works again, count my blessings, and leave it as it is? xD

Posted

See if you can follow this compaction of the main logic.


# Query eventlog of $fqdn, return $me instances of Microsoft Windows Security Auditing logs with ID of 4624 (Logon). Create array.
foreach ($event in Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624" } -MaxEvents $me  | where {$_.message.contains("C:\Windows\System32\winlogon.exe")}) {
  $log+=$event.message
}
$log | out-file c:\temp\temp.txt

Posted (edited)

Query event log of $fqdn for $me instances of logs created by Microsoft-Windows-Security-Auditing with the event ID of 4624. Check each ($_) 'message' as defined by Get-WinEvent's results, if it contains the string "C:\Windows\System32\winlogon.exe", add it to $log (since multiple events may match, += keeps all current data of $log intact). (if not, do nothing)

 

Write the contents of $log to C:\temp\temp.txt.

 

Edit: Which means I don't need to create $events, do I?

This ought to work? (time to test!)

{
(Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"} -MaxEvents $me) |
where ($_.message.contains("C:\Windows\System32\winlogon.exe")){ 
 	$fulllogs += ([string]"----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n" -replace [string]"(?s)Detailed.*key was requested.", "---------- Event message end ----------`r`n`r`n")
	$count = ([int]$count + 1)
}
}

 

Edit: It does not :( It echos (or is it prints? No, that's Python.. I think? I don't really know what the term is for Powershell) the command and immediately opens the txt file with zero results.

Edited by Garacesh
Posted

Edit: It does not :( It echos (or is it prints? No, that's Python.. I think? I don't really know what the term is for Powershell) the command and immediately opens the txt file with zero results.

 

You do need to iterate through the collection returned from the Get-WinEvent pipeline. Personally I think there is always a compromise between compactness and readability.

 

$fqdn="computer.domain.local"
$me=100

$count=0
$fulllog=""

# Query eventlog of $fqdn, return $me instances of Microsoft Windows Security Auditing logs with ID of 4624 (Logon). 
foreach ($event in Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624" } -MaxEvents $me  | where {$_.message.contains("C:\Windows\System32\winlogon.exe")}) {
  $fulllog += ([string]"----------" + $event.timecreated + "----------`r`n" + $event.message + "`r`n" -replace [string]"(?s)Detailed.*key was requested.", "---------- Event message end ----------`r`n`r`n")
  $count++   
}
Write-Host "$count events logged"
$fulllog | out-file c:\temp\temp.txt

Posted

@pcstru I was close! I just missed out the '$event in' and got some of my brackets mixed up.

@fiza Of course! It's working right now minus a few glitches, but I can throw it up right now if you'd like? Current problems are:

  • It needs to be run in Powershell v2 because Get-WinEvent doesn't return messages in Powershell 3
  • Timestamps are American (MM/DD/YYYY)
  • You may have to parse way more records than you actually want returning to offset for Service/lsass/other logons that aren't 'actual' logons. (In my case, 150 events brings back 1-4 'actual' logons)

Posted

To solve the last problem in your list you can use

 

Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624"; Data="C:\Windows\System32\winlogon.exe" } -MaxEvents $me 

 

instead of

 

Get-WinEvent -ComputerName "$fqdn" -FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing"; ID="4624" } -MaxEvents $me  | where {$_.message.contains("C:\Windows\System32\winlogon.exe")}

 

By having Data="C:\Windows\System32\winlogon.exe" in the -filterhashtable it will only return events with "C:\Windows\System32\winlogon.exe" in them, therefore the whole

 

| where {$_.message.contains("C:\Windows\System32\winlogon.exe")}

 

is no longer needed and $me can be the total number of proper logon events you want returning. Speeds up the code too as powershell does not need to parse each event looking for "C:\Windows\System32\winlogon.exe".

  • Thanks 1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now



×
×
  • Create New...