AI security & operations for MSPs

Microsoft 365

A PowerShell script you can review and run yourself. It builds the registration Manual: Microsoft 365 documents, the same 15 application permissions (12 Graph, 3 O365 Mgmt), the same 24-month client secret, the same Unified Audit Logging enabled, but the script makes the API calls so you don’t have to click through the Azure portal one permission at a time. You can connect the registration it builds by pasting the values it prints, as After the script describes, or keep it as a reference and an audit tool and connect the tenant with Hal-assisted onboarding.

Note

Source-readable and self-auditing. Plain PowerShell. No compiled binaries, no obfuscated calls. The only external dependencies are Microsoft’s own official PowerShell modules (ExchangeOnlineManagement and the Microsoft.Graph.* modules), which the script auto-installs in user scope if missing.

The script reads tenant state at each phase, reports what it found, and pauses inline before each consequential change so you can review and abort with Ctrl+C. Safe to run on a tenant that’s already ingesting, the audit phase reports drift between the live app registration and the script’s expected list before any change is made.

Download

Download hal-m365-tenant-setup.ps1

Or fetch from a PowerShell prompt:

Invoke-WebRequest -Uri https://runhal.com/scripts/hal-m365-tenant-setup.ps1 -OutFile hal-m365-tenant-setup.ps1

Open the saved file in any text editor and confirm the contents match what’s documented below before you run it. Microsoft’s permission GUIDs are stable, well-known constants, you can verify them against the Microsoft Graph permissions reference.

Prerequisites

  • PowerShell 7+ (Windows, macOS, or Linux). Required, the Microsoft Graph 2.x modules target .NET 5+ and won’t load under Windows PowerShell 5.1. You don’t have to install it ahead of time: if you launch the script under 5.1, it detects the version mismatch, offers to install PS7 via winget, and re-invokes itself under pwsh automatically. PS7 installs side-by-side with 5.1 and doesn’t replace it. To install manually instead:

    winget install --id Microsoft.PowerShell --source winget
  • Global Administrator access in the customer’s Microsoft 365 tenant.

  • Network egress to login.microsoftonline.com, graph.microsoft.com, outlook.office365.com, and the PowerShell Gallery (psg-prod-eastus.azureedge.net) if the modules aren’t already installed.

Running

From a PowerShell prompt on the customer’s admin workstation (or any machine where the admin can sign in), cd to the directory where you saved the script. First ask Hal in the portal chat to connect the client’s Microsoft 365 tenant with a registration you build: he opens a paste box for the client on Settings → Microsoft 365 and gives the registration its name, of the form hal-siem-log-collector-<deployment code>-<4 hex>. Then run:

PowerShell -ExecutionPolicy Bypass -File .\hal-m365-tenant-setup.ps1 -UserPrincipalName admin@customer.com -RegistrationName hal-siem-log-collector-abc-1a2b

Replace admin@customer.com with the customer’s Global Admin UPN, and hal-siem-log-collector-abc-1a2b with the name Hal gave you.

A few notes on the command:

  • -RegistrationName is required. The script refuses a name not of Hal’s form, finds an existing registration by the name, and creates one under it when there is none.
  • PowerShell (capital P, no pwsh) is the Windows-shipped 5.1 shell, available on every Windows machine. If the script detects 5.1 it prompts to install PS7 via winget and re-invokes itself under pwsh. Using pwsh directly works too if you already have PS7.
  • -ExecutionPolicy Bypass is scoped to this single process, the system-wide policy is unchanged. Without it, fresh Windows machines default to Restricted and refuse to run any script (signed, unsigned, local, or downloaded, see Microsoft’s execution policies reference).
  • Local administrator (UAC) is not required to run the script itself. It signs in to Microsoft Graph and Exchange Online interactively as the Global Admin you specify and makes all tenant changes via cloud API calls. The only step that may prompt UAC is the one-time PowerShell 7 install via winget.
  • You will see two interactive sign-in prompts as the same Global Admin, one for Exchange Online (Phase 2) and one for Microsoft Graph (Phase 4). The two are separate Microsoft APIs with separate token audiences (outlook.office365.com and graph.microsoft.com), so a single token can’t cover both. On Windows 11 the second prompt is usually just a one-click WAM (Windows Account Manager) account picker, not a full password re-entry.

