A menu-driven PowerShell cockpit for Microsoft 365 administration — built for MSP techs who live in Exchange Online and Entra ID all day. One launch, connect once, and drive mailboxes, permissions, mail flow, anti-spam, users, groups, licenses, MFA, and reporting from a single keyboard-friendly menu. Every tenant-changing action confirms first and is written to an audit log.
Modern stack only. Uses
ExchangeOnlineManagementv3+ (REST-based, no basic-auth/WinRM) andMicrosoft.Graph. The retiredAzureADandMSOnlinemodules are intentionally not used.
- PowerShell 7+ (
brew install powershellon macOS,winget install Microsoft.PowerShellon Windows) - Modules (the console can install them for you from Tools → Install modules):
ExchangeOnlineManagement≥ 3.4.0Microsoft.Graph≥ 2.15.0Az.Accounts,Az.Resources,Az.Compute,Az.Storage,Az.Billing,Az.App,Az.OperationalInsights(optional — only for the Azure infrastructure / Container Apps suite; install with the "include Azure" prompt)
pwsh ./Start-MSPConsole.ps1- Press C → connect Exchange / Graph / Azure (browser sign-in).
- Pick a platform hub — E Exchange Online, N Entra ID, A Azure — then a feature area. Reads are instant; writes confirm and log.
- Reports export timestamped CSVs into
exports/.
First run with no modules? Open Tools → Install modules and let it pull them.
Copy the sample and edit to taste (the live file is git-ignored):
cp config/settings.sample.json config/settings.json| Setting | Purpose |
|---|---|
DefaultTenantId |
Tenant to target on Graph connect |
DefaultDomain |
Auto-completes bare usernames → user@domain |
AntiSpamPolicyName |
Hosted content filter policy to edit (usually Default) |
DefaultUsageLocation |
Required before assigning licenses |
PageSize |
Rows per page in long listings |
ConfirmDestructive |
Master confirm gate for changes (keep true) |
GraphScopes |
Delegated scopes requested at Graph sign-in |
Settings are editable live from Tools → Edit settings.
The home screen is three platform hubs plus Connect and Tools.
Connect — Exchange / Graph / Azure sign-in, status, module install/check.
| Area | Highlights |
|---|---|
| Mailboxes | list/search, details+stats, create shared, convert type, forwarding, OOO, archive, litigation hold |
| Permissions | FullAccess, SendAs, Send-on-Behalf, calendar delegation |
| Anti-Spam / Allow-Block | policy allow/block lists, Tenant Allow/Block List, quarantine list & release |
| Mail Flow | transport rules, SCL -1 allowlist rule, block rule, message trace, connectors |
| Groups / DLs | distribution & security groups, M365 groups, membership |
| Reports | mailbox sizes, forwarding audit (BEC), shared-mailbox perms, inactive mailboxes |
| Area | Highlights |
|---|---|
| Users | list, details, create, reset password, enable/disable, revoke sessions, attributes |
| Groups | list, members, add/remove, create |
| Licenses | SKU availability, assign/remove, per-user report |
| Security & MFA | MFA registration report, Conditional Access, risky users, admin roles |
| Directory Reports | sign-ins, audit log, inactive users, tenant overview |
| Area | Highlights |
|---|---|
| App Registrations & Secrets | list/create apps, mint/rotate client secrets, request Graph API permissions, grant admin consent, create SP, delete |
| Domains | list, add custom domain, show DNS verification (TXT/MX) records, verify, remove |
| Infrastructure (Az) | subscriptions + switch, resource groups (create/delete), resources, VMs (start/stop/restart), storage accounts, month-to-date cost summary |
| Container Apps + logs | list apps by resource group, details (image/ingress/replicas), revisions, pull console/system logs from Log Analytics, restart a revision |
Install modules, edit settings, CSV bulk runner, log viewer, exports, session summary.
The console exposes all three because they are not equivalent:
- Anti-Spam → Add allowed sender — a spam-scoring hint. Convenient, but Defender can still quarantine high-confidence phish/malware. ("Almost never spam.")
- Mail Flow → Allowlist rule (SCL -1) — hard bypass of spam filtering, keyed on the envelope sender. The reliable "guarantee delivery" option.
- Anti-Spam → Tenant Allow/Block List — false-positive remediation for a spoofed/blocked sender, URL, or file hash.
- Logs:
logs/msp-console-YYYY-MM-DD.log— one line per operation, with the acting admin and target. Every change is taggedACTION. - Exports:
exports/<report>-<timestamp>.csv.
Start-MSPConsole.ps1 # entry point: load order, init, run loop
src/Core/ # engine — never changes when you add features
Config.ps1 # $Global:MSP shared state + settings
UI.ps1 # banners, tables, prompts, confirm gate
Logging.ps1 # audit log + Invoke-Safely wrapper
Helpers.ps1 # CSV import/export, pickers, size parsing
Connections.ps1 # EXO / Graph / Az connect + Assert-Connection
MenuEngine.ps1 # declarative menu renderer/dispatcher
src/Features/*.ps1 # one file per area; each exports a Get-*Menu builder
Exchange*.ps1 # Mailbox, Permissions, AntiSpam, MailFlow, Groups, Reporting
Entra*.ps1 # Users, Groups, Licenses, Security, Reporting
Azure*.ps1 # AppRegistrations, Domains, Infra (Az)
src/Menus/MainMenu.ps1 # platform hubs (Exchange/Entra/Azure) wired into the tree
tests/ # Run-Tests.ps1 (no deps), TenantMocks.ps1, Pester specs
Adding a feature = write an Invoke-* function and add a New-MenuItem to
the relevant Get-*Menu. The engine never changes. Menu keys are case-insensitive;
avoid B and Q (reserved for Back/Quit).
Everything runs offline — no tenant, no network.
pwsh ./tests/Run-Tests.ps1 # full harness (4 layers)
pwsh -c 'Invoke-Pester ./tests/Tenant.Tests.ps1 -Output Detailed' # just the mocked behavioural testsRun-Tests.ps1 layers:
- Syntax — every
.ps1parses. - Load — all modules load and every menu builds (
-SelfTest). - Conventions — no duplicate/reserved menu keys, every item wired.
- Tenant-mock behaviour (Pester 5+, auto-skips if absent) — runs
Tenant.Tests.ps1.
tests/TenantMocks.ps1 stubs every external cmdlet (Exchange / Graph / Az)
so the real Invoke-* action functions execute with no live connection.
tests/Tenant.Tests.ps1 then mocks the interactive prompts, invokes a function,
and asserts the correct cmdlet fired with the right arguments — e.g. that adding
an allowlisted sender calls Set-HostedContentFilterPolicy -AllowedSenders @{Add=...},
that the SCL -1 rule sets -SetSCL -1, and that container-app logs resolve the
Log Analytics workspace and query ContainerAppConsoleLogs_CL.
To add a test: mock the prompts (Read-Input, Select-FromList,
Confirm-Action, Read-YesNo), mock the target cmdlet, invoke, then
Should -Invoke <Cmdlet> -ParameterFilter { ... }. If you call a cmdlet not yet
in TenantMocks.ps1, add its name to the catch-all list (or give it a small
param-block stub if you need to filter on its arguments).
Install Pester once: Install-Module Pester -Scope CurrentUser -Force.
Mocks prove the console calls the right cmdlet with the right arguments — they
can't prove a real cmdlet/property name is correct against the live API. That's
what tests/Invoke-LiveSmokeTest.ps1 is for.
# already connected in your session:
pwsh ./tests/Invoke-LiveSmokeTest.ps1
# sign in first, test only Azure, target one app's log path:
pwsh ./tests/Invoke-LiveSmokeTest.ps1 -Connect -Azure -ResourceGroup rg-apps -ContainerApp web
# or fold it into the full harness as layer 5:
pwsh ./tests/Run-Tests.ps1 -LiveIt is strictly read-only. Only Get-* and Log Analytics query cmdlets run
— no New/Set/Remove/Add/Update/Start/Stop/Restart/Revoke. It reads a couple of
objects per service and, for container apps, resolves the managed-environment
Log Analytics workspace and runs a take 1 KQL query. It cannot create, change,
or delete anything; the worst case is a probe prints FAIL (which is the point —
it catches a mistyped cmdlet, a renamed property, or a missing permission). Each
probe reports PASS / FAIL / SKIP; the run exits non-zero only if a probe on
a connected service fails.
- Destructive actions gate behind
Confirm-Action(respectConfirmDestructive). - The forwarding audit and admin-role reports exist specifically to catch BEC / privilege creep — run them on any tenant you inherit.
config/settings.jsonandlogs/are git-ignored; never commit tenant data.
Community project — not affiliated with or endorsed by Microsoft. It calls the
official ExchangeOnlineManagement, Microsoft.Graph, and Az PowerShell
modules on your behalf. You are responsible for what you run against your
tenants; every change confirms and is logged, but review before you execute.
MIT — see LICENSE. Contributions welcome.