365TUNE

2.3.19

365TUNE PowerShell toolkit for Microsoft 365 license optimization, security compliance, cost reporting, and Azure permissions management.

Installation Options

Copy and Paste the following command to install this package using PowerShellGet More Info

Install-Module -Name 365TUNE

Copy and Paste the following command to install this package using Microsoft.PowerShell.PSResourceGet More Info

Install-PSResource -Name 365TUNE

You can deploy this package directly to Azure Automation. Note that deploying packages with dependencies will deploy all the dependencies to Azure Automation. Learn More

Manually download the .nupkg file to your system's default download location. Note that the file won't be unpacked, and won't include any dependencies. Learn More

Owners

Copyright

(c) 2025 Metawise Consulting LLC. All rights reserved.

Package Details

Author(s)

  • Metawise Consulting LLC

Tags

Azure Microsoft365 M365 MSP Licensing Security Compliance CIS Entra GDAP ExchangeOnline 365TUNE

Functions

Invoke-365TUNEConnectAll Invoke-365TUNERevokeAll Invoke-365TuneConnectAzure Invoke-365TuneRevokeAzure Invoke-365TuneConnectExchange Invoke-365TuneRevokeExchange Invoke-365TuneConnectTeams Invoke-365TuneRevokeTeams Invoke-365TuneTestAzure Invoke-365TuneTestExchange Invoke-365TuneTestTeams Invoke-365TUNETestAll

Dependencies

This module has no dependencies.

Release Notes

v2.3.19 (bug fix release)
- Fixed Remove-365TuneElevation: root-scope ARM DELETE requires a double-slash URL
 (https://management.azure.com//providers/.../roleAssignments/{guid}) not single-slash.
 The id returned by the GET response has a single leading slash; using it directly produced
 a single-slash URL that ARM returns 403 for. Now the GUID is extracted from the id and the
 double-slash URL is built explicitly.
- Added v2/MSAL-style token via "az account get-access-token --scope" (closer to what
 Remove-AzRoleAssignment uses in regular PowerShell, avoids v1 cached token issues).
- Added Graph API PATCH as Method 4: PATCH /beta/organization/{tenantId} with
 elevatedAccessForAuthorizationManagement=false. This is the same mechanism the Azure Portal
 uses and calls a completely different API surface, unaffected by ARM auth cache issues.
- ARM error code and message now included in the [WARN] output for easier diagnosis.

v2.3.18 (bug fix release)
- Fixed Remove-365TuneElevation for Cloud Shell: two root causes identified and fixed.
 (1) DELETE URL bug: found.id.TrimStart('/') was stripping the leading double-slash from
 root-scope assignment IDs (e.g. "//providers/.../roleAssignments/{guid}"), producing a
 single-slash URL that ARM rejects with 403. The raw id is now used directly to preserve
 the correct double-slash form in the DELETE URL.
 (2) Stale auth cache: Azure ARM's authorization cache stores the caller's evaluated roles
 keyed on the token; the cached pre-elevation token gets a stale "no UAA" cached result.
 A force-refreshed token (az account get-access-token --force-refresh) gets a new token
 that triggers a fresh RBAC lookup, which finds the new UAA assignment and allows DELETE.
 Also added "az role assignment delete" as a dedicated CLI fallback (Method 2) and
 demoted "az rest DELETE" to Method 3.

v2.3.17 (bug fix release)
- Fixed ConnectAzure and RevokeAzure for Cloud Shell CLI bridge: New-AzRoleAssignment and
 Remove-AzRoleAssignment also fail with 401/Unauthorized when the Az context is a static-token
 CLI bridge (no refresh token). Replaced both cmdlets with three escalating methods:
 (1) Invoke-WebRequest with the CLI ARM token and a direct REST PUT/DELETE to the ARM role
 assignment endpoint, (2) az rest --method PUT/DELETE (CLI native HTTP client), and
 (3) the original Az cmdlet as a fallback for regular PowerShell. Role assignment IDs from
 the earlier REST query are reused for DELETE to avoid an extra lookup.

v2.3.16 (bug fix release)
- Fixed Remove-365TuneElevation for Cloud Shell CLI bridge: the CLI ARM token works for GET and
 POST (elevateAccess) but not DELETE at root scope. Replaced the retry-delay approach with
 three escalating methods: (1) Invoke-WebRequest with CLI ARM token using api-version=2022-04-01,
 (2) az rest --method DELETE (CLI native HTTP client, handles auth differently), and
 (3) Remove-AzRoleAssignment Az cmdlet. Returns on first success.

v2.3.15 (bug fix release)
- Fixed Remove-365TuneElevation: UAA DELETE at root scope gets 403 when RBAC propagation is
 incomplete. Root cause: the GET to find the assignment works immediately (GA can always list
 root-scope assignments), but the authorization service may take 30-120 seconds to reflect
 UAA for write/delete operations. Replaced the single 20+20s retry with a retry loop:
 30s initial wait then up to 3 attempts, each 30s apart (90s max), with status feedback.

v2.3.14 (bug fix release)
- Fixed Invoke-365TuneElevation and Remove-365TuneElevation: replaced Invoke-AzRestMethod with
 direct Invoke-WebRequest calls using ARM tokens fetched via Get-AzAccessToken with az CLI
 fallback. Invoke-AzRestMethod has the same static-token limitation as Get-AzAccessToken when
 the context is established via Connect-AzAccount -AccessToken (CLI bridge) -- it sends no
 valid auth header, resulting in a 401 from the elevateAccess endpoint.

v2.3.13 (bug fix release)
- Fixed Cloud Shell CLI bridge token propagation (all 7 affected functions): when authentication
 uses the Azure CLI bridge (az account get-access-token + Connect-AzAccount -AccessToken), the
 resulting static-token context cannot issue tokens for other resources via Get-AzAccessToken
 (no refresh token). Every Get-AzAccessToken call is now wrapped in try/catch; if it throws,
 the catch re-fetches the token directly from az account get-access-token. This covers both
 standalone function calls and calls from orchestrators via -SkipAuth.

v2.3.12 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): when Get-AzContext -ListAvailable
 returns no user context, the module now bridges from Azure CLI which is always
 authenticated as the portal user in Cloud Shell. Uses az account get-access-token to
 obtain ARM and Graph tokens, then calls Connect-AzAccount -AccessToken to establish the
 user context in Az PowerShell without any interactive prompt or device code.

