Using Microsoft Graph and Powershell - Risk Detection Commands
Building on the last diary on Using MS Graph and Powershell, let's look at "Risky" logins.
Risky logins are a derived set of parameters that look at various (you guessed it) risky login parameters. What is considered a risk?
In most cases this is either impossible geography - in other words "we're not expecting to see you at that IP, in that subnet, ASN or country", or unusual device - ie "that's not your regular computer"
There are two groups of commands in this area. You can do Risk Detection in a basic Entra license, but to work with Persistent Risk User accounts you need to bump up your license. So it'll cost you every month to use these commands:
Get-MgRiskyUser
Confirm-MgRiskyUserCompromised
Get-MgRiskyUserHistory
However, you can get a fair way with a basic Entra license and the Get-MgRiskDetection command. Let's focus on just that, since we all have at least that license level (if you're still reading that is).
#first connect to graph with the right Identity Protection scopes
Connect-MgGraph -Scopes "IdentityRiskyUser.Read.All", "IdentityRiskEvent.Read.All"
$riskylogins = Get-MgRiskDetection -all
Note that if you've already done remediation and marked off events as dealt with, you can filter those events out with:
$riskylogins = Get-MgRiskDetection -All -Filter "riskState ne 'dismissed' and riskState ne 'remediated'"
Let's look at some data:
$riskylogins | select userdisplayname, activitydatetime, ipaddress, additionalinfo

hmm, that last field is the key one, it's in JSON format, with more info than we likely want for a summary. Let's look at one record, and convert from JSON:
$riskylogins[2].additionalinfo | convertfrom-json
Key Value
--- -----
riskReasons {UnfamiliarDevice, UnfamiliarEASId, UnfamiliarTenantIPsubnet}
userAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0 AnyConnect/5.1.9.113 (win)
alertUrl
mitreTechniques T1078.004
So most likely we'll want that list of risk reasons in our summary report - let's extract that for our test object:
($riskylogins[2].additionalinfo | convertfrom-json)[0].value
UnfamiliarDevice
UnfamiliarEASId
UnfamiliarTenantIPsubnet
OK, now let's pull the list with just that information, using our new best friend - yup, a computed field and a join!
$riskylogins | select userdisplayname, activitydatetime, ipaddress, @{N='Reason';E={ (($_.additionalinfo | convertfrom-json)[0].value ) -join '; '}} | out-gridview

In this case, looking deeper at the IP's, these are login attempts from Malaysia, Colombia and South Korea. Digging deeper into the text, we found a client IP from Warsaw. With another loop you could use something like the ipinfo API to relate those IP's back to geo-locations easily enough - it's always another loop in PowerShell it seems.
That second item and the last one lists the useragent though instead of the risk reasons, let's extract that key-value pair specifically rather than count on it being the first in the list
($riskylogins[4].additionalinfo | convertfrom-json) | where { $_.Key -eq "riskReasons" }
Key Value
--- -----
riskReasons {UnfamiliarDevice, UnfamiliarEASId, UnfamiliarTenantIPsubnet}
Close, but we just want the value:
(($riskylogins[4].additionalinfo | convertfrom-json) | where { $_.Key -eq "riskReasons" }).value
UnfamiliarDevice
UnfamiliarEASId
UnfamiliarTenantIPsubnet
So plugging that back into our single one-liner:
$riskylogins | select userdisplayname, activitydatetime, ipaddress, @{N='Reason';E={ ((($_.additionalinfo | convertfrom-json) | where { $_.Key -eq "riskReasons" })).value -join '; '}} | out-gridview

