Using Microsoft Graph and Powershell to Mine for Information - Stale Accounts and Licenses

    Published: 2026-08-20. Last Updated: 2026-08-20 11:47:54 UTC
    by Rob VandenBrink (Version: 1)
    0 comment(s)

    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!

    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.  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 you SOC might alert on?  Stay tuned, that's next ...

    ===============
    Rob VandenBrink
    rob@coherentsecurity.com

    0 comment(s)
    ISC Stormcast For Thursday, August 20th, 2026 https://isc.sans.edu/podcastdetail/10060

      Comments


      Diary Archives