The script’s flow:

  1. Pre-flight summary. Prints what it’s going to do and waits for you to press Enter (or Ctrl+C to abort).
  2. Module readiness. Installs missing PowerShell modules (ExchangeOnlineManagement, Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Identity.SignIns) in user scope.
  3. Exchange Online sign-in. Interactive sign-in as the Global Admin you specified.
  4. Read tenant state. Reads IsDehydrated and current UnifiedAuditLogIngestionEnabled value, prints them. Pauses before any tenant-side change. If both are already correct, prints “no changes needed” and continues without pausing.
  5. Microsoft Graph sign-in. Second interactive sign-in (same Global Admin). Reads tenant ID.
  6. App registration. Looks for hal-siem-log-collector. Found: reports the existing AppId / ObjectId / creation date and continues. Not found: pauses before creating.
  7. Permission audit. Reads the app’s current RequiredResourceAccess and compares against the script’s expected list. Reports MATCH / MISSING / EXTRA per resource. If drift is detected, pauses and shows exactly what will be added or removed before Update-MgApplication runs.
  8. Admin consent audit. Reads the app’s current role assignments, computes which of the 15 permissions still need consent. If any need consent, pauses and lists them before granting. If all 15 are already consented, prints “no grants needed” and continues without pausing.
  9. Client secret. Reads existing non-expired secrets and prints their key IDs and expiration dates. By default skips secret creation if any non-expired secret exists; with -ForceNewSecret, pauses and confirms before minting an additional one.
  10. Output. Prints the credentials, Tenant ID, Application (client) ID, Client Secret Value, Secret Expiration, to paste into the box Hal opened.

Pass -NonInteractive to skip all the inline pauses (useful for automation; only do this on tenants where you’ve already verified the permission list is in sync).

Idempotency

The script is safe to re-run. On a second invocation it will:

  • Reuse the existing app registration with the name you pass (no duplicates).
  • Skip permissions and consents already granted.
  • Skip new-secret creation if a non-expired secret already exists. Use -ForceNewSecret to override this and mint a fresh one (the old secret continues to work until it expires).
  • Re-verify hydration and UAL state and no-op if both are already correct.

Hydration propagation: what to do if the script tells you to wait

After running Enable-OrganizationCustomization on a freshly-provisioned tenant, Microsoft’s backend takes anywhere from a few minutes to several hours to finish propagating the change. During that window, Set-AdminAuditLogConfig will continue to fail with the same dehydration error.

When the script encounters this state it exits cleanly (exit code 2) with a wait-and-retry message rather than continuing to error. Just re-run the script later, the same command, no flags. The earlier steps are idempotent and will no-op; the retry only matters at Set-AdminAuditLogConfig. As soon as Microsoft’s propagation completes, the next run continues all the way through and prints credentials.

After the script

Paste the Directory (tenant) ID, the Application (client) ID and the secret’s Value into the box Hal opened on Settings → Microsoft 365, and tell him done; he checks the registration as Manual: Microsoft 365 describes. Hal holds what you paste for an hour, so tell him done within it.

A registration an earlier version of the script built is named hal-siem-log-collector, which Hal does not connect. To keep it, rename it to the name Hal gives, in the Microsoft Entra admin center under the registration’s Branding & properties, before you run the script, which then finds it under that name.

Or connect the tenant with Hal-assisted onboarding: Hal creates his own registration beside the one the script built, and you may delete the script’s afterwards.

What the script can’t do

It can’t toggle Microsoft prerequisites that aren’t exposed via API:

  • Entra ID P1 licensing for sign-in logs and risk detections. Directory audits need no premium license.
  • Entra ID P2 licensing for the premium risk detection kinds and sign-in risk scores.

If those licenses aren’t on the tenant, Hal still works for everything else, Microsoft just won’t deliver the sign-in or risk data to be ingested. See Audit Logging Prerequisites for the licensing details.

Questions? Contact us.

See what Hal surfaces on your own clients.

No deck. Ask us anything first. When you want to see Hal on your own tenants, we sign a short evaluation agreement and stand up your instance; you connect one tenant, Hal watches it for 14 days, and we walk you through what he found.

  1. 01You ask us your questions. No deck, no demo dataset.
  2. 02You sign a short evaluation agreement, and we stand up your own instance.
  3. 03You connect one tenant from your own admin console. Hal watches it for 14 days.
  4. 04We walk through what he found. Keep going month to month, or revoke the scopes yourself and stop.