# Build your own Microsoft 365 audit, with or without AI: when it is the right call

> With Microsoft Graph and an AI coding assistant, a Microsoft 365 audit script takes a weekend. The cost comes later: API changes, detections, proof.

Source: https://olyteck.com/blog/build-your-own-microsoft-365-audit
Published: 2026-09-25
Author: Oleg Garasym, Olyteck (France, EU-hosted)
Product: Olyteck Cyber
Topics: microsoft 365, microsoft graph, powershell, audit, anonymous links, oauth apps, entra id, governance
License: free to quote with attribution to Olyteck and a link to the source URL.

---

In large French enterprises the question has changed. Some IT departments are considering
replacing part of their SaaS subscriptions with apps built in house with AI. Others are already
standardising an internal AI-assisted development stack approved by security, with SSO and a shared
database. "We'll build it ourselves" is no longer a weekend project by an enthusiastic admin. It is
becoming company policy.

A Microsoft 365 audit is an obvious candidate. The APIs are documented, examples are everywhere, and
an AI coding assistant writes decent PowerShell in minutes. This article takes the idea seriously: a
working script, what it really costs to keep it alive, and the cases where building it yourself is
simply the right call.

## The scan is not the hard part

Let's be plain about it: with Microsoft Graph and an AI coding assistant, a competent admin has a
first audit script running in a weekend. Here is a minimal, read-only example that lists "Anyone with
the link" shares (scope `anonymous`, [how to find and triage them](/blog/find-anonymous-sharing-links-microsoft-365)) on the items of a single OneDrive.

```powershell
# Read-only: lists "anonymous" links on the items of ONE OneDrive.
# With delegated sign-in, Graph returns every link only on items you own.
# For another user's OneDrive, connect app-only (certificate) with the
# Files.Read.All application permission.
Connect-MgGraph -Scopes "Files.Read.All", "Sites.Read.All" -NoWelcome

$upn   = "first.last@yourdomain.com"             # the OneDrive to check
$drive = Get-MgUserDefaultDrive -UserId $upn
$root  = Get-MgDriveRoot -DriveId $drive.Id

function Get-AnonymousLinks($DriveId, $ItemId, $Path) {
    # -All follows paging (@odata.nextLink, 200 items per page by default)
    foreach ($item in Get-MgDriveItemChild -DriveId $DriveId -DriveItemId $ItemId -All) {
        $itemPath = "$Path/$($item.Name)"
        # Permissions cannot be expanded from the list: one call per item
        Get-MgDriveItemPermission -DriveId $DriveId -DriveItemId $item.Id -All |
            Where-Object { $_.Link.Scope -eq "anonymous" -and -not $_.InheritedFrom.Id } |
            ForEach-Object {
                [pscustomobject]@{
                    Path    = $itemPath
                    Type    = $_.Link.Type              # view or edit
                    Expires = $_.ExpirationDateTime
                }
            }
        # Unbounded recursion: every folder calls the function again
        if ($item.Folder.ChildCount -gt 0) {
            Get-AnonymousLinks -DriveId $DriveId -ItemId $item.Id -Path $itemPath
        }
    }
}

Get-AnonymousLinks -DriveId $drive.Id -ItemId $root.Id -Path "" | Format-Table
```

It works, and its limits are worth stating honestly:

- It covers one drive: for the whole tenant you have to enumerate every OneDrive, every site and every library.
- Recursion has no depth limit: a deep folder tree multiplies calls and runtime.
- Every item costs one permissions call: on a large tenant, think hours, not minutes.
- Graph returns 429 errors when you go too fast; the SDK honours the Retry-After header, but the run gets longer accordingly.
- With delegated access, a caller who does not own an item only sees the permissions that apply to them; a full inventory needs an app-only application, which means an app registration, a certificate and admin consent.
- The `InheritedFrom` filter avoids duplicates: a link set on a folder is reported once, at the folder.

None of this is insurmountable. That is exactly the point: the loop is easy to write. Everything
around it is what costs.

## The API moves, and your script with it

