--- name: Deny Graph API Batch Escape Hatch tags: - ms365 - deny-escape-hatches - ingress - iso27001-nist - soc2 publishedAt: 2026-07-12 description: | # ms365 / deny-graph-batch **Direction:** ingress (`tool_pre_invoke`) **Default:** deny the raw Graph batch tool, allow everything else **Package:** `ms365.ingress.deny_graph_batch` ## What it does Blocks the Microsoft 365 MCP server's raw-Graph passthrough tool (`graph-batch`, observed live as `ms365-graph-batch`). The tool accepts an arbitrary array of `{method, url, body}` requests in the Microsoft Graph `$batch` shape, so a single call can reach **any** Graph endpoint the token allows — deleting groups, creating mail rules, sending mail, changing permissions — bypassing every per-tool policy in this catalog. No argument inspection is attempted: this is a blanket deny with `default allow := false`. Callers whose IdP-asserted `groups` claim contains the placeholder group `m365-admin` are exempt and may use the tool. Missing or empty claims mean no exemption — the grant fails closed. This is the **bypass-closer** for the Microsoft 365 policy set: without it, every other ms365 policy (external-send guards, share-link guards, destructive-op freezes, …) can be trivially side-stepped through one batch call. Attach it first. ## Compliance alignment - **ISO 27001 A.8.2 / NIST 800-53 AC-6(9), AC-6(10)** — supports privileged access restriction on the MCP path: the one tool that carries full-tenant Graph reach is withheld from everyone except an explicitly named admin group, and attempted use by non-privileged callers is denied (and auditable via the gateway's decision logs). - **SOC 2 CC6.1 / CC6.6** — supports logical access security and boundary protection against external threats: the raw-Graph passthrough is the single tool that bypasses every per-tool boundary in this catalog, and it is closed to all non-admin callers on the agent channel. - **HIPAA §164.312(a)(1) / §164.308(a)(4)** — supports access control and information access management on a PHI-capable suite: `graph-batch` can reach any mailbox, drive, or SharePoint item the token allows, so denying it prevents the agent from side-stepping the minimum-necessary and access-control policies that protect ePHI. - **GDPR Art. 32 / Art. 25** — supports security of processing and data protection by default: the escape hatch can move personal data arbitrarily (sendMail, bulk export, permission changes), and a default-deny on it keeps agent access to personal data confined to the audited per-tool surfaces. ## Tool name matching The policy matches on `lower(input.resource.name)`: - `*-graph-batch` (suffix — any gateway server-name prefix) - `graph-batch` (exact — the server's own un-prefixed tool name) The DTwo gateway prefixes tool names with the configured MCP server name (the live deployment of `softeria/ms-365-mcp-server` exposes this tool as `ms365-graph-batch`), and that prefix is not standardized — matching on the suffix keeps the policy portable across server names. The bare `graph-batch` form is matched exactly as well, so a deployment that fronts the server with an empty or absent prefix (where the tool arrives as `graph-batch`, which does not end with a leading-hyphen `-graph-batch`) still fails closed rather than open. Verify the exact name your gateway sends with the dump-input debug technique before relying on this in production. ## Argument shape None inspected. `graph-batch` takes a `requests` array of `{method, url, body}` objects (Graph `$batch` shape); this policy deliberately does **not** try to classify individual sub-requests as safe or unsafe — URL-parsing allowlists over a passthrough surface are fragile (casing, encoding, `$batch`-relative URLs) and a single missed write defeats the entire catalog. Denying the tool outright is the only robust posture; admins who genuinely need it are exempted by group. ## Examples ### Allowed — any other ms365 tool ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "ms365-send-mail", "type": "tool" }, "payload": { "name": "ms365-send-mail", "args": { /* ... */ } } } } ``` `allow = true`, no reason. (Companion policies may still apply.) ### Denied — non-admin calls the batch tool ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "ms365-graph-batch", "type": "tool" }, "subject": { "sub": "user@example.com", "claims": { "groups": ["finance"] } }, "payload": { "name": "ms365-graph-batch", "args": { "requests": [{ "method": "DELETE", "url": "/groups/abc", "body": {} }] } } } } ``` `allow = false`, `reason = "The raw Graph batch tool is disabled (...)"`. ### Allowed — caller in the admin group ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "ms365-graph-batch", "type": "tool" }, "subject": { "sub": "admin@example.com", "claims": { "groups": ["m365-admin"] } }, "payload": { "name": "ms365-graph-batch", "args": { "requests": [] } } } } ``` `allow = true`. ## Composition This policy is single-purpose and is a **prerequisite** for the rest of the Microsoft 365 set: per-tool policies on `send-mail`, `create-drive-item-share-link`, `delete-*`, `create-mail-rule`, etc. are only meaningful when the batch escape hatch is closed. Attach it to every pipeline that fronts a Graph-backed M365 MCP server, alongside whichever per-tool ms365 policies your posture requires. ## Known limitations - **Lokka-style deployments are out of scope.** `merill/lokka` exposes a different single-tool passthrough (`Lokka-Microsoft`, args `apiType`/`path`/ `method`/`body`) that does not end with `-graph-batch` and therefore does not match this policy. If your gateway fronts a Lokka server, you need a separate policy that inspects Lokka's `method`/`path` arguments — name-based deny alone cannot distinguish its reads from writes. - **Group names are placeholders** — replace `m365-admin` with your IdP's group name at import time. The comparison is exact (case-sensitive), and the `groups` claim must be an array of strings; a missing, empty, or differently-shaped claim yields no exemption (fail closed). - **All-or-nothing.** Because no argument inspection is attempted, read-only batches (e.g., a batch of GETs) are denied for non-admins too. That is intentional: the specific `list-*`/`get-*` tools cover those needs. - **This policy must be attached for the rest of the ms365 catalog to hold.** A pipeline carrying only the per-tool ms365 policies but not this one leaves the batch bypass open. > **Compliance note.** This policy supports alignment with the cited framework controls **on the MCP path only**. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it. direction: ingress apps: - ms365 industries: [] bundles: - soc2 experimental: true schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package ms365.ingress.deny_graph_batch # Deny-by-default: only the explicit allow rules below permit the request. default allow := false # The raw-Graph passthrough tool. The gateway prefixes tool names with the # configured MCP server name (observed live as `ms365-graph-batch`), so we # match on the suffix to stay portable across naming conventions. Verify the # exact tool name on your gateway with the dump-input debug technique before # relying on this in production. is_graph_batch_tool if { endswith(lower(input.resource.name), "-graph-batch") } # Also match the server's own un-prefixed tool name. The softeria server names # this tool `graph-batch`; the `ms365-`/`contoso-` prefix is added by the # gateway and is "not standardized" — a deployment that fronts the server with # an empty/absent server-name prefix would send the bare `graph-batch`, which # does NOT end with a leading-hyphen "-graph-batch" and would otherwise fail # open. Match it exactly so the blanket deny holds regardless of prefixing. is_graph_batch_tool if { lower(input.resource.name) == "graph-batch" } # Groups asserted by the caller's IdP-issued JWT. A missing subject, missing # claims, or missing groups claim resolves to [] (or leaves the exemption rule # undefined) — either way the caller is not exempt: the grant fails closed. caller_groups := object.get(object.get(input.subject, "claims", {}), "groups", []) # Placeholder admin group — replace "m365-admin" with your IdP's group name # at import time. Exact match; the groups claim must be an array of strings. is_exempt_admin if { some group in caller_groups group == "m365-admin" } # Allow every tool that isn't the raw Graph batch passthrough. allow if { not is_graph_batch_tool } # Allow the batch tool only for callers in the admin group. allow if { is_graph_batch_tool is_exempt_admin } reasons contains "The raw Graph batch tool is disabled: one batch call can reach any Microsoft Graph endpoint and bypass per-tool policies. Use the specific Microsoft 365 tool for your task instead (for example send-mail, get-drive-item, or list-mail-messages). If you have a legitimate batch workflow, contact your administrator to request access." if { is_graph_batch_tool not is_exempt_admin } reason := joined if { count(reasons) > 0 reason_list := sort([r | some r in reasons]) joined := concat("; ", reason_list) } ```