Counter number of times a user triggers failed logon in powershell - powershell

My plan is to create a script that will gather each previous day's worth of failed logins from the security log. I want a way to count the number of times the same user triggers an alert since the end goal is to build a simple web site that displays the username, the number of failed logons they triggered and the name of the server where the alert was triggered. I have the start below:
#get a list of all disabled user email address and store it in
disabledUsers variable
$disabledUsers = Get-ADUser -filter * -SearchBase
"OU=Users,OU=Disabled,OU=COMPANY,DC=example,DC=local" -Properties mail | select -expandproperty mail
#empty array that will store all the usernames parsed below
$userArray = #()
#iterate through each user account in the disabledUsers variable
foreach ($user in $disabledUsers){
#for every email address, split the value based at the # symbol and retrieve the first field/index and add that to the userArray array
$userArray += $user.Split("#")[0]}
#sets the previous day's time and date
$yesterday = (Get-Date).AddDays(-1)
#get all failed login attempts since yesterday and extract the time of the error, server where error was logged, and user account that triggered the error
$failedLogins = get-eventlog -LogName Security -After $yesterday -EntryType FailureAudit -InstanceId 4625 | select timewritten,machinename,#{n='account';e={$_.replacementstrings[5]}}
#iterate through each entry in the failedLogins variable
foreach ($failedLogin in $failedLogins) {
<# define a counter variable to increment the check between the username in each failed event log against each
username defined in the userArray array. Loop keeps checking until $i is greater than or equal to the number of
entries in the userArray array
#>
for ($i=0; $i -lt $userArray.Length; $i++) {
#everytime $i is not equal or greater than # of counts in userArray, check and see if the
#username in the event log matches the current username in the userArray index (represented by the $i)
if ($failedLogins.account -match $userArray[$i]) {
#if there is a match......
} else {
}
}
}
I'm not sure how best to build this counter. In case anyone is wondering, the use case for this is because we started noticing a bunch of failed login attempts from a disabled user account about once every 1-2mins. Trying to build a way to monitor this.

Related

Powershell - Test if user is part of any Groups

I'm having trouble getting a small PS script to work the way I want it to.
Powershell version: 5.1
Ultimate goal: I want to parse all existing local User accounts and generate a list of those that are not part of any local Groups. (This is not an AD environment, nor part of a domain. This is all just local OS accounts and Groups.) Also, I am only interested in the Groups that I have manually created - I don't care about the default System Groups. (More on how I try to achieve this, later.) And then, after I get this part working, this output (list of User names) will then be used (in the future - not shown in this code) as input to another block that will add these not-in-a-Group users to a Group.
I found an example bit of code - which worked, and resulted in the correct set of user names. But it was slow - it took >5 minutes. So I wanted something faster.
The approach that I'm working with now generally seems like it will work, and is pretty quick. I'm just having trouble getting it to restrict things down correctly. It seems I'm having trouble referencing properties of objects returned by the cmdlets. (See code a bit further down. There are various counters and write-host steps in here, too, that are purely for helping me see what's going on and debugging - they aren't required for the actual task.)
My general outline of how I am going about this:
Compile a list of all Users in all Groups
But, I only want Groups that have no Description - these are the ones that I have created on the machine. The OS default Groups all have a Description. This will help me narrow down my list of Users.
Loop over the list of all Users on the system
Compare each user to the List from Step 1
If User is on the List, then that User is IN a Group, so skip it
If User is not on the List, then save/report that Name back
[Side note - the user 'WDAGUtilityAccount' turned out to also not be in any Groups, but I don't want to change that one; so have a specific test to exclude it from the list.]
[original version of code]
# --- Part 1
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
# Only want Groups that don't have a Description
if ( $null -eq $aGroup.Description ) {
Get-LocalGroupMember $aGroup.name
}
write-host "$groupCount -- $aGroup.Name _ $aGroup.Description"
}
# Just for debugging - to see the content of Part 1
write-host "List = $UsersList"
# ---- Part 2
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $UsersList.Name -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
It appears that my attempts to extract the Properties of the Group in Part 1 are failing - I am getting literal text 'Name' and 'Description', instead of the aGroup.Name aGroup.Description property values. So my output looks like "MyGroup1.Name" instead of "MyGroup1" (assuming the actual name of the group is 'MyGroup1').
I believe that I am approaching the 'Group' object correctly - when I do 'get-localgroup | get-member" it says that it is a 'LocalGroup' object, with various properties (Name and Description being two of those).
There may be many other approaches to this - I'm interested in hearing general ideas; but I would also like to know the specific issues with my current code - it's a learning exercise. :)
Thanks, J
[ Version 2 of code - after some suggestions... ]
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
#Write-Host $aGroup.Description
# Only want Groups that don't have a Description (i.e. are the Groups that we created, not the default System/OS groups)
if ( $null -eq $($aGroup.Description) ) {
$UserList += Get-LocalGroupMember $($aGroup.name)
write-host "$groupCount -- $($aGroup.Name) _ $($aGroup.Description)"
}
}
write-host "List = $UsersList"
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $($UsersList.name) -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
I believe this could be reduced to:
Store all members of a any group where the group's Description is null.
Get all local users and filter where their user's Name is not equal to WDAGUtilityAccount and they are not part of the stored member's SID array.
$members = Get-LocalGroup | Where-Object { -not $_.Description } | Get-LocalGroupMember
Get-LocalUser | Where-Object {
$_.Name -ne 'WDAGUtilityAccount' -and $_.SID -notin $members.SID
} | Format-Table -AutoSize