v2.3.11 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): replaced Connect-AzAccount with
 Get-AzContext -ListAvailable + Set-AzContext. When Cloud Shell is opened from the Azure
 Portal, it injects the user's context alongside MSI but MSI may be set as active. The
 fix finds the first non-MSI context in the list and switches to it without any login
 prompt. Falls back to Connect-AzAccount only if no user context is present at all.

v2.3.10 (bug fix release)
- Fixed Cloud Shell MSI handling (all 12 functions): when Cloud Shell starts with MSI
 context, functions now call Connect-AzAccount WITHOUT calling Disconnect-AzAccount first.
 This keeps MSI as a fallback while attempting browser-based (portal session) login.
 The v2.3.8 approach called Disconnect first, which stripped the MSI fallback and caused
 Connect-AzAccount to fail silently, leaving no context at all.
- Fixed Remove-365TuneElevation MSI account quirk: elevation cleanup used SignInName to
 find the User Access Administrator assignment, but MSI/SP accounts have no SignInName so
 the assignment was never found and left in place. Lookup now uses a REST API call filtered
 by principalId (OID from the JWT), which works for both user and MSI/SP accounts. The
 Az cmdlet is retained as a fallback.

v2.3.8 (bug fix release)
- Fixed Cloud Shell MSI fallback (all 12 functions): when Cloud Shell is opened outside
 the Azure Portal the pre-loaded context is MSI rather than a user account. All functions
 now detect this and call Connect-AzAccount (browser-based, no device code) to obtain a
 real user context before proceeding instead of throwing immediately.

v2.3.7 (bug fix release)
- Fixed Cloud Shell MSI authentication (all 12 functions): Connect-AzAccount in Cloud Shell
 was replacing the pre-loaded user context with the Cloud Shell Managed Service Identity.
 All functions now skip Connect-AzAccount in Cloud Shell and use the existing user context.
 Added MSI guard: throws a clear error if Cloud Shell context is MSI instead of a user.

v2.3.6 (bug fix release)
- Fixed UnsupportedFilter error in TestAzure, ConnectAzure, and RevokeAzure: root-scope
 role assignment queries now include principalId filter required by the Azure ARM API.
 Unfiltered queries at '/' scope are rejected by the API with UnsupportedFilter.

v2.3.5 (bug fix release)
- Fixed Cloud Shell authentication: all functions now authenticate as the signed-in user
 instead of the VM Managed Identity (MSI). MSI has no Graph API or Exchange Admin
 permissions, causing all Cloud Shell runs to fail silently or throw.
- Fixed 5 misplaced backtick line-continuations in ConnectExchange, ConnectTeams,
 RevokeTeams that broke command parsing and caused ParameterBindingExceptions.
- Fixed -TimeoutSec and -ErrorAction Stop being bound to Where-Object instead of
 Invoke-RestMethod in ConnectAzure and RevokeAzure (6 occurrences).
- Fixed -ErrorAction Stop applied to Out-Null instead of Invoke-RestMethod in
 ConnectExchange and ConnectTeams, causing silent swallowing of HTTP errors.
- Fixed RevokeAzure: REST queries between elevation and try/finally block were
 unprotected - if they threw, User Access Administrator elevation was never removed.
 All post-elevation code is now inside a single try/finally block.
- Updated ConnectExchange and RevokeExchange .DESCRIPTION: now supported in Cloud Shell.

FileList

Version History

Version Downloads Last updated
2.3.19 (current version) 4 3/4/2026
2.3.18 4 3/4/2026
2.3.17 4 3/4/2026
2.3.16 6 3/4/2026
2.3.15 3 3/4/2026
2.3.14 4 3/4/2026
2.3.13 3 3/4/2026
2.3.12 4 3/3/2026
2.3.11 3 3/3/2026
2.3.10 5 3/3/2026
2.3.9 5 3/3/2026
2.3.8 4 3/3/2026
2.3.7 8 2/25/2026
2.3.6 4 2/25/2026
2.3.5 6 2/25/2026
2.3.4 4 2/25/2026
2.3.2 5 2/25/2026
2.3.1 6 2/25/2026
2.3.0 4 2/25/2026
2.2.9 5 2/25/2026
2.2.8 4 2/25/2026
2.2.7 6 2/25/2026
2.2.6 3 2/25/2026
2.2.5 5 2/25/2026
2.2.4 5 2/25/2026
2.2.3 4 2/25/2026
2.2.0 4 2/25/2026
2.1.9 5 2/24/2026
2.1.8 3 2/24/2026
2.1.7 8 2/21/2026
2.1.6 5 2/21/2026
2.1.5 9 2/20/2026
1.9.1 7 2/19/2026
Show more