Use MS Graph and Sentinel to create dynamic Watchlists

This post is a follow up to my post about enriching Sentinel via MS Graph here and in response to the community post here – how do we create dynamic Watchlists of high value groups and their members. There are a couple of ways to do this, you can either use the Azure Sentinel Logic App or the Watchlist API. For this example we will use the Logic App. Let’s start with some test users and groups, for this example our test user 1 and 2 are in privileged group 1 and test user 2 are in privileged group 2.

First let’s start by retrieving the group ids from the MS Graph, we have to do this because Azure AD group names are not unique, but the object ids are. You will need an Azure AD app registration with sufficient privilege to read the directory, directory.read.all is more than enough but depending on what other tasks your app is doing, may be too much. As always, least privilege! Grab the client id, the tenant id and secret to protect your app. How you do your secrets management is up to you, I use an Azure Key Vault because Logic Apps has a native connector to it which uses Azure AD managed identity to authenticate itself.

First create our playbook/Logic App and set the trigger to recurrence, since this job we will probably want to run every few hours or daily or whatever suits you. If you are using an Azure Key Vault, give the Logic App a managed identity under the identity tab.

Then give the managed identity for your Logic App the ability to list & read secrets on your Key Vault by adding an access policy

First we need to call the MS Graph to get the group ids of our privileged groups, we can’t use the Azure AD Logic App connector yet because that requires the object ids, and we want to do something more dynamic that will pick up new groups for us automatically. Let’s use the Key Vault connector to get our client id, tenant id and secret, and we connect using the managed identity.

Next we POST to the MS Graph to get an access token

Add the secrets from your Key Vault, your tenantid goes into the URI, then clientid and secret into the body. We want to parse the JSON response to get our token for re-use. The schema for the response is

{
    "properties": {
        "access_token": {
            "type": "string"
        },
        "expires_in": {
            "type": "string"
        },
        "expires_on": {
            "type": "string"
        },
        "ext_expires_in": {
            "type": "string"
        },
        "not_before": {
            "type": "string"
        },
        "resource": {
            "type": "string"
        },
        "token_type": {
            "type": "string"
        }
    },
    "type": "object"
}

Now we use that token to call the MS Graph to get our object ids. The Logic App HTTP action is picky about URI syntax so sometimes you just have to add your URL to a variable and input it into the HTTP action, our query will search for any groups starting with ‘Sentinel Priv’ but you could search on whatever makes sense for you.

When building these Logic Apps, testing a run and checking the outputs is often valuable to make sure everything is working, if we trigger our app now we can see the output we are expecting

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups(displayName,id)",
  "value": [
    {
      "displayName": "Sentinel Privileged Group 1",
      "id": "54bb0603-7d02-40a1-874d-2dc26010c511"
    },
    {
      "displayName": "Sentinel Privileged Group 2",
      "id": "e5efe4a4-51e0-4ed7-96b5-9d77ffb7ab74"
    }
  ]
}

We will need to leverage the ids from this response, so parse the JSON using the following schema

{
    "properties": {
        "@@odata.context": {
            "type": "string"
        },
        "value": {
            "items": {
                "properties": {
                    "displayName": {
                        "type": "string"
                    },
                    "id": {
                        "type": "string"
                    }
                },
                "required": [
                    "displayName",
                    "id"
                ],
                "type": "object"
            },
            "type": "array"
        }
    },
    "type": "object"
}

Next we need to manually go create our Watchlist in Sentinel that we want to update. For this example we have created the PrivilegedUsersGroups watchlist using a little sample data

Finally we iterate through our groups ids we retrieved from the MS Graph, create a JSON payload and use the Add a new watchlist item Logic App, you will need to add your Sentinel workspace details in here of course.

When we run our Logic App, it should now insert the members to our watchlist, including their UPN, the group name and group id.

We can clean up the test data if we want, but now we can query on events related to those via our watchlist

let PrivilegedUsersGroups = (_GetWatchlist('PrivilegedUsersGroups') | project GroupName);
SecurityEvent
| where EventID in (4728, 4729, 4732, 4756, 4757)
| where TargetUserName in (PrivilegedUsersGroups)
| project Activity, TargetUserName

The one downside to using Watchlists in this way is that the Logic App cannot currently remove items, so when you run it each time it will add the same members again. It isn’t the end of the world though, you can just query the watchlist on its latest updated time, if your Logic App runs every four hours, then just query the last four hours of items.

_GetWatchlist('PrivilegedUsersGroups') | where LastUpdatedTimeUTC > ago (4h)

6 thoughts on “Use MS Graph and Sentinel to create dynamic Watchlists

  1. Hello Matthew,

    This is a superb article and a follow up to the community post we have been discussing on the matter.

    The outstanding question I have and admit I am struggling to apply is, how can I see if an account exists in the Watchlist and only gets updated IF it doesn’t exist?

    I see and understand you have suggested applying the query below:

    _GetWatchlist(‘PrivilegedUsersGroups’) | where LastUpdatedTimeUTC > ago (4h)

    Can that query actually be applied in the Logic App and specifically within the For…Loop (say at the member level) ?

    OR are you stating here that ‘duplicate’ sets of data will be added BUT you are querying ONLY the last set of ADDED items within the time interval? In this case 4 hours.

    The other consider I had (and not tested yet) was to ‘query’ inside the the For…Loop (member) with:

    _GetWatchlist(‘PrivilegedUsersGroups’)
    | extend AccountID = tostring(parse_json(WatchlistItem).UserPrincipalName)
    | where AccountID == “@{items(‘For_each’)?[‘UserPrincipalName’]}”

    …basically IF NOT found ADD to watchlist.

    Jason

    Like

    1. Hey Jason, thanks for the kind words – I saw your comment on the Tech Community post but haven’t yet had a chance to reply. 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. Hope that makes sense?

      Like

  2. Hi Matthew,

    I fully appreciate you conversing with me. It certainly helps me understand how to apply the more ‘advanced’ stuff in Sentinel, by just reading your posts. Keep them up because I can assure you they are benefitting a lot people in cyber security like me.

    As for your reply, yes I understand that mate.

    The Logic App would basically generate ‘record’ entries for object member object processed. Repeated items as you say BUT IF queried like you say (out the Logic App) on a time interval filter, would only ‘review’ the most current set of entries in the watchlist.

    I will go with this solution for the moment, as it is a suitable one for the client.

    IF you could reply to my Community Post, I would be extremely grateful though. More so that I could get your opinion and knowledge ‘know-how’ on it.

    Jason

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s