Skip to main content

Demo: Rehearsal as a pre-publish / pre-revoke control (CVE-2026-35435)

A sales-grade walkthrough on the contoso tenant. It answers the CISO question that follows every agent-platform CVE: "which agents are live, what can each one reach, and what would a NEW one be able to do before I approve it?"

What this demo is. A deterministic, read-only map of the standing authority an agent holds or would gain. We do not detect, exploit, or simulate the CVE. We make Microsoft's own post-CVE guidance ("inventory your agent footprint") literal.

What this demo is NOT. Not prompt-injection detection, not a kill switch, not threat detection. Rehearsal rehearses consequences of an access change over modeled RBAC. Every claim on screen is projected/observed, never asserted as fact about behavior.


The framing (narration only — never product copy)

"CVE-2026-35435: improper access control in Azure AI Foundry M365 published agents lets an attacker elevate privileges over a network. Microsoft's guidance was to inventory your published-agent footprint. Like Midnight Blizzard in 2024, the danger isn't the exploit itself — it's the standing access the identity already holds, or would hold. Rehearsal is how you see that access before it's abused."

Name only CVE-2026-35435 and Midnight Blizzard (both Microsoft's own published incidents). If asked about other recent agent/SaaS incidents, keep them generic: "a backup vendor's service principal", "a third-party AI integration". Do not name third-party victims.


Prep (once per environment)

The demo needs the rehearsal_enabled feature flag ON for contoso. It is off by default and both rehearsal surfaces 403 / render nothing without it.

# super-admin session (local dev: AUTH_PROVIDER=dev → /auth/callback?code=dev-bypass)
curl -X PATCH "<BASE>/api/v1/admin/tenants/contoso/features" \
-H "Content-Type: application/json" -H "X-Tenant-Id: contoso" \
-b <session-jar> -d '{"rehearsal_enabled": true}'
# → {"data":{"rehearsal_enabled":true}}

Pin contoso first (MANDATORY — the props hardcode the anchor). The Beat-2 prop pages hardcode the role id cdf4693d… and the result text "1 new path". The live cone is recomputed from current tenant state, so a drifted contoso will 404 the deep-link (ROLE_NOT_FOUND) or show a count that contradicts the prop. Before presenting, on the demo preview:

gh workflow run pr-preview-admin.yml --ref main -f instance=pr-N -f action=restore-contoso
gh workflow run pr-preview-admin.yml --ref main -f instance=pr-N -f action=enable-rehearsal -f rehearsal_tenant=contoso

Then open the deep-link yourself and confirm the cone shows exactly ONE appeared path to v02scopea079 (matching the prop's "1 new path"). If it shows 0 or a different count, re-pin the prop text / role id before you present — never quote a number the live cone won't reproduce.

Pin the demo anchors before presenting (data drifts between snapshots — verify, don't trust this file's ids). The anchors below were verified on the 2026-06-10 contoso snapshot (191 entities), which matched live dev exactly.

RoleWhat it isHow to re-find it
Beat-1 revocation entityd9272705710e7f38a106aee5 — Foundry project identity sv0-foundry-agent-project/projects/sv0-proj-default, runs 3 agents, exactly one HAS_ROLE (Foundry Project Member) so the revocation panel auto-picks itproject identity with HAS_ROLE count == 1 and ≥1 finding
Beat-1 findingeval:da73c78e73096f90a60791215e6ec552dormant_authority / high on that identityany finding whose entity_id is the identity above
Beat-2 rolecdf4693d17546ed4c984034fContributor → full control-plane management of storage account v02scopea079 (incl. listKeys → data; not direct blob read/write) (90f9d617a18c41c600fa8fca)highest-impact role (Contributor on a storage account); confirm runDeploymentRehearsal returns a non-empty reach_cone

Beat 1 — footprint + revocation (shipped features, real data)

1a. The footprint

Open the Foundry-agent footprint:

<BASE>/t/contoso/graph?source_system=azure_ai_foundry

"Here are your published Foundry agents and everything they touch — deterministically, from your real connector data. Six agents, each running as a project identity, each carrying critical findings: LLM egress, scope drift, reachable sensitive domains. Can you produce this list today, on demand? This is that list."

1b. Revocation rehearsal — what's safe to cut during a patch window

Open the dormant-authority finding on the project identity:

<BASE>/t/contoso/findings/eval:da73c78e73096f90a60791215e6ec552

Scroll to Rehearse revocation → it auto-selects the single role → click Rehearse revocation.

"Patch guidance says pull access during the window. What actually breaks? Rehearsal replays it: 4 access paths lost, 0 would break, 37 findings affected. Every path is tagged No observed use in 90 days — this authority is dormant. You can cut it during the window with zero operational risk. Read-only — nothing was changed."

This is the pre-revoke control: cut with confidence, not guesswork.


Beat 2 — deployment rehearsal (the aha; new in this pilot)

"Now the other direction. Your team wants to publish a new SOC-triage agent that requests the Contributor role on your storage. Before you approve it — what would it be able to do on day one?"

2a. The trigger — where the proposed change comes from

Mandatory spoken beat — say this BEFORE the click, do not wait to be asked: "The trigger screen you're about to see is a stand-in for your CI / request surface. Everything from the click onward — the graph, the reach, the numbers — is the live, deterministic engine running on this tenant's real RBAC." (The prop page also carries a visible "Example" label, so this is reinforced on-screen, not just verbally.)

The proposed grant arrives the way a real change arrives: as a pre-apply artifact your team already produces. Lead with this one flow.

Open the prop page that stands in for the customer's pull request:

<BASE>/demo/deployment-rehearsal-check.html

"This is the PR that publishes the agent. The team ran az deployment group what-if — Azure's own pre-apply preview — and it shows one change: grant Contributor to the sv0-soc-triage service principal on v02scopea079. Watch the checks."

A SecurityV0 / deployment-rehearsal check runs and posts back the result: "1 new path to sensitive storage (v02scopea079)." The page frames itself as an example of where this plugs in, and is explicit that the CI integration is roadmap: "in production this runs as a pre-apply check — a Terraform run task, a GitHub deployment-protection check, or an Azure DevOps check — not yet shipped; what is live today is the projected-reach engine behind View rehearsal." Say that out loud — do not imply we ship a CI check today. Click View rehearsal →.

Name the asset, don't read the GUID. v02scopea079 is a storage-account id; in the room, say what it holds for this customer ("the customer-PII export store", "the billing ledger"). The risk only lands when the target is a business asset, not a token. Resolve it from the customer's data-classification before you present.

This opens: <BASE>/t/contoso/graph?propose=cdf4693d17546ed4c984034f:sv0-soc-triage-proposed.

2b. The cone — projected day-one reach

A PROPOSED agent node fades in (amber ring + pill), an amber edge lights the path it would gain, and a corner caption appears. Narrate it accurately — the on-screen caption currently overstates the reach (it says "delete / read / write"; the control-plane-accurate wording fix is tracked in sv0-platform#1557), so say:

"sv0-soc-triage-proposed is not deployed and has never run. If you approve the Contributor role, it would gain full control-plane management of the v02scopea079 storage account on day one — including reading its access keys, which is the path to the stored data. Two identities hold comparable reach today. This path does not exist until you approve the role."

Accuracy note (binding). Contributor is a control-plane role; it does NOT grant blob data-plane read/write directly — it can listKeys (one hop) or needs a data role. Say "control-plane management incl. key read → data access", not a flat "read/write the data". The on-screen caption currently overstates this; the wording + on-screen key-auth caveat fix is tracked in sv0-platform#1557. If asked about data access, give the key-read hop honestly.

Click the PROPOSED node → the drawer shows the projected agent (it isn't in your inventory; it's a projection).

"This is the pre-publish control. You see the blast radius of an access grant before it exists, deterministically, from your own RBAC. Same engine as the revocation you just saw — one model, run in both directions."

Compliance hook (for the partner deck, not product copy): this is the control gap an auditor maps to least-privilege (NIST AC-6) and agent-identity governance (OWASP Agentic ASI03 Identity Abuse / ASI08 Insufficient Control) — a privileged, unowned agent path approved without seeing its reach.

The human / plain-English front door is a deck slide, not a live screen. Where the input is going long-term — an analyst describes a change in English, an LLM drafts the exact grant, a human confirms it, and only then does the deterministic engine run — is the "How this works in production" architecture below. Show it as a roadmap diagram if an IAM/SOC buyer asks "how do I feed this to you"; do not stage it as a working screen, and do not volunteer "LLM" (determinism is the headline). The earlier scripted "ask in English" prop was cut for this reason.

Close

"Rehearsal is the pre-publish and the pre-revoke control for agent authority. Read-only. Deterministic. No agent was deployed, no access was changed, nothing was written. You decide with the blast radius in front of you instead of after the incident."


Honesty guardrails (binding)

  • On-screen copy says projected / over modeled role-based grants, never "this agent did X". The agent has never executed.
  • SAFE_NO_OBSERVED_USE ("No observed use") is evidence-window-bounded (90d) and proxy-grade for Foundry — say so if asked.
  • Coverage caveat (shown in the verdict): not modeled — PIM / just-in-time elevation, Conditional Access, deny assignments, resource-local (key-based) auth.
  • The "N identities hold comparable reach today" number is the deterministic pre_existing_paths_to_same_resource count, not an estimate.
  • We map the authority path the damage would ride through. We do not model the prompt injection, token forgery, or the CVE itself.

Mechanics (for the operator running it)

  • Both beats are gated on rehearsal_enabled (per-tenant flag, off by default).
  • Beat-2 deep link ?propose=<roleId>:<agentName> auto-seeds the graph focus on the role (neighborhood, depth 2) so the role → permission → resource chain renders; the overlay then adds the synthetic agent + amber HAS_ROLE edge client-side. No data is written.
  • Backend: GET /api/v1/rehearsal/deployment?agent_name=&role_id= (read-only). Engine: the additive twin of revocation — inject a synthetic identity + one role edge, re-materialize all paths, diff appeared = augmented − baseline.
  • The Beat-2a trigger page (/demo/deployment-rehearsal-check.html) is a static demo prop served from ui/public/demo/ (sv0-platform #1554). It illustrates only the input surface — the az deployment group what-if block is canned, framed on-screen as an example, with the CI integration stated as roadmap. It links to the real ?propose= deep-link. Nothing is parsed; no LLM runs. The earlier "ask in plain English" prop was cut (round-2 review): it took on the "an AI decides my access?" objection for an unshipped feature; the LLM front door is now a deck diagram (see "How this works in production"), not a screen.

How this works in production (the honest answer to "how does the node appear?")

The demo fakes the trigger; the architecture behind it is real and is what we sell. The principle: many ways in, one deterministic core.

non-deterministic INPUT  (Terraform plan · ARM/Bicep what-if · az CLI scripts · plain English)

LLM TRANSLATOR interprets intent → a candidate grant {principal, role, scope}.
│ It interprets only. It never computes the blast radius.
══ HUMAN CONFIRMATION GATE ══ ← the determinism boundary is crossed HERE
│ "Interpreted as: grant Contributor to sv0-soc-triage on v02scopea079 — confirm?"
DETERMINISTIC ENGINE frozen graph snapshot → inject the confirmed edge → re-materialize →
│ diff → projected reach cone. Read-only, byte-reproducible.
DETERMINISTIC OUTPUT the cone you see in Beat-2b.

What this means for the three questions a CISO asks:

  • "How would I feed this to you in production?" Through whatever artifact you already produce before you apply: a Terraform plan (terraform show -json), an ARM/Bicep what-if, a CloudFormation change set, or a plain-English request. Each is a thin input adapter; the most credible for an Azure estate is the native az deployment group what-if. No source-code access — these are pre-apply artifacts, not your repo.
  • "Where does the integration live?" The recognizable shape is a pre-apply check — a Terraform run task, a GitHub deployment-protection check, or an Azure DevOps check — that receives the proposed change, runs the rehearsal, and posts back a result + a link to the cone (advisory, or mandatory to block the apply). This is the same pattern as Snyk / Checkov / OPA gates a security team already trusts.
  • "Can I trust the answer if an LLM is involved?" The LLM only interprets a fuzzy description into a candidate grant that a human confirms. Past the confirmation gate, the blast radius is computed by a deterministic engine over a frozen snapshot — same input, same answer, every time, re-runnable for audit. The fuzziness is quarantined to interpretation and never reaches the math. Read-only throughout; nothing is written to Azure.

One model, run in both directions, fed many ways. Revocation (Beat 1) and deployment (Beat 2) are the same deterministic engine; the input adapters and the optional LLM translator sit in front of it, never inside it.