RE: How do you identify 'non-privileged' users...

Occasional Contributor

Looking to generate a KQL query or Analytics rule to identify 'Multiple failed user logon attempts' from Windows PCs only and the user is classified as 'non-privileged'.


Just looking for the most effective way to define a 'non-privileged' user from either security alerts/events and/or AAD related logs.


Any best practice advice welcomed.

12 Replies
So most of your log sources like Security Events / AAD sign on logs aren't going to be able to tell you if a user is privileged or not, because that is going to be pretty unique to your environment. There are some products like Defender for Identity that can do some of the work for you for sure, but really you are going to have to feed that information in to Sentinel yourself because it will be unique to your company. There are a few posts around about adding AAD Info to Sentinel - there is a great blog post here and a shameless promotion here for my blog -

Is there a reason why you want to identify non-privileged users instead of the opposite? I would guess your list of privileged users is smaller. In that case you may want to enrich Sentinel with the info about your privileged users, then you can exclude them from your queries.

Hello @m_zorich.


Thank you very much for the reply. It is very much appreciated.


The reason for searching for 'non-privileged' status is to identify those logging onto resources that are short of permissions or privilege. Also the list of privileged users is smaller.


Ideally I want to review the EventID == 4625 from SecurityEvent where I can then 'join' or 'union' those failed user logons (account) against a(n) user (account record) that can identify if they are a privileged user or not.


So for a user (account) entity is there a property that can 'state' if a user has privileged status, OR if  they are NOT a member of a privileged access group, OR a member added to a security enabled group (EventIDs in "4728", "4732", or "4756") ?


So a simple starting point for failed logons being:


| where TimeGenerated >= ago(1d)
| where EventID == 4625
| summarize FailedLogons=count() by Account, Computer
| sort by FailedLogons desc


I will of course look at your links, but any further feedback to the questions I ask, would be very much welcomed.



Sure I get your use case a little more now, so you will still need to enrich Sentinel with the right information about what users & groups are privileged, a 4625 event for a non privileged vs privileged user is going to look the same. If you have a list of users who are privileged, you could add them to a Watchlist - Then you use that Watchlist as part of your query, and for your example you want results that aren't in the Watchlist. If you create a Watchlist called PrivilegedUsers with a column named Account, then a list of accounts, your query would look like -

let PrivilegedUsers = (_GetWatchlist('PrivilegedUsers') | project Account);
| where TimeGenerated >= ago(1d)
| where EventID == 4625
| where Account !in (PrivilegedUsers)
| summarize FailedLogons=count() by Account, Computer
| sort by FailedLogons desc

You could do the same for group membership changes, create a Watchlist called PrivilegedGroups with TargetAccount as the column header, then the list of names of the groups you want to monitor, then your query will be

let PrivilegedGroups = (_GetWatchlist('PrivilegedGroups') | project TargetAccount);
| where EventID in (4728, 4732, 4756)
| where TargetAccount in (PrivilegedGroups)

You can combine the two as well, so something like

let PrivilegedGroups = (_GetWatchlist('PrivilegedGroups') | project TargetAccount);
let PrivilegedUsers = (_GetWatchlist('PrivilegedUsers') | project Account);
| where EventID in (4728, 4732, 4756)
| where TargetAccount in (PrivilegedGroups) and Account !in (PrivilegedUsers)

Would give you a non privileged user being added to a privileged group

Instead of watchlists the alternate would be ingest this info as a custom table, PrivilegedGroups_CL and PrivilegedUsers_CL (see the examples in the last post), then you could join & lookup that way. The key will be keeping those lists current.
Hello @m_zorich.

Thank you for the great response.

Watchlists I understand. Definitely a possibility but the key will be is to understand how to make them dynamic. What I mean is, is there a way to keep them up to date on regular basis, like a TIP (Threat Intelligent Platform/STIX/TAXII) feed?

I will look at the ingesting method from your previous post, so creating a custom table of privileged users. I will keep this post open, in case I run into any issues, if that is OK with you?

The end goal is run this query as an analytics rule that can trigger as an Incident. The trigger would activate a workbook/logic app that could disable the said 'non-privileged' account.

