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.ps1Open 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 underpwshautomatically. PS7 installs side-by-side with 5.1 and doesn’t replace it. To install manually instead:winget install --id Microsoft.PowerShell --source wingetGlobal 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.comReplace admin@customer.com with the customer’s Global Admin UPN.
A few notes on the command:
PowerShell(capital P, nopwsh) is the Windows-shipped 5.1 shell, available on every Windows machine. If the script detects 5.1 it prompts to install PS7 viawingetand re-invokes itself underpwsh. Usingpwshdirectly works too if you already have PS7.-ExecutionPolicy Bypassis 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.comandgraph.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:
- Pre-flight summary. Prints what it’s going to do and waits for you to press Enter (or Ctrl+C to abort).
- Module readiness. Installs missing PowerShell modules
(
ExchangeOnlineManagement,Microsoft.Graph.Authentication,Microsoft.Graph.Applications,Microsoft.Graph.Identity.SignIns) in user scope. - Exchange Online sign-in. Interactive sign-in as the Global Admin you specified.
- Read tenant state. Reads
IsDehydratedand currentUnifiedAuditLogIngestionEnabledvalue, prints them. Pauses before any tenant-side change. If both are already correct, prints “no changes needed” and continues without pausing. - Microsoft Graph sign-in. Second interactive sign-in (same Global Admin). Reads tenant ID.
- App registration. Looks for
hal-siem-log-collector. Found: reports the existingAppId/ObjectId/ creation date and continues. Not found: pauses before creating. - Permission audit. Reads the app’s current
RequiredResourceAccessand 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 beforeUpdate-MgApplicationruns. - 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.
- 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. - 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
-ForceNewSecretto 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.