Sending reports
The built-in Maester Cloud Runner runs your
tests for you over the Microsoft Graph connection from setup. You can also
send reports yourself, from wherever your runs already happen, with the
Send-MaesterCloudReport command.
Maester Cloud accepts two report families: Maester test results
(the JSON produced by Invoke-Maester) and the
Zero Trust Assessment report.
Who can upload
Uploads are authenticated with Entra tokens from the tenant your portal signs in with, and nobody can upload until an Owner allows them on the portal's Access page. That includes Owners themselves: Uploader is a separate permission from Reader, Admin, and Owner, and it starts off for everyone.
Send-MaesterCloudReport ships in the MaesterCloud module on the PowerShell Gallery. It needs
PowerShell 7.2 or later, plus the Microsoft.Graph.Authentication module for signing in as a
person, or the Azure CLI for automation.
Upload as yourself
- Sign in to the portal once, so you appear on the Access page.
- An Owner opens Access and ticks Uploader next to your name under Users. If you are the Owner, tick it for yourself.
- Install the module:
Install-Module MaesterCloud -Scope CurrentUser -
Check your access. A browser prompt opens; pick the account you use for the portal. Nothing is uploaded.
Test-MaesterCloudUpload -PortalUrl "https://your-portal-url" -AuthenticationMode Delegated - When every check passes, send a report with the command below. In the same PowerShell session, you won't be asked to sign in again.
Upload from automation
- Pick the identity the pipeline runs as, in the portal's tenant: an app registration (for GitHub Actions, give it a federated credential for your repository) or a managed identity (for Azure Automation or an Azure VM).
-
Find its two IDs:
- Application (client) ID: on the app registration's Overview, or the managed identity's Client ID.
- Service principal object ID: the Object ID under Enterprise applications, or the managed identity's Object (principal) ID. It is not the Object ID on the App registration page, and using that one is the most common reason uploads are refused.
- As an Owner, open Access and, under Automation uploaders, search for the app by name in Find the app, or paste its client ID or either object ID. Picking it fills in both IDs; each result links to the app in Entra so you can check it is the right one. Then select Add uploader. You can also type both IDs yourself: when the portal's tenant has a verified Microsoft Graph connection, the portal checks the object ID and replaces an App registration Object ID with the right one for you. Search needs that connection too.
- Select the new uploader and run the Grant Default Access PowerShell the page shows, once, as an Entra administrator. It assigns the identity to the portal's enterprise application, which is required when that application has Assignment required turned on.
-
Sign the pipeline in to the Azure CLI as that identity:
azure/loginin GitHub Actions,az login --service-principal, oraz login --identityfor a managed identity. -
Check its access, then send with
-AuthenticationMode AppOnly:Test-MaesterCloudUpload -PortalUrl "https://your-portal-url" -AuthenticationMode AppOnly
Check your access
Test-MaesterCloudUpload signs in the same way Send-MaesterCloudReport does,
reports each step, and uploads nothing. A failed step says what to change.
| Check | What it confirms |
|---|---|
Portal | The portal answers and is set up for Entra sign-in. |
Sign-in | A token for your portal's API was acquired. |
Identity | Your portal accepts the token, and which user or application it belongs to, with its IDs. A token from another tenant fails here. |
Uploader | The portal allows this identity to upload. When it doesn't, the detail names the IDs to enter on the Access page. |
Add -Quiet to get only $true or $false, for example to fail a
pipeline before it runs Maester.
Common problems
| You see | Fix |
|---|---|
Uploader fails: not an active uploader | An Owner ticks Uploader for that person on the Access page. Owners need it too. |
Uploader fails: No active automation uploader matches this identity | Add the identity under Automation uploaders with the two IDs the message shows, or enable it if it is disabled. |
Identity fails: The portal rejected the token | Usually the account or app is in another tenant. Uploaders must be in the tenant the portal signs in with. |
Sign-in fails in AppOnly mode with AADSTS65001 | The Azure CLI is signed in as a person, not the automation identity. Sign it in as the service principal or managed identity. |
AADSTS501051 or AADSTS50105 | The portal's enterprise application requires assignment. Run the Grant Default Access script for an automation uploader, or assign the user to the enterprise application. |
Uploader shows Skip | Your portal predates this check (Identity shows Skip too). Update the portal, or send a report to confirm. |
The command
The portal's Get started page shows this command with your portal URL filled in.
Send-MaesterCloudReport `
-PortalUrl "https://your-portal-url" `
-JsonPath ./TestResults.json `
-HtmlPath ./TestResults.html `
-Source "Manual upload" `
-AuthenticationMode Delegated -PortalUrlis your Maester Cloud portal. The command reads the tenant, client ID, and scopes it needs from the portal, so there is nothing else to configure.-JsonPathis required.-HtmlPathis optional but recommended, because the original HTML report is stored and viewable from the portal.-AuthenticationModeisDelegatedfor a person, who signs in through a browser prompt, orAppOnlyfor automation signed in to the Azure CLI as a service principal or managed identity. The default,Auto, tries app-only first and then delegated.-Sourceis a free-text label ("GitHub Actions", "Azure Automation") shown on every run for provenance.
If a machine can't reach the PowerShell Gallery, your portal also serves the same commands:
download /scripts/MaesterCloudReport.ps1 and /scripts/MaesterCloudUploader.psm1
from your portal URL into one folder, then Import-Module ./MaesterCloudUploader.psm1.
Use your portal's test configuration
The built-in runner applies what you set on the portal's Configuration page automatically. When you run
Maester yourself, call Get-MaesterCloudConfig before Invoke-Maester so your run
matches: it writes the tenant's emergency access accounts into Custom/maester-config.json in
your tests folder, and returns the tests turned off (or awaiting review) for that tenant to pass to
-ExcludeTag.
$config = Get-MaesterCloudConfig `
-PortalUrl "https://your-portal-url" `
-TenantId $tenantId `
-Path ./tests `
-AuthenticationMode AppOnly
Invoke-Maester -Path ./tests -ExcludeTag $config.ExcludeTag Not every Maester test carries its id as a tag, so your portal also drops turned-off tests when the report arrives: a test you turned off never lands in your history either way.
What happens after you send
- Upload-init: your portal validates the token (issuer, audience, and tenant must match your deployment, and the caller must be an allowed uploader) and opens a new run.
- Artifacts upload: the JSON (and HTML) are sent through the same authenticated API into your deployment's private storage. The run is only marked complete after every file has arrived, so processing never starts on a half-uploaded report.
- Processing: your deployment identifies the report family, confirms the assessed tenant, and parses every test into a stored result: id, title, outcome, severity, block, tags, and the Microsoft product it belongs to. Tests turned off for that tenant, or new and awaiting review, are left out. Transient failures retry automatically.
- Recording: the run appears in the portal with pass/fail/skip counts; the raw files stay downloadable as evidence; the run is diffed against your previous one; and, if you've set them up, drift emails go out.
Run it on a schedule
Drift is only as fresh as your runs. If you don't use the built-in runner,
schedule a daily run (a GitHub Actions workflow, an Azure DevOps pipeline,
or Azure Automation) ending with Send-MaesterCloudReport -AuthenticationMode AppOnly.
Real PowerShell-written files (including UTF-8 BOMs) are handled as-is; no
massaging needed.
In GitHub Actions, sign in with a federated credential on the uploader's app registration, so the workflow stores no secret:
permissions:
id-token: write
contents: read
steps:
# ... run Maester so ./TestResults.json exists ...
- uses: azure/login@v2
with:
tenant-id: ${{ vars.MAESTER_CLOUD_TENANT_ID }}
client-id: ${{ vars.MAESTER_CLOUD_UPLOADER_CLIENT_ID }}
allow-no-subscriptions: true
- name: Send to Maester Cloud
shell: pwsh
run: |
Install-Module MaesterCloud -Scope CurrentUser -Force
Send-MaesterCloudReport `
-PortalUrl "https://your-portal-url" `
-JsonPath ./TestResults.json `
-HtmlPath ./TestResults.html `
-Source "GitHub Actions" `
-AuthenticationMode AppOnly Statuses you'll see
| Status | Meaning |
|---|---|
Succeeded | Parsed and recorded; results, products, and drift are available. |
Processing / Uploaded | In flight, usually seconds. |
Retrying | A transient failure; the workflow retries automatically. |
Failed | The report couldn't be understood (e.g. no tenant ID). The raw files are kept so nothing is lost; the error is shown on the run. |