So the risks in the list above boil down to: you are in an unusual location (IP address, subnet, ASN, Location, or you are using an unfamiliar device.
Hmm - looking at those IP addresses, you're thinking - can I look those up using the APIs for ipinfo or maxmind? No need, it's already there, if you run "$riskylogins | gm", you'll see a "location" object.
$RiskyLogins[4].location
City CountryOrRegion State
---- --------------- -----
Gunseo-Myeon KR Chungcheongbuk-Do
But normally it's just the country that you want, so what we want is
($RiskyLogins[4].location).countryorregion
KR
Which means we need another computed field to make things work in the "report" command:
$riskylogins | select userdisplayname, activitydatetime, ipaddress, @{N='Country';e={($_.location.countryorregion)}}, @{N='Reason';E={ ((($_.additionalinfo | convertfrom-json) | where { $_.Key -eq "riskReasons" })).value -join '; '}} | out-gridview

To just view this in a text table, you could use " | ft " instead of out-gridview, or send it to an excel-readable file wiht "| out-csv"
Please, use our comment form and let us know if you've used these concepts in Graph to find a security event that you wouldn't otherwise have found!
===============
Rob VandenBrink
rob@coherentsecurity.com
Using Microsoft Graph and Powershell to Mine for Information - Stale Accounts and Licenses
Microsoft Graph is a newer API that is meant to replace several others. OK, it's at version 2.3.9, so it's not all that new, but it's new enough that lots of folks (and commercial tools) aren't using it yet. It allows you to Get and Set info from/to M365, Entra Users and Entra managed machines for starters. Let's dig in!
First, some preparation if you don't already have these modules installed:
Install-Module Microsoft.Graph -Repository PSGallery
# the beta likely isn't needed for most installs, but install it if desired
Install-Module Microsoft.Graph.Beta -Repository PSGallery
Next, an import (again, if needed)
Import-Module Microsoft.Graph
Finally, you'll need to connect to your Entra account / directory
Connect-MgGraph -Scopes "User.Read.All
Let's start exploring just by dumping a user table:
$AllUsers = Get-MgUser -All -Property Id, DisplayName, UserPrincipalName, AccountEnabled, SignInActivity | Where-Object { $_.AccountEnabled -eq $true }
Note the "-All" - this API has a default "first 100 objects" limit, if you are managing an actual domain you likely will always need a "-All" unless you are testing a script and want it to run faster.
If you want last password change included? You'll need to ask for that in the initial get-mguser call, it's not in the default returned list of results:
Get-MgUser -All -Property DisplayName, UserPrincipalName, LastPasswordChangeDateTime | Select-Object DisplayName, UserPrincipalName, LastPasswordChangeDateTime
Cool, now you have a list of accounts and their last password change, that's worth a sort in Excel (or | Out-GridView) and a few emails. Heck, since your in excel you could automate that right down to the email if you wanted.
What else? Looking at $allusers | gm, we see a property called "assignedLicenses" - let's look at that:
Get-MgUser -UserId $u -Property AssignedLicenses | Select-Object -ExpandProperty AssignedLicenses
DisabledPlans SkuId
------------- -----
{} 05e9a617-0261-4cee-bb44-138d3ef5d965
{} 639dec6b-bb19-468b-871c-c5c441c4b0cb
{} 5b631642-bd26-49fe-bd20-1daaa972ef80
{} a403ebcc-fae0-4ca2-8c8c-7a907fd6c235
{} f30db892-07e9-47e9-837c-80727f46fd3d
hm, just the GUIDs (SkuId) for each license, that's not so useful.
For the real thing (that a human can read), we'll want a whole different command:
get-mguserlicensedetail -userid $u | Select-Object SkuId, SkuPartNumber
SkuId SkuPartNumber
----- -------------
05e9a617-0261-4cee-bb44-138d3ef5d965 SPE_E3
639dec6b-bb19-468b-871c-c5c441c4b0cb Microsoft_365_Copilot
5b631642-bd26-49fe-bd20-1daaa972ef80 POWERAPPS_DEV
a403ebcc-fae0-4ca2-8c8c-7a907fd6c235 POWER_BI_STANDARD
f30db892-07e9-47e9-837c-80727f46fd3d FLOW_FREE
So to add this to a regular one-liner without a powershell loop for each account, we'll need a join, we'll add this to our original get-mguser call (because if this is for a human to read, you don't want the SkuId normally):
@{N='License';E={(Get-MgUserLicenseDetail -All -UserId $_.id).SkuPartNumber -join ';'}}
And to also, just for fun let's also pull the last interactive and non-interactive login dates:
@{N='LastInteractiveSignInDate';E={$_.SignInActivity.LastSignInDateTime}}, `
@{N='LastNonInteractiveSignInDate';E={$_.SignInActivity.LastNonInteractiveSignInDateTime}}
Also, let's pull the list of properties into a variable to make the call simpler (the computed statements are bolded):
$Properties = @('AccountEnabled','City','Country','Department','DisplayName','JobTitle','UserPrincipalName','CreatedDateTime','SignInActivity', 'LastPasswordChangeDateTime')
$Users = Get-MgUser -All -Property $Properties |
Select-Object @{N='AccountEnabled';E={$_.AccountEnabled}}, `
@{N='City';E={$_.City}}, `
@{N='Country';E={$_.Country}},
@{N='Department';E={$_.Department}},
@{N='DisplayName';E={$_.DisplayName }}, `
@{N='JobTitle';E={$_.JobTitle }}, `
@{N='UserPrincipalName';E={$_.UserPrincipalName}}, `
@{N='CreatedDateTime';E={$_.CreatedDateTime}}, `
@{N='LastInteractiveSignInDate';E={$_.SignInActivity.LastSignInDateTime}}, `
@{N='LastNonInteractiveSignInDate';E={$_.SignInActivity.LastNonInteractiveSignInDateTime}},
@{N='License';E={(Get-MgUserLicenseDetail -UserId $_.UserPrincipalName).SkuPartNumber -join '; '}}
This can take a while - for each user, those last 3 lines add time. The two "signindate" fields are an additional lookup, and the license detail line is a whole other command for each line.
So it's essentially another loop, but buried in standard syntax so you don't have to code a less efficient version ....
OK, so we have the last login dates so we can pick off inactive accounts, and license usage. Also accounts that have been explicity disagbled (that firstr "AccountEnabled" field) Dump the whole thing out to a CSV file with "| Out-CSV" , and you're an Excel sort away from a list of MS licenses you can stop paying for and a list of Entra accounts that you can likely disable or delete. Or if you are philosphically opposed to spreadsheets or excel in particular, you can do the same with "| Out-GridView", except emailing your results can be a problem from there ...

But what about a real security thing, something your SOC might alert on? Stay tuned, that's next ...
===============
Rob VandenBrink
rob@coherentsecurity.com

Comments