Skip to content

[Bug] destroy_azure blast radius: subscription-wide tag delete with no anchor, dry-run, or confirmation #8

Description

@sjohnston1972

Background

destroy_azure (in backend/src/claude/custom-tools.ts, runDestroy ~line 604) tears down resources for a project/topology. In tag-filter mode it runs az group list across the entire subscription and deletes every resource group whose tags match ALL the supplied filters, plus every standalone matching resource — with az group delete --yes --no-wait.

Problem / Goal

The delete is powerful and largely unguarded, so a too-broad or wrong tag filter can destroy far more than intended:

  • Loose/empty filters aren't rejected meaningfully. The only guard (~line 611) requires either a resource group name or at least one non-empty tag entry. A single common tag (e.g. mcp-project=lab — or worse, some generic key that also exists on unrelated resources) is enough to sweep the whole subscription for matches. There's no requirement that the canonical mcp-project tag be present, and no lower bound on specificity.
  • No plan / dry-run. The tool deletes immediately; the model (and user) never see the match set before it's gone. Combined with --no-wait, deletions are irreversible before anyone can react.
  • Project-wide teardown filters by mcp-project only (chat.ts:227), which is by design, but with no confirmation step a mis-scoped project name deletes everything tagged for that project across the subscription.

Given the SP typically has broad rights on the subscription, the blast radius of a bad filter is "everything the SP can see."

Where to look

  • backend/src/claude/custom-tools.tsrunDestroy ~604: filter guard ~611, RG listing/JMESPath ~637-661, delete loop ~651-653.
  • backend/src/routes/chat.ts:223-249 — the teardown system-prompt block that tells Claude which tags to pass.

Suggested approach

  1. Require a project anchor. Reject a tag-filter destroy unless it includes a non-empty mcp-project (or azure-mcp-project) tag, so a destroy can never run on an unanchored/overly-generic filter. Keep the explicit resource_group_name mode as-is.
  2. Add a dry-run / plan mode. Compute and return the matched RG names + standalone resource ids without deleting, unless an explicit confirm: true (or similar) is set. This lets the loop surface "about to delete N groups: …" and require a confirming turn.
  3. Sanity-cap the match set. If the number of matched resource groups exceeds a configurable threshold, refuse and return the list rather than mass-deleting, on the assumption the filter is wrong.

Reproduce locally: point AZURE_CLI_IMAGE at a stub that echoes canned az group list output, call runDestroy with an empty-value/over-broad filter, and confirm current behaviour proceeds to the delete branch; after the fix, confirm it refuses or returns a plan.

Acceptance criteria

  • A tag-filter destroy without a non-empty mcp-project/azure-mcp-project tag is refused with a clear message and deletes nothing.
  • With a plan/dry-run flag, the tool returns the matched RGs/resources and performs no deletion.
  • Deletion only happens when explicitly confirmed (flag set) or via the explicit resource_group_name path.
  • Legitimate per-topology and project-wide teardown still work when properly anchored + confirmed.

Testing

  • Unit tests for the guard: missing project tag → refused; present → allowed. Empty match set and over-threshold match set → refused/plan.
  • Integration test (stubbed az) verifying dry-run returns matches and does not reach the delete commands.

Out of scope

  • Command-injection hardening and tag validation (separate parent) — but coordinate the "require non-empty project tag" rule with that parent's tag validation.
  • Stage-based authorization (separate parent).

Sub-issues: (1) require a project anchor + reject over-broad/empty filters, (2) add a dry-run/plan-then-confirm flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions