JIRA: Freeze Destructive Issue Operations
Freezes the three irreversible Jira operations on the agent channel: jira delete issue, jira remove issue link, and jira remove watcher.
- Direction
- ingress
- Rego package
jira.ingress.freeze_destructive_ops- App
- jira
- Bundles
- atlassiansoc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:a1fdaf3066fa072df7ee75cfd8b685b139f903092e0bc1fe5bc38ef92175bda3
jiraatlassianfreeze-destructive-opsrecord-integritydata-protectioningresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on the frozen destructive tools, allow everything else
Package: jira.ingress.freeze_destructive_ops
What it does
Freezes the three irreversible Jira operations on the agent channel:
jira_delete_issue, jira_remove_issue_link, and jira_remove_watcher. Any
tool call whose lowercased name ends with one of those suffixes is denied for
all callers, with an optional break-glass exemption for a placeholder
jira-admins group. Every other Jira tool — reads, searches, issue
creates/edits, comments, worklogs, transitions, link creation, watcher
addition — passes through untouched.
The check runs at ingress, before the call reaches the Jira MCP server, so a frozen operation never executes: the issue, link, or watcher survives an injected prompt or an erring agent. When destruction is genuinely required, a human performs it through a reviewed native Jira workflow (or, for planned maintenance, from an account in the break-glass group).
These three tools exist only on the community sooperset/mcp-atlassian server. The official Atlassian Rovo MCP server exposes no delete tools at all (verified in the app landscape note — it cannot delete issues, links, pages, or comments). So on official-connector deployments this policy is a zero-cost safety net that never fires; on community / Data-Center deployments it is the control that actually stops destructive agent behaviour. Because it costs nothing where it cannot fire, it is worth keeping attached everywhere as a categorical backstop.
Relationship to deny-write-sensitive-projects
This policy complements
jira/deny-write-sensitive-projects,
which blocks writes only within a configured sensitive-project set. That
policy leaves destruction available outside the sensitive set; this one makes
the three irreversible operations unavailable to the agent org-wide, on every
project, so records survive agent error or prompt injection regardless of
which project they live in. Run both: sensitive-project write fencing plus a
categorical destruction freeze.
Compliance alignment
This policy instantiates the record-freeze family (PF-06,
freeze-destructive-ops) on Jira's destructive surface, and supports alignment
with:
- SOC 2 PI1.5 — integrity of stored records: denies agent-driven deletion that would compromise the completeness of stored issue data.
- HIPAA §164.312(c) — integrity (anti-alteration) of ePHI that may live in Jira issues; §164.530(c) — privacy safeguards, by removing an irreversible destruction path from the agent channel.
- GDPR Art. 5(1)(d) — accuracy: prevents mass agent-driven loss of records (an accuracy/availability failure) by freezing destruction over MCP.
Tool name matching
The gateway prefixes tool names with the configured MCP server name (e.g.
mcp-atlassian-jira_delete_issue), and that prefix is not standardized. The
policy matches on the tool-name suffix so it stays portable across
server-name conventions, and lowercases the name first so casing never causes a
silent miss:
*jira_delete_issue*jira_remove_issue_link*jira_remove_watcher
These are the community sooperset/mcp-atlassian names (verified in the
landscape note). The official Rovo server has no delete tools, so there is no
official-naming variant to add. If your community deployment renames these
tools, add the new suffixes to destructive_tool_suffixes in policy.md.
The name is read from both the PARC field (input.resource.name) and the
legacy alias (input.payload.name) via object.get chains, and the two are
matched independently — a request that omits the resource block, or one
carrying a malformed (non-string) value in either field, still cannot skip the
match. Each field is coerced to a lowercased string (a number, null, array, or
object resolves to the empty string), so a non-string value in one field can
never suppress a genuine destructive suffix in the other.
name_of also fails closed against a malformed container: if resource
or payload is itself a bare string (e.g. "resource": "jira_delete_issue"
instead of {"name": ...}), the string is matched directly rather than lost
to object.get's default. A number/array/null container, or one with a
non-string name, resolves to the empty string. The gateway always builds
these as objects, so this is defense-in-depth, not a reachable gateway path.
Argument shape
None. The decision uses only the tool name (input.resource.name, with the
legacy input.payload.name as fallback) and the caller's identity
(input.subject.claims.groups); input.payload.args is never read. A frozen
call is denied even if it arrives with missing, empty, or unexpected arguments;
there is no arg shape an attacker can craft to slip past it.
Identity / break-glass
An optional allow if branch exempts members of a placeholder jira-admins
group, read from the caller's IdP-issued groups claim via
object.get(object.get(input.subject, "claims", {}), "groups", []). This lets a
designated maintenance account perform destruction through the agent during
planned cleanup without detaching the policy. The check fails closed: a caller
with no subject, no claims, no groups, or no matching group is not
exempt and the operation is denied. The groups claim is honored only when
it is a JSON array (is_array guard): a bare string, or an object/map shape
such as {"role": "jira-admins"}, is rejected — without that guard a Rego
some group in groups would iterate an object's values and let a map-shaped
claim satisfy the grant.
To freeze destruction for everyone (including admins), delete the break-glass
allow if { is_destructive_tool; caller_is_admin } branch — the
default allow := false then denies all callers on the three frozen tools.
Examples
Denied (agent tries to delete an issue)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-jira_delete_issue", "type": "tool" },
"subject": { "sub": "google-apps|agent@acme.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "mcp-atlassian-jira_delete_issue",
"args": { "issue_key": "PROJ-123" }
}
}
}
allow = false,
reason = "This destructive Jira operation ... is frozen on the agent channel. ...".
Allowed (non-destructive Jira write)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-jira_add_watcher", "type": "tool" },
"payload": {
"name": "mcp-atlassian-jira_add_watcher",
"args": { "issue_key": "PROJ-123", "account_id": "5b10..." }
}
}
}
allow = true, no reason. (Adding a watcher is allowed; only removing one is
frozen.)
Allowed (break-glass admin deletes during maintenance)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-jira_delete_issue", "type": "tool" },
"subject": { "sub": "google-apps|admin@acme.com", "claims": { "groups": ["jira-admins"] } },
"payload": {
"name": "mcp-atlassian-jira_delete_issue",
"args": { "issue_key": "PROJ-123" }
}
}
}
allow = true, no reason.
Composition
Single-purpose by design. Useful companions in the
atlassian bundle:
jira/deny-write-sensitive-projects— write-side fencing for designated Jira projects (see above).confluence/freeze-page-deletion— the parallel freeze forconfluence_delete_page/confluence_delete_attachment(this policy only fences Jira destruction).- A publication-control policy on Confluence page creates/updates to keep drafts from publishing org-wide.
Known limitations
- Exact-suffix match only. The rule fires on names ending in
jira_delete_issue/jira_remove_issue_link/jira_remove_watcher. A future community tool with a different name (e.g.jira_delete_issuesorjira_purge_issue) would not be covered — add its suffix if your server exposes one. It does not fire on names where the destructive verb is embedded mid-string. - Suffix match assumes the literal registered tool name. A name padded
with surrounding whitespace or a trailing newline (e.g.
"jira_delete_issue\n"or" jira_delete_issue ") does not end in a frozen suffix and would pass through. This is not reachable through the gateway: the upstream MCP server routes only its exactly-registered tool names, so a padded name never resolves to a real destructive tool and never executes. Atests.yamlcase pins this behaviour so a future change to addtrim/ normalisation is a deliberate decision, not an accident. - Group names are placeholders — replace
jira-adminswith your IdP's group name at import time. The break-glass branch is only as trustworthy as thegroupsclaim your IdP issues; Auth0 and Entra ID both require explicit configuration to emitgroups. If callers can self-assert group membership, remap it to a claim your IdP controls, or remove the branch entirely to freeze destruction for all callers. With nogroupsclaim the policy still fails closed: destruction is denied for everyone. - Community-server-specific. These tool names exist only on sooperset/mcp-atlassian. On official Rovo deployments the policy is inert (no delete tools exist), which is intended defense-in-depth, not a gap.
- Destruction via other paths is out of reach. This only covers the MCP
channel. A user deleting an issue in the Jira web UI or via the REST API is
outside the gateway's scope by design. Destruction-by-overwrite (e.g.
jira_update_issueblanking fields) is also out of scope — that belongs to a write-gating companion policy, not this delete/remove freeze.
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.
Policy source (Rego)
package jira.ingress.freeze_destructive_ops
# Deny-by-default: only the explicit allow rules below permit a request. Every
# non-destructive tool is allowed; the three frozen tools are allowed only for
# the break-glass admin group.
default allow := false
# -----------------------------------------------------------------------------
# FROZEN TOOLS: irreversible community sooperset/mcp-atlassian Jira operations.
# These names exist only on the community server; the official Rovo server has
# no delete tools, so the rule simply never fires there. Suffix matching keeps
# the policy portable across gateway server-name prefixes (e.g.
# `mcp-atlassian-jira_delete_issue`).
# -----------------------------------------------------------------------------
destructive_tool_suffixes := {
"jira_delete_issue",
"jira_remove_issue_link",
"jira_remove_watcher",
}
# -----------------------------------------------------------------------------
# BREAK-GLASS: members of this IdP group may still run destructive operations
# (planned maintenance). Placeholder — remap to your IdP's group name at import
# time. Delete the `allow if { is_destructive_tool; caller_is_admin }` branch
# below to freeze destruction for everyone, including admins.
# -----------------------------------------------------------------------------
admin_group := "jira-admins"
# Tool name is read via object.get chains from BOTH the PARC field
# (input.resource.name) and the legacy alias (input.payload.name), so a request
# that somehow omits the resource block still cannot skip matching (red-team
# hardening: missing resource must not fail open). name_of coerces to a
# lowercased string and handles three container shapes so the match cannot be
# skipped by a malformed request:
# 1. normal: input[key] is an object with a string "name".
# 2. bare-string container: input[key] is ITSELF the tool-name string (a
# malformed request that sends `resource: "jira_delete_issue"` instead of
# `resource: {"name": "..."}`). object.get(<string>, "name", "") returns
# the default "" — so without this branch the destructive name would be
# lost and the call would fail OPEN. Match the container value directly.
# 3. anything else (missing key, number, array, null, or a non-string name):
# resolves to "" rather than leaving the rule undefined.
name_of(key) := lower(container) if {
container := object.get(input, key, {})
is_string(container)
}
name_of(key) := lower(v) if {
container := object.get(input, key, {})
not is_string(container)
v := object.get(container, "name", "")
is_string(v)
}
name_of(key) := "" if {
container := object.get(input, key, {})
not is_string(container)
v := object.get(container, "name", "")
not is_string(v)
}
resource_name := name_of("resource")
payload_name := name_of("payload")
# The two names are matched independently. Keeping separate branches means a
# malformed (non-string) value in one field cannot suppress a real destructive
# suffix in the other.
is_destructive_tool if {
some suffix in destructive_tool_suffixes
endswith(resource_name, suffix)
}
is_destructive_tool if {
some suffix in destructive_tool_suffixes
endswith(payload_name, suffix)
}
# Groups from the caller's IdP-issued JWT. Fail closed: a missing subject,
# missing claims, missing groups, or a non-array groups value all yield "not
# admin", so the destructive call is denied.
caller_is_admin if {
claims := object.get(input.subject, "claims", {})
groups := object.get(claims, "groups", [])
# Only honor an array-shaped groups claim. `some x in obj` iterates an
# object's VALUES, so without this guard an object-shaped claim such as
# {"role": "jira-admins"} would silently satisfy the break-glass grant.
# is_array forces every non-array shape (string, object, number, null) to
# fail closed — no exemption.
is_array(groups)
some group in groups
group == admin_group
}
# Allow everything that is not a frozen destructive tool.
allow if {
not is_destructive_tool
}
# Break-glass: allow a frozen destructive operation for members of the admin group.
allow if {
is_destructive_tool
caller_is_admin
}
# Deny reason for a non-admin caller hitting a frozen destructive tool.
reasons contains "This destructive Jira operation (delete issue, remove issue link, or remove watcher) is frozen on the agent channel because it is irreversible: records must survive agent error and prompt injection. Perform the deletion through a human-reviewed native Jira workflow instead. For legitimate admin cleanup, ask a member of your Jira admin group (placeholder: jira-admins) to run it, or ask your InfoSec team to add you to that group." if {
is_destructive_tool
not caller_is_admin
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa