TATER Ops - Cloud Script Execution
Run PowerShell maintenance, audit, and diagnostic scripts against your M365 / Entra ID tenant directly from TATER Ops. Cloud scripts execute in an Azure Automation runbook context with app-only Graph / EXO auth, capture stdout/stderr, and post results back to TATER for review, drift detection, and MCP-driven analysis.
Overview
Where the device-targeted Script Library dispatches PowerShell / Bash to endpoint hostnames via the TATER Agent, cloud scripts dispatch to a per-org Azure Automation runbook (Run-OpsScriptCloud) that connects to M365 services and runs your script against the tenant. This makes Ops the right place for:
- Help-desk user diagnostics ("Why can't Jeff send email?")
- Tenant maintenance audits (license utilization, stale guests, app permission drift)
- Security investigations (suspicious inbox rules, mailbox delegations, external sharing)
- One-off Graph / Exchange queries that you don't want to ad-hoc-paste into a tech's PowerShell window
Cloud scripts are distinguished from device scripts by setting executionTarget: cloud on the script record. The Run button still works the same way - just don't supply a hostname list (cloud scripts ignore targets).
Architecture
TATER Ops UI / MCP
│
│ POST /api/ops/scripts/:id/execute
▼
opsScripts.ts executeOpsScript
│
│ resolve org.opsScriptCloudWebhookUrl
│ create OpsScriptJob (targets=['__cloud__'], status=Queued)
│ POST { jobId, scriptContent, cloudAuthContext, callbackUrl } → webhook
▼
Azure Automation Account (aa-tater-{client})
│
│ Run-OpsScriptCloud.ps1
│ - install cert from Key Vault
│ - Connect-MgGraph / Connect-ExchangeOnline (per cloudAuthContext)
│ - execute script body via [ScriptBlock]::Create
│ - capture stdout, stderr, exitCode
▼
POST callbackUrl /api/ops/script-jobs/:id/cloud-result
│ X-Api-Key: <org-bound TATER key>
│ { status, stdout, stderr, exitCode, errorMessage }
▼
opsScripts.ts postOpsScriptCloudResult
│
│ update OpsScriptJob.status + cloudResult
│ audit-log via='cloud-runbook'
▼
get_script_job_status (poll for completion)
Setup (per client org)
Cloud script execution requires per-org infrastructure that mirrors the existing per-org scan runbook setup (see the Azure Runbook Setup Guide). If the org already has scanning infrastructure provisioned, you can re-use it:
- Publish the runbook. Upload
Runbooks/Run-OpsScriptCloud.ps1to the org's Azure Automation Account (e.g.aa-tater-cb). The runbook is PS 7.2-targeted. The publish flow is identical toScan-M365Cloud.ps1:az automation runbook create \ --resource-group rg-tater-prod \ --automation-account-name aa-tater-cb \ --name Run-OpsScriptCloud \ --type PowerShell72 \ --description "TATER Ops cloud script executor" # Upload draft content via REST API (CLI replace-content is unreliable for PS72) # See Runbooks/CLAUDE.md §12af for full upload script - Create a webhook on the published runbook. Recommended expiry: 5 years from now. Save the URL - it's a one-time reveal.
- Confirm Automation Variables already exist on the AA (these are shared with the scan runbook):
AppClientId- client tenant's app registrationTenantDomain- e.g.caronbletzer.onmicrosoft.comKeyVaultName-kv-tatersec-taterCertName- e.g.tater-cb-scannerApiKey- org-bound TATER API key (used to call back to the cloud-result endpoint)
- Store the webhook URL on the org record in TATER:
Or use the UI: TATER Manage → Connections → Microsoft Tenant → Ops Script Cloud Webhook URL.PUT /api/organizations/<orgId> { "opsScriptCloudWebhookUrl": "https://<guid>.webhook.eus.azure-automation.net/webhooks?token=..." } - Verify Graph + EXO modules are installed in the AA. The scan runbook depends on the same modules - if scans are working, cloud scripts will too:
Microsoft.Graph.Authentication(for graph-managed-identity context)ExchangeOnlineManagement 3.4+(for exo-managed-identity context)
stderr. Add permissions to the app reg if needed and re-consent.
Maintaining the runbook: do not load the Az modules
This is the single most important constraint on Run-OpsScriptCloud.ps1, and it is not
obvious from reading it.
Connect-AzAccount pulls Azure.Core 1.35 and Azure.Identity 1.6.1 into the
runbook session. Microsoft.Graph.Authentication then loads Azure.Core 1.51 alongside
them, and the credential the Graph SDK builds dies in a type initializer:
ClientCertificateCredential authentication failed: The type initializer for
'Azure.Core.Pipeline.RequestActivityPolicy' threw an exception.
Every cloud script attempted at one firm failed there — three of three — and the runbook
had never once succeeded. The fix was to stop loading Az at all: the signing certificate is fetched from
Key Vault over the sandbox’s managed-identity REST endpoint
(IDENTITY_ENDPOINT / IDENTITY_HEADER → vault.azure.net →
/secrets), which needs no module at all. The certificate is still installed into
CurrentUser\My, because Exchange Online and any script body that resolves a cert by
thumbprint expect to find it there.
Connect-AzAccount to this runbook, every cloud script breaks
again. Both auth contexts — Graph and Exchange Online — were verified working only
after the Az modules were removed from the session. Two plausible-sounding alternative explanations
were tested and disproved first: passing the certificate object instead of a thumbprint makes no
difference, and the modules are not categorically incompatible (the scanning runbook connects to
both in one session and succeeds). It is specifically the duplicate Azure.Core.
Auth contexts
The cloudAuthContext field on a script tells the runbook which M365 service to connect to before running the script body. Pick the lightest one that covers what the script needs - runbook overhead drops if it doesn't have to connect to EXO unnecessarily.
| Auth context | Connects to | Use when |
|---|---|---|
graph-managed-identity | Microsoft Graph | Default. Users, groups, sign-ins, licenses, app registrations, SharePoint (via Graph), Intune. |
exo-managed-identity | Exchange Online | Mailbox permissions, transport rules, inbox rules, anti-spam policies. Anything Get-EXOMailbox / Get-InboxRule family. |
azure-managed-identity | Azure Resource Manager | Azure subscription audits (NSG rules, Key Vault config, RBAC). Requires the AA's managed identity to have RBAC on the target subscriptions. |
Three ways to execute a cloud script
1. From the TATER Ops UI
Open Operations → Script Library, click ▶ Run on a cloud-target script. The targets field is hidden / ignored for cloud scripts. Click Run. The job appears in Recent Jobs → Active while the runbook executes, then moves to Inactive when results land.
2. From an MCP agent
The execute_script MCP tool now accepts cloud scripts. Targets array is optional. Returns immediately with the queued job ID; poll get_script_job_status for completion.
execute_script({
script_id: "ops-script-mfa-all-users-status",
run_as: "system"
})
// → "☁️ Queued cloud execution of MFA - All Users Status Audit
// - job opsjob-abc123. Runbook will report results back
// asynchronously. Poll with get_script_job_status."
3. Scheduled via Ops Schedules
Cloud scripts work in scheduled execution too - create a schedule with no targets, point it at the cloud script, and set the recurrence (e.g. weekly on Monday 06:00 UTC). The scheduler fires the same per-org webhook as the on-demand Run button — one shared dispatch, so a scheduled run cannot behave differently from a manual one (ADO #2270). A cloud run stays Running until the runbook posts its result back, then finalises Success, Failed or Drift from that result. If the org has no Ops Script Cloud Webhook URL configured, the run is recorded as Failed with that reason — not skipped, and not green. Until 2026-08-24 a scheduled cloud script recorded Skipped, advanced its next-run time and executed nothing, which looked identical to a working schedule; if you have cloud schedules that predate this, check their run history rather than their next-run time.
Template library
TATER ships a curated set of starter scripts in Runbooks/OpsCloudTemplates/ covering the most common help-desk and audit scenarios. These are opinionated, read-only, and tenant-safe - designed to drop into any client org without modification:
| Template | Category | Auth | Purpose |
|---|---|---|---|
User-FullDiagnostic.ps1 | User Diagnostics | Graph | Help-desk: full M365 profile + sign-ins + auth methods + groups + licenses + risk events for one user |
MFA-AllUsersStatus.ps1 | Identity & Access | Graph | Tenant-wide MFA registration audit - surfaces users without strong auth |
Licenses-SeatUtilization.ps1 | Tenant Health | Graph | Per-SKU seat utilization (excludes viral pools) - find over/under-provisioned licenses |
Mailbox-DelegationAudit.ps1 | Email Security | EXO | Full Access + Send As + Send on Behalf + calendar default sharing across every mailbox |
InboxRules-SuspiciousAudit.ps1 | Email Security | EXO | BEC indicator scan - external forwards, auto-hide alerts, mark-all-read rules |
Guests-StaleAccountAudit.ps1 | Identity & Access | Graph | Guests who have never signed in or are inactive > N days |
AppRegistrations-PermissionAudit.ps1 | Identity & Access | Graph | Service principals with high-impact Graph permissions |
SharePoint-ExternalSharingAudit.ps1 | Data Protection | Graph | Per-site sharing capability + anyone-link surface |
Every template includes a structured .METADATA comment block with the script's intended executionTarget, cloudAuthContext, riskLevel, default timeout, and TATERpedia slug. Importing today is manual; see "Future work" below for the planned auto-import flow.
Importing a template (manual)
- Open
Runbooks/OpsCloudTemplates/<name>.ps1from the TATER repo. - Read the
.METADATAblock at the top - that's your reference for the script's settings. - In TATER Ops: Script Library → + New Script.
- Fill in: name, description (copy
.SYNOPSIS), language=PowerShell, executionTarget=cloud, cloudAuthContext matching the metadata. - Paste the script body (everything below the comment block).
- Set defaultTimeoutSec and riskLevel from the metadata.
- Save. Click ▶ Run to execute on demand.
Writing your own templates
Cloud scripts are just PowerShell. They execute inside a [ScriptBlock]::Create scope after the runbook has already connected to Graph / EXO. You can use any cmdlet the auth context supports. Follow these conventions to keep scripts MCP-analyzable and drift-detectable:
- Output structured JSON at the end via
ConvertTo-Json -Depth N. The cloud-result endpoint stores stdout for MCP agents and Ops Schedule drift detection to compare across runs. - Wrap risky calls in try/catch and append errors to a
$errorsarray. Don'tthrow- the runbook needs to capture all output and post back even on partial failure. - Read-only by default. If you must mutate state, set
riskLevel: highand document rollback in the TATERpedia page for the script. - Cap output ~1MB. The cloud-result endpoint truncates stdout at 64KB. Use
-Top N/ sample parameters when developing. - Idempotent. Running twice should give equivalent output. Critical for scheduled drift detection.
Recommended frontmatter (a comment block at the top of every script):
<#
.SYNOPSIS
One-line summary.
.DESCRIPTION
Multi-paragraph detail of what the script does, what it surfaces,
and when to run it.
.METADATA
name: Human-readable script name
category: Group label for the Ops library
executionTarget: cloud
cloudAuthContext: graph-managed-identity
language: powershell
riskLevel: low
defaultTimeoutSec: 180
taterpediaSlug: ops-cloud-script-<slug>
defaultRunAs: system
.NOTES
Required Graph / EXO permissions, gotchas, related scripts.
#>
Troubleshooting example: Jeff Polchlopek
The canonical "help, my user is broken" workflow:
- An MCP-aware help-desk agent picks up the ticket. Step 0 of the Help-Desk Session Pattern -
list_tasker_taskswith Jeff's email to dedupe against any open ticket. - Agent calls
execute_scriptwithscript_id=ops-script-user-full-diagnosticandparameters: { UserPrincipalName: "jeff.polchlopek@..." }. - Run-OpsScriptCloud connects to Graph, calls
Get-MgUser,Get-MgUserAuthenticationMethod,Get-MgAuditLogSignIn, etc. against Jeff's account. ~30 seconds. - Results land in
cloudResult.stdoutas JSON. Agent pollsget_script_job_statusuntilstatus=Completed. - Agent parses the JSON. Sees Jeff's last sign-in failed with
50126("invalid credentials") from an IP in Vietnam at 03:14 UTC, plus three more failures in the next 10 minutes from rotating IPs. Risk event showsriskState: atRisk,riskLevel: high. - Agent appends findings to the Ops task ("Account targeted - recommend immediate password reset + revoke sessions"), creates a ConfigDoc "Jeff Polchlopek - account targeting investigation 2026-05-29", and updates the TATERpedia page "Account targeting response playbook" with the new IPs observed.
The whole loop runs without anyone opening Entra portal or pasting PowerShell into a tech's window. Everything is audit-logged with via='cloud-runbook' + via='mcp' attribution.
Security model
- Per-org webhook isolation. Each client org has its own webhook URL pointing at its own Azure Automation Account. Compromise of one client's webhook does not leak to others.
- SSRF-guarded. The webhook URL passes through
validateExternalUrlat both writer (PUT /organizations) and reader (executeOpsScript fetch site). - API key org binding. The cloud-result endpoint refuses to accept results from an unbound (tenant-level) API key. The runbook's
ApiKeyvariable must be a key bound to the specific org. - IDOR-safe callback. The cloud-result endpoint verifies
job.organizationId === apiKey.organizationIdbefore accepting results. A leaked client API key cannot post results for jobs in another org. - Audit attribution. Job creation logs
via='web'orvia='mcp'with the requesting user; result post logsvia='cloud-runbook'with the API key's prefix. Both sides of the loop are traceable. - Body cap. Cloud-result POST limited to 1.5 MB;
stdouttruncated to 64KB,stderrto 16KB stored on the job document.
Future work - the shared library vision
The current state requires you to manually copy templates from the repo into TATER Ops. The shared library roadmap closes that loop:
- Library API -
GET /api/ops/script-libraryreturns the catalog of available templates with their metadata, parsed at API build time fromRunbooks/OpsCloudTemplates/*.ps1. - One-click import - a + Browse Library button in the Ops Script Library. Click a template to preview metadata + script body. Click "Add to my org" to snapshot it into the org's OpsScripts container with a
librarySourceIdbackref. - TATERpedia auto-seed -
wikiSeedTimer.tswalks the template directory, parses the metadata block, and ensures a wiki page exists for each script'staterpediaSlug. Page covers script purpose, parameters, sample output, operational guidance, and a "View source" link. - Update notifications - when a library template gets a security or correctness fix, orgs that imported it see a "Library update available" prompt in the Ops Script Library row, with a diff view.
- Community contributions - users can submit new templates via the
create_script_templateMCP tool or pull request; submissions land inRunbooks/OpsCloudTemplates/community/after review. - MCP discovery - new tools
list_script_templatesandimport_script_template(slug)so AI agents can suggest "this is a known scenario - want me to import the User Diagnostic template?" instead of constructing scripts from scratch.
Until that ships, treat Runbooks/OpsCloudTemplates/ as the canonical reference and copy-paste manually. See its README for the full pattern guidelines.
Related guides
- Ops Script Library - device-targeted execution via TATER Agent
- Ops Scheduled Execution - recurring scripts with drift detection
- Azure Runbook Setup - per-org Automation Account provisioning
- Help-Desk Session Pattern - the MCP workflow for ticket resolution
- TATERpedia - the shared wiki where script docs live
TATER