Yep sounds good, best of luck with it. The custom table essentially becomes some threat intelligence that is unique to your environment, it won't be in STIX/TAXII format of course but you will still be able to join/lookup against it. If you ingest it say daily to keep it accurate, then when you query that table just look at the last 24 hours of logs to make sure the data is current.
Hi again @m_zorich,

In regard to ingesting Azure AD information and specifically looking at users as an extension property to Azure Sentinel, are there any extension properties relating to if a user has privileged status or will using an array of sorts with known privileged AAD Group ID’s probably a better way of looking at this?

Using a watchlist is still my preferred solution.

The reason I ask, is because I still clarifying with the end client how a privileged user account is defined and where it is applied.

The only thing you can pull out of Azure AD that indicates privilege is probably membership of Azure AD roles? Over and above that you will need a list of privileged group ids, then you can feed the membership of those into a watchlist. Definitely need your clients help pointing you in the right direction if you don't know the environment well enough yourself.
Yes I will get clarification from the client on Thursday.

I have just been reading your blog item on this (link you provided). May I say that is excellent as well.

Just a couple of queries...

The Logic App you created was from within Sentinel itself and from the Automation 'navigation menu' item and then by creating a new playbook?

Also, you mention this could be done using a Watchlist; you mean creating a CSV of values based on the properties of: UserPrincipalName and AADObjectID?
Yep you can create a Logic App via the automation menu item and new playbook.

Also rather than writing an essay for you - I put together an example for you of accessing MS graph, searching for groups based on name, then adding members to a Watchlist, hope it helps -

Hi again @m_zorich,


After speaking to the client, due to where 'privileged' users are located, and to simplify the solution a little (no reference to MS Graph for the moment), an array with known privileged access groups will be used.


The array will contain each AAD Object Group's ID, that will be 'looped' through to obtain 'member' data.


So the 'initial' Logic App design includes the following (with highlighted area still to be resolved; struggling with):


* Apply Recurrence 'pattern'
* Initialise Variable GroupIDs (array type)
* Add GroupIDs to 'array'
* Parse JSON (of GroupID)
* Of Body returned, FIRST For Each (Return 'Group Members')


* For Each (Group Member)


* //HOW IS IT BEST TO QUERY ON THIS? - Account below represents Group Member
* ?// _GetWatchlist('PrivilegedUsers')
* ?// | extend AccountID = tostring(parse_json(WatchlistItem).AccountID)
* ?// | where AccountID == "@{items('For_each')?['Account']}"
* //OR
* ?// let PrivilegedUsers = (_GetWatchlist('PrivilegedUsers') | project AccountID);
* ?// | where Account !in (PrivilegedUsers)
* ?// | summarize AccountFound=count() by Account


* //Apply condition statement of:
* ?//If AccountID 'LENGTH' = 0
* //OR
* ?//If AccountFound 'COUNT' = 0


* Compose (input content for WATCHLIST; being AccountID : Account)
* Apply action: Watchlists - Add a new watchlist item


So the outstanding question I have is, how can I see if the account exists in the Watchlist and only add it if it doesn’t exist?


Of the watchlist, I initially pre-created it with only the 'HEADERS' and no items.



best response confirmed by JMSHW0420 (Occasional Contributor)
Hey Jason, I believe you could write the logic to query the Watchlist and only add new members but believe it would be easier to just load everyone each time, and then query your Watchlist on the same frequency as your Logic App. For example – if you add 10 members on the first run, then add 3 members and run it again 4 hours later, your Watchlist will have 23 total items (original 10, plus 13 – the original 10 again and the new 3), but if you query your Watchlist on items added in the last 4 hours it will only show 13. If you then remove 5 users and run it again 4 hours later, your watchlist will have 31 total items (original 10, then the 13 we added, then the 8 current members), but if you query your Watchlist on the last 4 hours then you will just see the current 8 members and it’s accurate.
I hope you don't mind me asking @m_zorich, but one other thing; of 'Non-Privileged' users RETURNED, is there a way to check through 'logs', the “threshold” for the multiple failed logon activity from an in alerting policy defined within MCAS?