--- name: "JIRA: Redact Sensitive Information from Issue Views" tags: - jira - atlassian - pii - secrets - dlp - redaction - egress publishedAt: 2026-06-15 description: | # jira / redact-sensitive-info **Direction:** egress (`tool_post_invoke`) **Default:** allow (transform-only — never denies) **Package:** `jira.egress.redact_sensitive_info` ## What it does Redacts sensitive content from the responses of JIRA issue-view tools before they reach the caller. The goal is durable: prevent PII, credentials, and secrets from leaking out through JIRA issue bodies, comments, worklogs, and linked-issue content. It is a transform-only egress policy — it never denies a call, it only rewrites matching content in the response to `[REDACTED]`. Any tool that is not a JIRA issue-view tool passes through untouched. ## Why egress and not ingress The risk here is *reading* secrets that already live inside JIRA issues (pasted into a description, a comment, or a worklog). Those values exist regardless of this gateway, so there is nothing to block at ingress — the leak happens when the content is returned to an MCP client. Masking on the egress (response) path is the only place to catch it. ## Scope / tool matching The policy applies only to JIRA tools that surface issue-body content, matched by tool-name **suffix** so it stays portable regardless of the MCP server name prefix the gateway adds (`atlassian-`, `atlassian-jira-mcp-`, etc.): - `*-getjiraissue` - `*-searchjiraissuesusingjql` - `*-getcommentsforjiraissue` - `*-getjiraissuecomments` - `*-getworklogsforjiraissue` - `*-getjiraissueworklog` - `*-getjiraissueremoteissuelinks` The tool name is read from both `input.resource.name` and `input.tool_metadata.name`, so the policy matches regardless of which surface the gateway populates first. Confirm the exact tool names your gateway sends using the dump-input debug technique before relying on this in production; if your JIRA MCP server exposes other issue-view tools, add their suffixes to `view_tool_suffixes`. ## What gets redacted Redaction works two ways. **By pattern** in any string value: - PII — US SSN, credit card numbers, email addresses, US phone numbers - Cloud / SaaS keys (vendor-prefixed shapes) — AWS access keys (`AKIA…`, `ASIA…`), Google API keys (`AIza…`) and OAuth tokens (`ya29.…`), GitHub tokens (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`), GitLab PATs (`glpat-…`), Slack tokens (`xox[abprs]-…`), Stripe keys (`sk_live_`, `sk_test_`, `pk_live_`, `pk_test_`) - OAuth / bearer — `Authorization: Bearer …` headers and JWTs - Generic `key: value` / `key=value` secret assignments (api_key, password, secret, token, client_secret, credentials, …) - Database connection strings with embedded credentials (postgres, mysql, mongodb, redis, amqp, mssql/sqlserver URIs; JDBC strings; ADO.NET-style `Server=…;User Id=…;Password=…`) - PEM private-key blocks (`-----BEGIN … PRIVATE KEY----- … -----END …-----`) And **by field name** — any response field whose key is one of `password`, `passwd`, `pwd`, `secret`, `api_key`, `apikey`, `token`, `access_token`, `refresh_token`, `id_token`, `client_secret`, `private_key`, `connection_string`, or `credentials`. Matches are replaced with `[REDACTED]`. ## Examples ### Redacted (JIRA issue view) ```jsonc { "input": { "action": "tool_post_invoke", "resource": { "name": "atlassian-jira-mcp-getjiraissue", "type": "tool" }, "tool_metadata": { "name": "atlassian-jira-mcp-getjiraissue" } } } ``` `allow = true`, with a `transform` that supplies the redaction patterns, field names, and `replacement = "[REDACTED]"` for the gateway to apply to the response body, plus `reason = "Sensitive content redacted from Jira response"` so the redaction is explained in the dashboard. ### Passed through (any non-issue-view tool) ```jsonc { "input": { "action": "tool_post_invoke", "resource": { "name": "atlassian-jira-mcp-getjiraproject", "type": "tool" }, "tool_metadata": { "name": "atlassian-jira-mcp-getjiraproject" } } } ``` `allow = true`, no `transform` — the response is returned unchanged. ## Composition This policy is single-purpose and transform-only, so it composes cleanly with access-control policies on the same egress pipeline. Useful companions: - An ingress policy that blocks *writing* secrets into JIRA in the first place (so new issues don't accumulate credentials). - Equivalent egress redaction policies for other Atlassian apps (Confluence, Bitbucket). See the [`bundles/atlassian`](../../../bundles/atlassian/README.md) bundle for the curated Atlassian set. ## Known limitations - **Regex over plain text.** Detection is pattern-based, so novel or non-standard token formats, short-lived rotating tokens, and custom-shaped secrets may not be caught. Treat this as a high-signal layer, not a complete DLP solution. - **Scope is issue-view tools only.** Other JIRA tools that might surface issue content (e.g. bulk export or attachment-download endpoints) are not inspected unless you add their suffixes to `view_tool_suffixes`. - **No identity-based exemptions.** All callers get the same redaction. Add an `input.subject.claims`-gated branch if a break-glass role needs raw values. direction: egress apps: - jira industries: [] bundles: - atlassian schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package jira.egress.redact_sensitive_info # Transform-only policy — never denies, only redacts sensitive content. # Scoped to JIRA tools that return issue-body content. Other tools (including # non-JIRA egress) pass through this policy untouched. default allow := true # ----------------------------------------------------------------------------- # Scope: JIRA tools that surface issue content (summary, description, comments, # worklog, custom fields, linked issues). Suffix matching makes this work # regardless of the MCP server name (atlassian-, atlassian-jira-mcp-, etc.). # ----------------------------------------------------------------------------- view_tool_suffixes := { "-getjiraissue", "-searchjiraissuesusingjql", "-getcommentsforjiraissue", "-getjiraissuecomments", "-getworklogsforjiraissue", "-getjiraissueworklog", "-getjiraissueremoteissuelinks", } is_jira_issue_view if { some suffix in view_tool_suffixes name := lower(input.resource.name) endswith(name, suffix) } is_jira_issue_view if { # Egress hooks also expose the tool name under tool_metadata.name — check # both so we match regardless of which surface the gateway populates first. some suffix in view_tool_suffixes name := lower(object.get(input.tool_metadata, "name", "")) endswith(name, suffix) } # ----------------------------------------------------------------------------- # Redaction transform # ----------------------------------------------------------------------------- transform := { "redact_patterns": [ # ---- PII ---- `\d{3}-\d{2}-\d{4}`, # US SSN `\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}`, # Credit card `[\w.-]+@[\w.-]+\.[\w.-]+`, # Email `\+?1?[- .]?\(?\d{3}\)?[- .]?\d{3}[- .]?\d{4}`, # US phone # ---- Cloud / SaaS API keys (vendor-prefixed shapes) ---- `AKIA[0-9A-Z]{16}`, # AWS access key ID `ASIA[0-9A-Z]{16}`, # AWS temporary (STS) access key `AIza[0-9A-Za-z_-]{35}`, # Google API key `ya29\.[0-9A-Za-z_-]+`, # Google OAuth access token `ghp_[A-Za-z0-9]{36}`, # GitHub personal access token `gho_[A-Za-z0-9]{36}`, # GitHub OAuth token `ghu_[A-Za-z0-9]{36}`, # GitHub user-to-server token `ghs_[A-Za-z0-9]{36}`, # GitHub server-to-server token `ghr_[A-Za-z0-9]{36}`, # GitHub refresh token `glpat-[A-Za-z0-9_-]{20}`, # GitLab personal access token `xox[abprs]-[A-Za-z0-9-]+`, # Slack tokens (bot/app/user/refresh/etc.) `sk_live_[A-Za-z0-9]{24,}`, # Stripe live secret key `sk_test_[A-Za-z0-9]{24,}`, # Stripe test secret key `pk_live_[A-Za-z0-9]{24,}`, # Stripe live publishable key `pk_test_[A-Za-z0-9]{24,}`, # Stripe test publishable key # ---- OAuth / bearer ---- `(?i)bearer\s+[A-Za-z0-9._~+/-]+=*`, # Authorization: Bearer `eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+`, # JWT (header.payload.signature, base64url) # ---- Generic key/value secret patterns ---- `(?i)(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*\S+`, # api_key=..., api-key: ... `(?i)(?:password|passwd|pwd|secret|token|credentials|client[_-]?secret)\s*[:=]\s*\S+`, # ---- Database connection strings ---- # URI form with embedded user:password (postgres, mysql, mongo, redis, amqp, mssql) `(?i)(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis(?:s)?|amqps?|mssql|sqlserver)://[^:\s]+:[^@\s]+@[^/\s]+(?:/\S*)?`, `(?i)jdbc:[a-z0-9]+:[^\s]+`, # JDBC strings `(?i)(?:Server|Data Source)\s*=\s*[^;]+;\s*(?:User Id|UID)\s*=\s*[^;]+;\s*(?:Password|PWD)\s*=\s*[^;]+`, # ---- PEM private keys (multi-line, lazy match between BEGIN/END markers) ---- `-----BEGIN [A-Z ]+PRIVATE KEY-----.+?-----END [A-Z ]+PRIVATE KEY-----`, ], "redact_fields": [ "password", "passwd", "pwd", "secret", "api_key", "apikey", "token", "access_token", "refresh_token", "id_token", "client_secret", "private_key", "connection_string", "credentials", ], "replacement": "[REDACTED]", } if { is_jira_issue_view } # Surfaced on the decision event whenever the redaction is in scope, so the # dashboard can explain the rewrite. reason := "Sensitive content redacted from Jira response" if { is_jira_issue_view } ```