Multiple strings search in consecutive lines in a log file

I have a log file Input.log which records failed and successful login attempts made by different users and it keeps updating in real time. I am interested only in failed login attempt made by one user i.e. master. Whenever there is a failed login attempt by user master, following 3 fixed text strings will always come in 3 consecutive lines as shown below in sample Input.log file:
Authenticating the user "master" with the password
Failed to logon to the system due to something unexpected
SSCMPE-00102: Failed to authenticate user. Invalid credentials. Enter valid credentials
Input.log file sample for a failed login attempt by master:
[2021-05-14T04:18:41.378-06:00] [FoundationServices0] [NOTIFICATION] [01216] [oracle.bi.bifndnepm.bpmui.logon.CSSAuthenticate] [tid: 30] [userId: <anonymous>] [ecid: 00j8DrPuyNGB1FwDwFj8CW0001hC0004FL,0:1] [APP: WORKSPACE#11.1.2.0] [SRC_CLASS: com.hyperion.bpm.logon.CSSAuthenticate] [SRC_METHOD: authenticateUser:473] Authenticating the user "master" with the password "*********".
[2021-05-14T04:18:41.573-06:00] [FoundationServices0] [ERROR] [02601] [oracle.bi.bifndnepm.bpmui.logon.LogonServlet] [tid: 30] [userId: <anonymous>] [ecid: 00j8DrPuyNGB1FwDwFj8CW0001hC0004FL,0:1] [APP: WORKSPACE#11.1.2.0] [SRC_CLASS: com.hyperion.bpm.logon.LogonServlet] [SRC_METHOD: writeLogonCssException:206] Failed to logon to the system due to something unexpected.[[
SSCMPE-00102: Failed to authenticate user. Invalid credentials. Enter valid credentials.
at com.hyperion.css.store.identity.IdentityStoreImpl.authenticate(IdentityStoreImpl.java:1845)
at com.hyperion.css.spi.impl.nvdb.NativeProvider.authenticate(NativeProvider.java:74)
at com.hyperion.css.facade.impl.CSSAbstractAuthenticator.authenticateUser(CSSAbstractAuthenticator.java:645)
at com.hyperion.css.facade.impl.CSSAPIAuthenticationImpl.authenticate(CSSAPIAuthenticationImpl.java:69)
I want to create a monitoring script sothat as soon as we have these 3 text strings appeared in 3 consecutive lines, I should get an email alert about the failed login attempt made by user master.
I will schedule the script to run in Windows task scheduler. I'd like to make the script run continuously to detect the failed login attempts in real time. So it should read only freshly written entries in Input.log file from the previous run of the script.
So far I have below code that, in failed.log, gives me all the lines matching above three strings coming consecutively in three lines (what I actually want) but also many other unwanted lines matching the three strings individually in different lines (which I don't want).
$File = "C:\data\Input.log"
$EmailParam=#{
To='usergroup#domain.com'
From='user#domain.com'
SmtpServer='smtp.serveraddress.com'
Subject='Failed Login Attempt by the user Master'
Body='Alert! Failed Login Attempt found for the user Master'
Attachment='failed.log'
}
$String='Authenticating the user "master" with the password','Failed to logon to the system due to something unexpected','SSCMPE-00102: Failed to authenticate user. Invalid credentials. Enter valid credentials'
Get-Content $File | Select-string -Pattern $String | Set-Content failed.log | ForEach {
Send-MailMessage #EmailParam
}
Would appreciate if you could guide me to fix it. Thanks!
Use Get-Content's -Wait switch to keep waiting - indefinitely - for new lines to be added to the log file.
Maintain state via a ForEach-Object call in which you can use -match, the regular-expression matching operator to examine the lines.
$matchingLineCount = 0
$matchingLines = #()
Get-Content -Wait $File | ForEach-Object {
if ($matchingLineCount -eq 2) { # 3rd line -> send email.
$matchingLines += $_
$matchingLines | Set-Content failed.log
Send-MailMessage #EmailParam
}
elseif ($matchingLineCount -eq 0 -and $_ -match 'Authenticating the user "master"') {
$matchingLines += $_
++$matchingLineCount
return
} elseif ($matchingLineCount -eq 1 -and $_ -match 'Failed to logon to the system due to something unexpected') {
$matchingLines += $_
++$matchingLineCount
return
}
$matchingLineCount = 0; $matchingLines = #()
}
Note:
Given that only 3 lines must be collected, I'm defining $matchingLines as a regular array (#()) and using += to "append" lines to it, for brevity and convenience.
However, in general this approach to iteratively building a collection is inefficient, because every += causes a new array to be created behind the scenes - of necessity, given that arrays are immutable with respect to their element count - see this answer for more information.
GitHub issue #5643 discusses switching PowerShell's default collection data structure from non-extensible arrays to an extensible collection type.
a possible solution: the regex pattern looks at 3 consecutives lines
$pat1 = 'Authenticating the user "master" with the password'
$pat2 = 'Failed to logon to the system due to something unexpected'
$pat2 = 'SSCMPE-00102: Failed to authenticate user. Invalid credentials. Enter valid credentials'
$pat = $pat1 + ".*?`r?`n.*?" + $pat2 + ".*?`r?`n.*?" + $pat3
$path = "C:\Users\ThierryK\Documents\test.log"
$lines = Get-Content -Raw -Path $path
$option = [System.Text.RegularExpressions.RegexOptions]::Singleline
$pattern = [regex]::new($pat, $option)
$matches = $pattern.Matches($lines)
foreach($m in $matches){
$m.Groups[0].Value
#send your email
}
i suggest you to keep the time each time you scan the log to avoid to scan the same thing every time

Using powershell to list AD users and making an interactive menu to remove specific user

so what i want to do is use powershell to list all ADusers into a basic interactive menu so that a specific user can be chosen and removed.
This is what I got so far, and this all users and allows me to select a specific one. But the Remove-ADUser -identity $ouEntry (on line: 18) runs right after I start the script and selects all the users for removal before I can select a specific one. I need it to run after i select an option, and with the correct user. I have been looking into a switch menu, but with poor results since I cant embed the ForEach properly.
Appreciate all help. I'm also open to alternate solutions
Clear-Host
$ouCounter = 1
$MenuArray = #()
$DomainName = ($env:USERDNSDOMAIN).split('.')[0]
$Tld = ($env:USERDNSDOMAIN).split('.')[1]
Write-Host "`nChoose the user you want to delete"
foreach ($ouEntry in ((Get-ADUser -SearchBase "DC=$DomainName,DC=$Tld" -Filter *).name))
{
$(" "+$ouCounter+".`t"+$ouEntry)
$ouCounter++
$MenuArray += $ouEntry + " was removed"
$MenuArray += Remove-ADUser -identity $ouEntry
}
do
{ [int]$menuSelection = Read-Host "`n Enter Option Number"}
until ([int]$menuSelection -le $ouCounter -1)
$MenuArray[ $menuSelection-1]
Output
Choose the user you want to delete
1. Administrator
2. Guest
3. user1
4. user2
5. user3
6. user4
7. user5
8. user6
9. Jon Snow
Enter Option Number:
Previous reference: Making a dynamic menu in Powershell
You might consider doing this in a Windows Forms GUI, so the list to choose from can have a scrollbar.
Having said that, the code you have now writes the entry on screen as menu item and immediately removes that user.
Below code first gets the users in an array once and creates a List object from that.
The reason for using a List object is because with that it is easy to remove an item (unlike with using an array).
$DomainName = ($env:USERDNSDOMAIN).Split('.')[0]
$Tld = ($env:USERDNSDOMAIN).Split('.')[1]
# get an array of users in the given OU, sorted on the Name property
$users = Get-ADUser -SearchBase "DC=$DomainName,DC=$Tld" -Filter * | Sort-Object Name
# store them in a List object for easy removal
$list = [System.Collections.Generic.List[object]]::new()
$list.AddRange($users)
# now start an endless loop for the menu handling
while ($true) {
Clear-Host
# loop through the users list and build the menu
Write-Host "`r`nChoose the user you want to delete. Press Q to quit." -ForegroundColor Yellow
$index = 1
$list | ForEach-Object { " {0}.`t{1}" -f $index++, $_.Name }
$menuSelection = Read-Host "`r`nEnter Option Number."
# if the user presses 'Q', exit the loop
if ($menuSelection -eq 'Q') { break }
# here remove the chosen user if a valid input was given
if ([int]::TryParse($menuSelection, [ref]$index)) {
if ($index -gt 0 -and $index -le $list.Count) {
$userToDelete = $list[$index - 1]
Write-Host "Removing user $($userToDelete.Name)" -ForegroundColor Cyan
Remove-ADUser -Identity $userToDelete.DistinguishedName
# remove the user from the list
$list.RemoveAt($index - 1)
}
}
# add a little pause and start over again
Start-Sleep -Seconds 1
}
Hope that helps

Hash Table - Sort in order of how they are input

I have a hash table here and I have it eventually outputting to an Excel spreadsheet, but the issue appears to be the way the system sorts the hash table by default. I want it to return the machines in the same order that they are inputted, they way it currently works is a box pops up and you paste in all your machine names so they are all in memory prior to the foreach loop. I was previously sorting this by the longest uptime but it now needs to be the same way they are inputted. My initial thought is to create another hash table and capture them in the same order versus the $machineList variable, but that might even leave me in the same position. I tried to search but I couldn't find info on the default way that hash tables sort.
Any ideas?
$machineUptime = #{}
foreach($machine in $machineList){
if(Test-Connection $machine -Count 1 -Quiet){
try{
$logonUser = #gets the logged on user
$systemUptime = #gets the wmi property for uptime
if($logonUser -eq $null){
$logonUser = "No Users Logged on"
}
$machineUptime[$machine] = "$systemUptime - $logonUser"
}
catch{
Write-Error $_
$machineUptime[$machine] = "Error retrieving uptime"
}
}
else{
$machineUptime[$machine] = "Offline"
}
}
Create $machineUptime as an ordered hashtable (provided you have PowerShell v3 or newer):
$machineUptime = [ordered]#{}

Search GC replicas and find AD account

I have an issue which is to do with AD replication. We use a 3rd party app the create accounts in AD and then a powershell script (called by the app) to create the exchange accounts.
In the 3rd party app we can not tell which GC the ad account has been created on and therefore have to wait 20 minutes for replication to happen.
What I am trying to do is find which GC the account has been created on or is replicated to and connect to that server using....
set-adserversettings -preferredserver $ADserver
I currently have the below script and what I can't work out is to get it to stop when it finds the account and assign that GC to the $ADserver variable. The write-host line is only there for testing.
$ForestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$GCs = $ForestInfo.FindAllGlobalCatalogs()
Import-module activedirectory
ForEach ($GC in $GCs)
{
Write-Host $GC.Name
Get-aduser $ADUser
}
TIA
Andy
You can check whether Get-ADUser returns more than 0 objects to determine whether the GC satisfied your query. After that, use Set-ADServerSettings -PreferredGlobalCatalog to configure the preference
You will need to specify that you want to search the Global Catalog and not just the local directory. The Global Catalog is accessible from port 3268 on the DC, so it becomes something like:
$ForestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$GCs = $ForestInfo.FindAllGlobalCatalogs()
Import-module ActiveDirectory
$ADUserName = "someusername"
$ADDomainDN = "DC=child,DC=domain,DC=tld"
$FinalGlobalCatalog = $null
foreach ($GC in $GCs)
{
$GCendpoint = "{0}:3268" -f $GC.Name
$SearchResult = Get-ADUser -LDAPFilter "(&(samaccountname=$ADUserName))" -Server $GCEndpoint -SearchBase $ADDomainDN -ErrorAction SilentlyContinue
if(#($SearchResult).Count -gt 0){
$FinalGlobalCatalog = $GC
break
}
}
if($FinalGlobalCatalog){
Write-Host "Found one: $($FinalGlobalCatalog.Name)"
Set-ADServerSettings -PreferredGlobalCatalog $FinalGlobalCatalog.Name
} else {
Write-Host "Unable to locate GC replica containing user $ADUserName"
}