Microsoft Office Server Directory DirectoryObject NotFound Exception

Can’t delete a sharepoint site that used to have a group attached to it, but the group has long since been deleted?

Apparently, the sharepoint admin center is unable to handle this scenario, and just throws a “could not be deleted” error. Using fiddler the only extra info I could get was an underlying 500 Internal Server Error Microsoft.Office.Server.Directory.DirectoryObjectNotFoundException was thrown

Obviously that directory object not found means the office 365 group. Fora seem to point to MS support for manual fixes, but since Microsoft Support has rarely been a pleasant experience for me I went over the PS module for SPO and found a MUCH faster fix, hope it also works for you!

  1. start PowerShell 5 (don’t use a newer version until the module supports it)
  2. install-module Microsoft.Online.SharePoint.PowerShell -force -scope currentuser (unless you already have the module installed)
  3. import-module Microsoft.Online.SharePoint.PowerShell
  4. Set-SPOSite -Identity https://xxx.sharepoint.com/sites/xxx -ClearGroupId
  5. Remove-SPOSite -Identity https://xxx.sharepoint.com/sites/xxx -noWait

Average device daily usage report in PowerBI

A customer wanted to know how many hours on average their devices were being used, without modifying their configuration.

This to help target replacements, upgrades, #desks per location, #shared devices per location etc.

At first I figured it’d be a simple task, just getting the right event ID’s from the local eventlog, but no, that required additional auditing to be enabled and how would the data flow to PowerBI?

The next attempted data source was much better; Defender Advanced Hunting. Especially the DeviceLogonEvents table, which contains cached and interactive logons to all monitored devices.

Using logon events, we can get an idea of a device’s relative use in the fleet. It won’t be exact as measuring the time between pure lock/unlock/signin/signout events.

This was fine for the customer’s purpose and checking their data the number of daily logon events (7-9) was actually much higher than the expected 2-3 per device per day (at least on Windows 11, our target OS).

Getting that data into PowerBI turned out to be even easier 🙂

Option 1: use my PowerBI template

Option 2: build your own report using below steps:

  1. Open up PowerBI and click Blank Query
  1. Open the Advanced Editor
  1. Paste the KQL query and click done

KQL:

 let
        AdvancedHuntingQuery = "DeviceLogonEvents
| where Timestamp between (ago(28d) .. now())
| where ActionType == ""LogonSuccess""
| where LogonType == ""Interactive""
| extend LogonDate = startofday(Timestamp)
| where LogonDate < startofday(now())  
| summarize 
    FirstLogon = min(Timestamp), 
    LastLogon = max(Timestamp)
    by DeviceName, LogonDate
| extend UsageDuration = LastLogon - FirstLogon
| project DeviceName, LogonDate, FirstLogon, LastLogon, UsageDuration
| order by DeviceName asc, LogonDate desc",

        HuntingUrl = "https://api.security.microsoft.com/api/advancedqueries",

        Response = Json.Document(Web.Contents(HuntingUrl, [Query=[key=AdvancedHuntingQuery]])),

        TypeMap = #table(
            { "Type", "PowerBiType" },
            {
                { "Double",   Double.Type },
                { "Int64",    Int64.Type },
                { "Int32",    Int32.Type },
                { "Int16",    Int16.Type },
                { "UInt64",   Number.Type },
                { "UInt32",   Number.Type },
                { "UInt16",   Number.Type },
                { "Byte",     Byte.Type },
                { "Single",   Single.Type },
                { "Decimal",  Decimal.Type },
                { "TimeSpan", Duration.Type },
                { "DateTime", DateTimeZone.Type },
                { "String",   Text.Type },
                { "Boolean",  Logical.Type },
                { "SByte",    Logical.Type },
                { "Guid",     Text.Type }
            }),

        Schema = Table.FromRecords(Response[Schema]),
        TypedSchema = Table.Join(Table.SelectColumns(Schema, {"Name", "Type"}), {"Type"}, TypeMap , {"Type"}),
        Results = Response[Results],
        Rows = Table.FromRecords(Results, Schema[Name]),
        Table = Table.TransformColumnTypes(Rows, Table.ToList(TypedSchema, (c) => {c{0}, c{2}}))

    in Table
  1. Edit your credentials and sign in

Now you’ve got the data connected, and just need to create a table with the fields you want.

Categorizing Service Principals in Entra

For M365Permissions I wanted to categorize service principals in an actually useful way.

This is what I came up with so far

    function get-servicePrincipalType{
        Param(
            [Parameter(Mandatory=$true)][object]$spn
        )

        #managed identities are simple :)
        if($spn.servicePrincipalType -eq "ManagedIdentity"){
            return "ManagedIdentity"
        }        

        #other SPN's can be hosted by us, by Microsoft or by a third party 
        #Although 9188040d-6c67-4c5b-b112-36a304b66dad is also officially Msft, it contains consumer apps not built or vetted by Microsoft thus we treat it as third party
        if($spn.appOwnerOrganizationId -in ("f8cdef31-a31e-4b4a-93e4-5f571e91255a","72f988bf-86f1-41af-91ab-2d7cd011db47","7579c9b7-9fa5-4860-b7ac-742d42053c54")){
            return "MicrosoftApplication"
        }elseif($spn.appOwnerOrganizationId -eq <YOURTENANTID>){
            #this is either a homebrew app or an AI agentic app
            if($spn.tags -and ($spn.tags -contains "AgenticApp" -or $spn.tags -contains "AIAgentBuilder")){
                return "AiAgent"
            }else{
                return "InHouseApplication"
            }
        }else{
            return "ThirdPartyApplication"
        }              
    }      

Function to Spot ALL All-User and All-Guest Groups in Entra ID

There are probably many scenario’s where you’d like to identify which Entra groups contain ‘all users’, ‘all guests’ or a combination (all members+all guests).

In my case, I want to use this in M365Permissions, but also needed it for a Maester test to be more precise. It had to be language and implementation agnostic.

M365Permissions uses this mainly for reports that look at oversharing (e.g. when securing a tenant or implementing copilot). But this could also be useful for red/blue teams or any other tenant analysis tooling.

In M365Permissions, I initially looked at the dynamic rule itself, but this is unreliable. Dynamic rules can contain many additional components and can be ordered or written in many ways or the group may have been created without a dynamic rule through e.g. automation.

So I decided to use another approach!

Just get all tenant users from Graph (counts per type).

Then for a given group, look if it matches one of those counts and return a type. Of course, casting members to users to avoid counting devices and looking up membership recursively 🙂

Function: https://github.com/jflieben/assortedFunctionsV2/blob/main/Get-EntraDynamicGroupType.ps1

Azure VM Spot Pricing API

The Azure Retail Prices API does not give correct prices for SPOT VM’s (which can change at any time).

As I use SPOT VM’s a lot to scan largers tenants with M365Permissions to avoid throttling and get results FAST, I needed a reliable way to get the lowest priced F-series VM to temporarily finish a scan queue before getting discarded again.

I created an initial function using the ‘hidden’ retail billing API which calls the ‘https://s2.billing.ext.azure.com/api/Billing/Subscription/GetSpecsCosts?SpotPricing=true’ endpoint.

My post on Linked then got some advice Morten which I now finally got around into creating a function for, using an official instead of ‘hidden’ API!

This resulted in the Get-VmSpotPrices function, which I’m sharing for free through Github:

https://github.com/jflieben/assortedFunctionsV2/blob/main/Get-VmSpotPrices.ps1

Microsoft 365, Azure, Automation & Code