Microsoft Graph has two versions. v1.0 is meant for production; beta can change or disappear at any
time, and using it in production applications is not supported. Some data that matters for an audit
shows up in beta first, and an AI coding assistant will happily suggest either. On the v1.0 side,
Microsoft declares an API deprecated at least 24 months before removing it: comfortable, as long as
someone actually reads the changelog.

The last two years showed what that means in practice:

- The MSOnline and AzureAD PowerShell modules, deprecated since 30 March 2024, were retired in 2025: MSOnline between early April and late May, AzureAD after 1 July.
- Azure ACS, the legacy app authentication mechanism for SharePoint Online, stopped working for all tenants on 2 April 2026, with no option to extend.
- SharePoint Add-Ins stopped working on the same date, 2 April 2026.

Each of these retirements broke scripts that ran perfectly the day before. An in-house tool is not a
deliverable. It is a subscription to Microsoft's documentation, paid in hours.

## The detection library is the product

The script above answers one question. An audit asks dozens: sites open to "Everyone except external
users", guests, [ex-employee OneDrives](/blog/employee-leaves-microsoft-365-onedrive-sharing-access), ownerless teams, inactive admin accounts, OAuth consents, mail
forwarding rules. The list grows with every new feature: new sharing options, Copilot surfacing
whatever a user can already read, [agents acting with delegated permissions](/blog/ai-agents-microsoft-365-security), apps asking for broader
scopes than they did last year.

Knowing what to look for, and keeping that knowledge current, is the real work. The loop that walks
the tenant is the part every control shares; knowing what is risky, and what changed this quarter, is
what makes an audit worth running. An AI coding assistant writes the loop very well. It will not tell
you which detection is missing.

## The trail, and the auditor's question

A script prints a state. An auditor asks for a dated history: what was found, when, who fixed it, on
what date, and what proves the fix held. Today's CSV export answers none of that. To answer it you
have to store every run, compare results week to week, log each fix with its author, and keep the
whole thing where nobody can rewrite it after the fact. That is a second project, longer than the
first.

Then comes the question an auditor rarely phrases this way but always asks: who wrote the control,
who can change it, and is the person being checked the one who wrote the check? When the tenant
admin writes the script that audits their own configuration choices, can edit it, and is the only
one presenting its results, there is no separation of duties. That is not an accusation. It is the
most basic question in any control framework.

## Who maintains it when its author leaves?

This is the question that settles it. The script lives in a repository, on a laptop or in a
scheduled task. It depends on a certificate that expires, an app registration only its author knows
about, and assumptions nobody wrote down. The day that person leaves, the control either keeps
running unattended or quietly stops, and nobody notices.

That is exactly what an audit finds in the tenant itself: spaces without an owner. Teams whose last
owner left, ex-employee OneDrives, links created by someone who is no longer there. An audit tool
with no owner is the same problem, one level up.

## The second-order effect: your internal apps in Entra ID

AI-assisted development has a less visible consequence. Every internal app that offers "Sign in with
Microsoft" is registered in Entra ID: an application object, plus a service principal that shows up
under Enterprise applications. By default a user can register an application, and becomes its owner
automatically. The delegated Microsoft Graph permissions it requests are often consented by the
person who built it, then by colleagues the first time they sign in.

These apps show up in the tenant's list of OAuth consents, next to third-party apps. Three questions
for each one:

- Who owns it today, and is that person still with the company?
- Which scopes were consented, and are they broader than what the app actually uses?
- Is the app still in use, or is its consent outliving an abandoned prototype?

If your company encourages AI-assisted development, this list is going to grow. Better to look at it
now, and to check the "Users can register applications" setting in Entra ID.

## When building it yourself is the right call

There are cases where it clearly is, and they deserve to be said:

- A technical team with real time to spend on it, and someone to maintain it.
- A single, stable tenant, with no subsidiary or second tenant to consolidate.
- A one-off need: an inventory before a migration, a check after an incident.
- No auditor to satisfy: no ISO 27001, no parent company, no customer asking for evidence.
- Learning: writing this script is the best way to understand the Microsoft 365 sharing model.
- A check so specific to your organisation that no product will ever cover it.

The general rule fits in two sentences:

> Build what makes you different: your processes, your business workflows. Buy the controls that
> have to be independent, maintained and provable.

## How we approach this at Olyteck

**Olyteck Cyber** reads the tenant through an application whose 19 Microsoft Graph permissions are
all read-only, with no agent and no file export. It covers seven modules, including SharePoint and
OneDrive anonymous links, Entra ID identity and OAuth apps, with the consents granted, high-risk
scopes and stale consents. We count findings, never files: only counts, flags and severities are
stored. Every answer states its own coverage, for example how many sites were searched out of the
total. Paid plans add daily and weekly scans with a report of what changed, and every fix is logged
with the date, the administrator, the object and the outcome, exportable for your auditor. The first report arrives within minutes; on a large tenant the full analysis carries on in the
background.

## FAQ

### Can an AI coding assistant write a reliable Microsoft 365 audit script?

For the scan loop, yes, provided you review the code and check every cmdlet against the Microsoft
Graph PowerShell documentation. Typical mistakes are a beta API presented as stable, missing paging,
or a permission broader than needed. Reliability over time depends on maintenance, not on the first
version.

### Which permissions do I need to audit sharing links read-only?

Reading an item's permissions needs at least Files.Read delegated or Files.Read.All as an
application permission; Sites.Read.All also works. With delegated access, Graph returns every link
only on items you own. A tenant-wide inventory therefore needs an app-only application with admin
consent, and no write permission at all.

### How do I find internal apps registered in Entra ID?

In the Entra admin center, the App registrations page lists applications created in your tenant with
their owners, and the Enterprise applications page shows service principals and their consented
permissions. In Graph, the oauth2PermissionGrant resource lists the delegated permissions granted.
Look for apps with no active owner or no recent sign-in.

### Can a home-built script be enough for an ISO 27001 or parent-company audit?

It can, if you keep dated results, a log of fixes with their author, and a clear answer on who wrote
the control and who can change it. That is doable, but it is a project in its own right. Ask your
auditor what evidence they expect before you start coding.

## Sources

- https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.authentication/connect-mggraph?view=graph-powershell-1.0
- https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.files/get-mguserdefaultdrive?view=graph-powershell-1.0
- https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.files/get-mgdriveroot?view=graph-powershell-1.0
- https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.files/get-mgdriveitemchild?view=graph-powershell-1.0
- https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.files/get-mgdriveitempermission?view=graph-powershell-1.0
- https://learn.microsoft.com/en-us/graph/api/driveitem-list-permissions?view=graph-rest-1.0
- https://learn.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0
- https://learn.microsoft.com/en-us/graph/api/resources/sharinglink?view=graph-rest-1.0
- https://learn.microsoft.com/en-us/graph/api/resources/permission?view=graph-rest-1.0
- https://learn.microsoft.com/en-us/graph/throttling
- https://learn.microsoft.com/en-us/graph/versioning-and-support
- https://learn.microsoft.com/en-us/powershell/azure/active-directory/overview?view=azureadps-2.0
- https://techcommunity.microsoft.com/blog/microsoft-entra-blog/action-required-msonline-and-azuread-powershell-retirement---2025-info-and-resou/4364991
- https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/retirement-announcement-for-azure-acs
- https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/retirement-announcement-for-add-ins
- https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals
- https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview
- https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions
- https://learn.microsoft.com/en-us/graph/api/oauth2permissiongrant-list?view=graph-rest-1.0

## Read next

- [Anonymous sharing links in Microsoft 365: find every "anyone" link and fix the risky ones first](/blog/find-anonymous-sharing-links-microsoft-365)
- [AI agents in Microsoft 365: the new attack surface to audit](/blog/ai-agents-microsoft-365-security)
- [SharePoint Advanced Management ships with Copilot: what it covers, and what it does not see](/blog/sharepoint-advanced-management-included-copilot-gaps)

