Skip to content

Microsoft 365

A PowerShell script you can review and run yourself. Same end-state as Manual: Microsoft 365 — same Azure app registration, same 15 application permissions (12 Graph + 3 O365 Mgmt), same 24-month client secret, 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.

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, then:

PowerShell -ExecutionPolicy Bypass -File .\hal-m365-tenant-setup.ps1 -UserPrincipalName admin@customer.com

Replace admin@customer.com with the customer’s Global Admin UPN.

A few notes on the command:

  • 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 — for you to paste into your MSP portal.

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 matching name (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 four output values into your Hal MSP portal — see Onboarding Tenants for the portal-side steps.

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 directory audits.
  • Entra ID P2 licensing for risk detections.

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.