JIRA: Cap Field and Result Exposure on Reads
Narrows the breadth of JIRA read requests before they run, on the two read surfaces that can pull large amounts of issue data into model context:
- Direction
- ingress
- Rego package
jira.ingress.cap_read_field_exposure- App
- jira
- Bundles
- soc2gdpr-ccpaatlassian
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:637a57a8d7b022024175ead61be45bbac0f1c2fbd2e9e9302077df36694c24ef
jiraatlassiancap-bulk-exportdata-minimisationingresssoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow (transform-only — never denies)
Package: jira.ingress.cap_read_field_exposure
What it does
Narrows the breadth of JIRA read requests before they run, on the two read surfaces that can pull large amounts of issue data into model context:
- Strips over-broad field tokens. When a search or issue-view call's
fieldsargument contains a broad wildcard —"*all"or"*navigable", which expand to every / every navigable field (including custom fields — salary bands, customer identifiers — that the default field set deliberately omits) — or"comment"(which pulls full comment threads), those tokens are removed and the remaining fields pass through. If stripping leaves nothing, the request falls back to a conservative approximation of Jira's default safe field subset (default_safe_fieldsinpolicy.md— a documented tuning knob). Matching is case-insensitive and whitespace-tolerant ("*ALL"," *all "are stripped too), both the official array form and the community comma-separated string form offieldsare handled, and — because Jira comma-splits afieldsvalue server-side — a single array element that packs a banned token behind a comma ("summary,*all") is dropped whole rather than slipping through. - Clamps search page size. On the search tools,
maxResultsis rewritten down to 50 whenever it is higher than 50, absent, non-positive, or non-numeric — bounding bulk trawling well inside the server's own 100-row hard limit. A numeric value already in[1, 50]passes through unchanged. Note the absent case is deliberately clamped: a search that names no page size would otherwise run at the server's default, so the ceiling is injected.
All other arguments (jql, cloudId, issueIdOrKey, …) are preserved via
object.union. Calls are never denied — the request always proceeds,
just narrower. Any tool outside the matched read set, any egress hook, and
any call by an exempt power user passes through untouched. An issue-view
call with no fields argument at all is left untouched (issue-view tools
take no maxResults, so nothing is injected there).
Identity exemption: callers whose input.subject.claims.groups include
the placeholder group jira-power-users bypass the policy entirely. The
exemption fails closed — missing subject, claims, or groups means the caps
apply.
Compliance alignment
This policy instantiates field-level minimum-necessary / data-minimisation (family PF-08, cap-bulk-export) and supports alignment with:
- SOC 2 CC6.7 — supports the restriction on transmission/movement/removal of information by bounding how many fields and rows a single agent read can move out of Jira.
- HIPAA §164.502(b) / §164.514(d) — supports the minimum-necessary standard: issue trackers routinely accumulate PHI in custom fields and comment threads; reads stay scoped to the default field subset unless a designated role needs more.
- GDPR Art. 5(1)(c) — data minimisation on the agent channel: the query's breadth is minimised before it reaches Jira, so personal data in custom fields and comments is never pulled into model context.
- CCPA 11 CCR §7002 — supports proportionality: collection and use of personal information stays proportionate to the task rather than defaulting to every-field, maximum-row retrieval.
Why ingress
Once a fields: ["*all"], maxResults: 100 search has executed, the data is
already in the response, the model context, and the gateway logs — an
egress policy can only mask patterns in text that has already been fetched.
Rewriting the request at ingress is the only place the breadth of the
read can be controlled. The companion egress policy
(redact-sensitive-info) then masks sensitive values in whatever the
narrowed query still returns.
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. atlassian-searchjiraissuesusingjql), so matching is by
case-insensitive suffix to stay portable across deployments:
- Search tools (fields stripped +
maxResultsclamped):*searchjiraissuesusingjql(official Rovo / Claude connector, verified) and*jira_search(sooperset community server). - Issue-view tools (fields stripped only):
*getjiraissue(official, verified — samefields/"*all"semantics as search) and*jira_get_issue(community).
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.
Argument shape
searchJiraIssuesUsingJqltakesjql,fields[](array of strings; default is a safe subset;"*all"pulls every field and"comment"pulls full comment threads), andmaxResults≤ 100 — verified from the live official-connector schemas.getJiraIssuehas the samefields/"*all"semantics.- The community server's
fieldsparameter is conventionally a comma-separated string; the policy handles that form with the same strip-and-fallback logic (rejoining with commas). Community per-field schemas are not independently verified — see Known limitations. - Every argument is read with
object.get— no direct indexing — so malformed or missing args never crash the policy; they simply pass through (a non-array, non-stringfieldsvalue is not rewritten).
Examples
Untouched
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "atlassian-getjiraissue", "type": "tool" },
"payload": {
"name": "atlassian-getjiraissue",
"args": { "issueIdOrKey": "ENG-42" }
}
}
}
allow = true, no transform — no fields argument, and issue-view tools
take no maxResults.
Transformed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "atlassian-searchjiraissuesusingjql", "type": "tool" },
"payload": {
"name": "atlassian-searchjiraissuesusingjql",
"args": { "jql": "project = ENG", "fields": ["summary", "*all", "status"], "maxResults": 100 }
}
}
}
allow = true, transform rewrites the args to
{ "jql": "project = ENG", "fields": ["summary", "status"], "maxResults": 50 }.
Had fields been ["*all"] alone, the rewrite would fall back to the
default safe subset instead.
Exempt
The same search issued by a caller whose input.subject.claims.groups
include jira-power-users passes through completely unchanged.
Composition
This policy narrows the query; it does not inspect returned values or fence which projects are reachable. Useful companions:
apps/jira/redact-sensitive-info— egress redaction that masks PII/credentials in whatever the narrowed read still returns (defense in depth).apps/jira/deny-view-search-sensitive-projects— the project-level fence; this policy bounds breadth within the projects that fence still allows.apps/jira/freeze-destructive-ops— guards the destructive surface of community-server deployments.
Known limitations
- Group names are placeholders — replace
jira-power-userswith your IdP's group name at import time. The exemption expectsgroupsto be an array of strings; a single-stringgroupsclaim is not matched (the caps then apply — fail closed for the grant). - Community argument shapes are unverified. The sooperset server's
jira_search/jira_get_issuefield parameter (comma-separated string) and its page-size parameter name were not independently verified in the landscape research. Notably, if the community server reads its page size from a key other thanmaxResults(e.g.limit), that key is not clamped by this policy — confirm from your server'stools/listand extend the clamp before relying on it. - Per-call caps do not stop patient pagination. Stateless Rego cannot track cumulative volume: an agent can still page through results 50 rows at a time, and named non-sensitive fields are never stripped. Use gateway audit logs to spot high-frequency crawls, and pair with the project fence and egress redaction listed above.
- Only the
"*all","*navigable", and"comment"tokens are stripped. A caller who explicitly enumerates individual custom field IDs (e.g.customfield_10042) still receives them; blocking specific fields by name is a separate allowlist policy."*navigable"is a documented Jira wildcard, not independently verified in the landscape note — it is stripped as defense in depth. If Jira adds a further broad-expansion token, add it tobanned_field_tokens. - Absent
maxResultsis injected, not left alone. A bare search gainsmaxResults: 50. If your server's default page size is already lower, this is a no-op in practice but the argument will appear in the call. - Other bulk-read tools are out of scope. Community tools like
jira_get_project_issuesorjira_batch_get_changelogshave their own shapes; cover them with additional policies if your deployment exposes them.
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.cap_read_field_exposure
# Transform-only policy — never denies, only narrows the read before it runs.
default allow := true
# --- Tuning knobs ---------------------------------------------------------------
# Ceiling for search page size. Jira's own hard limit is 100; 50 keeps a single
# call from pulling the maximum the API allows.
max_results_ceiling := 50
# Field tokens stripped from `fields`: "*all" and "*navigable" are Jira wildcards
# that expand to every / every navigable field (including custom fields the
# default subset deliberately omits — salary bands, customer identifiers) and
# "comment" pulls full comment threads. "*navigable" is a documented Jira
# wildcard, not verified in the landscape note — stripped as defense in depth.
banned_field_tokens := {"*all", "*navigable", "comment"}
# Fallback when stripping leaves no fields: a conservative approximation of
# Jira's default safe field subset. Tune to your environment.
default_safe_fields := [
"summary",
"status",
"issuetype",
"priority",
"assignee",
"reporter",
"created",
"updated",
"labels",
]
# Callers in this IdP group bypass the policy entirely. Placeholder name —
# replace with your IdP's group name at import time.
power_user_group := "jira-power-users"
# --- Tool matching ----------------------------------------------------------------
# The gateway prefixes tool names with the configured MCP server name, so we
# match case-insensitively by suffix to stay portable. Official Rovo/Claude
# connector names are verified; community (sooperset) names per its docs.
# Search tools: fields stripped AND maxResults clamped.
search_tool_suffixes := ["searchjiraissuesusingjql", "jira_search"]
# Issue-view tools: fields stripped only (they take no maxResults).
get_tool_suffixes := ["getjiraissue", "jira_get_issue"]
is_search_tool if {
some suffix in search_tool_suffixes
endswith(lower(input.resource.name), suffix)
}
is_get_tool if {
some suffix in get_tool_suffixes
endswith(lower(input.resource.name), suffix)
}
is_read_tool if is_search_tool
is_read_tool if is_get_tool
# The caps apply to the ingress hook only — egress hooks on the same tool
# names pass through.
is_capped_read_call if {
input.action == "tool_pre_invoke"
is_read_tool
}
# --- Identity exemption -----------------------------------------------------------
# Fail closed for the grant: a missing subject, claims, or groups claim means
# the caller is NOT exempt and the caps apply.
caller_groups := object.get(
object.get(object.get(input, "subject", {}), "claims", {}),
"groups",
[],
)
is_power_user if {
some g in caller_groups
is_string(g)
lower(g) == power_user_group
}
# --- Argument access (object.get everywhere — fields may be missing) ---------------
args := object.get(object.get(input, "payload", {}), "args", {})
fields_value := object.get(args, "fields", null)
# --- fields rewrite: array form (official Rovo/Claude connector, verified) ---------
# A token is banned when — lowercased and whitespace-trimmed — it matches a
# banned wildcard/token. Each array element is *also* comma-split first, because
# Jira splits a comma-joined `fields` value server-side: an element such as
# "*all,comment" or "summary,*all" would otherwise pass through atomically and
# still expand to every field. If any comma-part is banned the whole element is
# dropped (fail-safe: over-stripping falls back to the default safe subset).
# Non-string entries are never banned (and are preserved as-is).
is_banned_token(f) if {
is_string(f)
some part in split(f, ",")
banned_field_tokens[lower(trim_space(part))]
}
banned_in_array if {
is_array(fields_value)
some f in fields_value
is_banned_token(f)
}
# Original order preserved; only banned tokens are dropped.
sanitized_array := [f |
is_array(fields_value)
some f in fields_value
not is_banned_token(f)
]
# --- fields rewrite: comma-separated string form (community servers; shape ---------
# --- not independently verified — see Known limitations) ---------------------------
banned_in_string if {
is_string(fields_value)
some t in split(fields_value, ",")
banned_field_tokens[lower(trim_space(t))]
}
sanitized_string_tokens := [trimmed |
is_string(fields_value)
some t in split(fields_value, ",")
trimmed := trim_space(t)
not banned_field_tokens[lower(trimmed)]
trimmed != ""
]
# --- Patches ------------------------------------------------------------------------
# Each patch defaults to {} so `patch` below is always defined; the transform
# fires only when at least one patch has content.
default fields_patch := {}
fields_patch := {"fields": sanitized_array} if {
banned_in_array
count(sanitized_array) > 0
}
fields_patch := {"fields": default_safe_fields} if {
banned_in_array
count(sanitized_array) == 0
}
fields_patch := {"fields": concat(",", sanitized_string_tokens)} if {
banned_in_string
count(sanitized_string_tokens) > 0
}
fields_patch := {"fields": concat(",", default_safe_fields)} if {
banned_in_string
count(sanitized_string_tokens) == 0
}
max_results_value := object.get(args, "maxResults", null)
# Clamp when maxResults is absent — a bare search would otherwise run at the
# server's own default page size.
needs_max_results_clamp if {
max_results_value == null
}
# Clamp a numeric maxResults above the ceiling.
needs_max_results_clamp if {
is_number(max_results_value)
max_results_value > max_results_ceiling
}
# Clamp non-positive page sizes: several servers read 0/negative as "use the
# default" or "unbounded", so anything outside [1, ceiling] is rewritten.
needs_max_results_clamp if {
is_number(max_results_value)
max_results_value < 1
}
# Fail safe: a non-numeric maxResults is replaced with the ceiling rather than
# letting the server's parsing decide.
needs_max_results_clamp if {
max_results_value != null
not is_number(max_results_value)
}
default max_results_patch := {}
# The page-size clamp applies to search tools only — issue-view tools take no
# maxResults, so nothing is ever injected into them.
max_results_patch := {"maxResults": max_results_ceiling} if {
is_search_tool
needs_max_results_clamp
}
patch := object.union(fields_patch, max_results_patch)
# --- Transform ------------------------------------------------------------------------
# Rewrites only the offending keys; every other argument is preserved.
transform := {"transformed_payload": object.union(args, patch)} if {
is_capped_read_call
not is_power_user
count(patch) > 0
} 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