Fence Sensitive Box Folders by IdP Group
Fences pinned sensitive Box subtrees (HR, Finance, Legal, …) by ID.
- Direction
- ingress
- Rego package
box.ingress.fence_sensitive_folders- App
- box
- Bundles
- soc2hipaagdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:44bf6f7b7b2874b2aa81637a97f8375767cd559bb8566acac8be339f6dac5ab4
boxfence-sensitive-scopesingresssoc2hipaagdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny fenced targets for callers outside the mapped group, allow otherwise
Package: box.ingress.fence_sensitive_folders
What it does
Fences pinned sensitive Box subtrees (HR, Finance, Legal, …) by ID. The policy carries two
placeholder maps — fenced_folders and fenced_files — that pair Box folder/file IDs with the
IdP group required to touch them (e.g. folder 1234567890 → finance). At ingress it denies:
- File reads, moves, and copies —
get_file_content,get_download_url,get_file_preview,ai_qa_single_file,ai_qa_multi_file,ai_extract_*,move_file,copy_file(official server) andbox_file_download_tool/box_file_text_extract_tool/box_file_copy_tool(community server) — when the targetfile_id(or anyitems[].id/file_ids[]entry on multi-file AI tools) appears infenced_filesand the caller lacks the mapped group. - Folder listings, details, moves, and copies —
list_folder_content_by_folder_id,get_folder_details,move_folder,copy_folder(official) andbox_folder_items_list_tool/box_folder_move_tool(community) — when the targetfolder_idappears infenced_foldersand the caller lacks the mapped group. - Search — community
box_search_toolis denied for non-privileged callers unlessancestor_folder_idsis present and every fenced ID in it maps to a group the caller holds (an unscoped search is allowed only for callers holding every fenced group, since it can surface content from any tree). Officialsearch_files_keyword/search_files_metadatafolder-scoping is checked the same way where the scoping argument is present.
Group membership is read from input.subject.claims.groups via object.get chains and fails
closed: a missing, empty, or malformed groups claim never grants access to a fenced target.
All tools this policy does not inspect pass through untouched.
Compliance alignment
- SOC 2 C1.1 — supports identification and protection of confidential information by gating agent access to designated confidential Box trees to their mapped groups; P4.1 — supports limiting personal-information use to identified purposes by keeping PI-bearing folders behind role fences on the agent channel.
- HIPAA §164.308(a)(4) — supports information access management: access to PHI-bearing Box folders is authorized by IdP group on the MCP path; §164.522(a) — fenced file IDs can encode agreed-to restrictions on specific patient records.
- GDPR Art. 9 — supports special-category protection by fencing folders holding health, HR, or other Art. 9 data; CPRA §1798.121 — supports the right to limit use of sensitive personal information by fencing SPI folders to a minimal group.
Tool name matching
Tool names are matched case-insensitively as an exact name or by -/_-separated suffix, so
the policy tolerates any gateway server-name prefix (e.g. box-remote-get_file_content):
- Official (mcp.box.com):
get_file_content,get_download_url,get_file_preview,ai_qa_single_file,ai_qa_multi_file,move_file,copy_file,list_folder_content_by_folder_id,get_folder_details,move_folder,copy_folder,search_files_keyword,search_files_metadata; any tool whose name containsai_extract_(covers all sixai_extract_*variants). - Community (box-community/mcp-server-box):
box_file_download_tool,box_file_text_extract_tool,box_file_copy_tool,box_folder_items_list_tool,box_folder_move_tool,box_search_tool.
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.
Argument shape
- File tools:
file_id(string or number). Multi-file AI tools:items[].id; a flatfile_ids[]array is also checked defensively. - Folder tools:
folder_id(string or number). - Search tools:
ancestor_folder_idsas an array of IDs or a comma-separated string. On the official server the exact scoping field name is partially unverified (see Known limitations).
Examples
Allowed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "box-remote-get_file_content", "type": "tool" },
"subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "box-remote-get_file_content",
"args": { "file_id": "5550001111" } // not in fenced_files
}
}
}
allow = true, no reason.
Denied
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "box-remote-get_file_content", "type": "tool" },
"subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "box-remote-get_file_content",
"args": { "file_id": "9876543210" } // fenced_files → finance
}
}
}
allow = false, reason = "Box file 9876543210 is fenced as sensitive and requires the 'finance' IdP group. (...)".
Composition
This policy is single-purpose: it fences reads/moves/search of pinned sensitive IDs. Useful companions:
- An external-sharing guard on
create_collaboration/*shared_link*tools, so fenced content that an authorized caller reads cannot be re-shared outward. - An egress PII/PHI redaction policy on
get_file_content,ai_qa_*, and search responses — it also mops up snippets an unscoped official search may surface (see Known limitations). - A destructive-op gate for the community server's
box_file_delete_tool/box_folder_delete_tool.
Known limitations
- Literal-ID matching only — no ancestry resolution. The policy matches the exact IDs in its maps and cannot resolve Box folder ancestry statelessly. A file reached through an unlisted descendant ID is not fenced. Customers must pin the sensitive subtree's folder IDs (the root and any high-value descendant folders/files) at import time; the shipped ID lists are placeholders.
- Broad operations can still surface fenced descendants. The same statelessness means an
operation whose own target is an unfenced ancestor of a fenced tree is not caught:
a recursive community listing of a parent folder (
box_folder_items_list_toolwithis_recursive: true, e.g.folder_id: "0") enumerates items inside fenced subtrees, and a community search scoped to an unfenced ancestor (ancestor_folder_ids: ["0"], or a malformed non-comma scope string that parses to a single non-fenced token) reads into fenced subtrees — both satisfy the "scoped is safe" allow rule and thereby sidestep the unscoped-search restriction. The scoped checks only test whether the literal ancestor IDs supplied are themselves fenced. Mitigate by pinning the sensitive root folder IDs (so a recursive listing or scoped search of the root itself is denied), pairing with an egress redaction policy (see Composition), and/or restricting recursive listing and root-scoped search operationally. - Move/copy argument names partially unverified. The
copy_file/box_file_copy_toolfences readfile_id, andmove_folder/copy_folder/box_folder_move_toolreadfolder_id, matching the underlying Box API and the move/read tools already covered. Box does not publish these schemas; if a live tool names the moved/copied source item differently, that fence silently does not fire. Confirm against a livetools/list. - Placeholder configuration. Folder/file IDs and group names are placeholders — replace
financewith your IdP's group name at import time, and replace the IDs with your real Box folder/file IDs. - Official search scoping is partially unverified. Box's docs do not publish the official
server's full argument schemas; this policy assumes the scoping argument is named
ancestor_folder_ids(matching the underlying Box Search API). If the live tool uses a different field name, the scoping check silently never fires. Confirm against a livetools/listbefore relying on it. - Unscoped official search passes. Per the design,
search_files_keyword/search_files_metadataare only checked when the scoping argument is present — an unscoped official search can still surface fenced-tree snippets (Box enforces the caller's own Box permissions, but not this policy's group fence). Pair with an egress redaction policy, or tighten this policy to deny unscoped official search if that residual is unacceptable. - AI-extract argument shapes are partially unverified. The
ai_extract_*andai_qa_multi_filechecks readfile_id,items[].id, andfile_ids[]; if the live schema nests file references elsewhere, those calls pass unchecked. - Metadata reads are not fenced.
get_file_details,list_file_comments, and communitybox_file_info_toolcan still reveal fenced item names/metadata; this policy targets content reads, listings, moves, and search only. groupsclaim must be an array of strings. A string-valued or otherwise malformed claim fails closed (fenced targets deny). If your IdP emits groups under a different claim name (e.g. a namespaced custom claim), updatecaller_groupsin the Rego.
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 box.ingress.fence_sensitive_folders
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# ---------------------------------------------------------------------------
# Fence configuration — PLACEHOLDERS, replace at import time.
#
# The policy matches literal IDs and cannot resolve Box folder ancestry
# statelessly, so pin the folder IDs of every sensitive subtree root AND any
# high-value descendant folders here. Group names must match your IdP's
# `groups` claim values (compared case-insensitively).
# Box folder ID -> IdP group required to list/read/search inside it.
fenced_folders := {
"1234567890": "finance", # e.g. /Finance subtree root
"1234567891": "hr", # e.g. /HR subtree root
"1234567892": "legal", # e.g. /Legal subtree root
}
# Box file ID -> IdP group required to read or move it. Use for pinned
# high-value documents (payroll exports, cap tables, case files).
fenced_files := {
"9876543210": "finance", # e.g. payroll-2026.xlsx
"9876543211": "hr", # e.g. employee-roster.xlsx
}
# ---------------------------------------------------------------------------
# Identity — read groups via object.get chains so a missing subject/claims/
# groups fails closed (no group -> no access to fenced targets).
caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])
# True when the caller's groups claim (an array of strings) contains `group`.
# A malformed (non-array) claim makes the iteration fail -> fail closed.
caller_has_group(group) if {
some g in caller_groups
lower(g) == lower(group)
}
# Every distinct group referenced by the fence maps. A caller holding all of
# them may run unscoped community searches (they could read every tree anyway).
fence_groups contains group if {
some _, group in fenced_folders
}
fence_groups contains group if {
some _, group in fenced_files
}
privileged_search_caller if {
every group in fence_groups {
caller_has_group(group)
}
}
# ---------------------------------------------------------------------------
# Tool matching. The gateway prefixes tool names with the configured MCP
# server name (separator not standardized), so match the exact name or a
# `-`/`_`-separated suffix, case-insensitively. Verify the exact names your
# gateway sends with the dump-input debug technique.
tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))
tool_matches(suffix) if {
tool_name == suffix
}
tool_matches(suffix) if {
endswith(tool_name, sprintf("-%s", [suffix]))
}
tool_matches(suffix) if {
endswith(tool_name, sprintf("_%s", [suffix]))
}
# File-targeting tools (official server) — content reads, previews, AI Q&A,
# and moves of a specific file ID.
is_file_tool if tool_matches("get_file_content")
is_file_tool if tool_matches("get_download_url")
is_file_tool if tool_matches("get_file_preview")
is_file_tool if tool_matches("ai_qa_single_file")
is_file_tool if tool_matches("ai_qa_multi_file")
is_file_tool if tool_matches("move_file")
# Copying a fenced file duplicates its content into a caller-chosen (unfenced)
# location, from which it can be read freely — so fence copy like move/read.
is_file_tool if tool_matches("copy_file")
# All six official ai_extract_* variants send file content through Box AI.
is_file_tool if contains(tool_name, "ai_extract_")
# File-targeting tools (community box-community/mcp-server-box).
is_file_tool if tool_matches("box_file_download_tool")
is_file_tool if tool_matches("box_file_text_extract_tool")
is_file_tool if tool_matches("box_file_copy_tool")
# Folder-targeting tools — listings, details, and moves/copies of a specific
# folder ID. Relocating or duplicating a fenced folder by its literal ID is
# fenced too (symmetric with move_file); reaching a fenced tree through an
# UNFENCED ancestor ID cannot be caught statelessly — see Known limitations.
is_folder_tool if tool_matches("list_folder_content_by_folder_id")
is_folder_tool if tool_matches("get_folder_details")
is_folder_tool if tool_matches("move_folder")
is_folder_tool if tool_matches("copy_folder")
is_folder_tool if tool_matches("box_folder_items_list_tool")
is_folder_tool if tool_matches("box_folder_move_tool")
# Search tools.
is_community_search_tool if tool_matches("box_search_tool")
is_official_search_tool if tool_matches("search_files_keyword")
is_official_search_tool if tool_matches("search_files_metadata")
is_search_tool if is_community_search_tool
is_search_tool if is_official_search_tool
# Any tool this policy inspects.
is_fenced_scope_tool if is_file_tool
is_fenced_scope_tool if is_folder_tool
is_fenced_scope_tool if is_search_tool
# ---------------------------------------------------------------------------
# Argument extraction — object.get everywhere; Box IDs may arrive as strings
# or numbers, so normalize both to a trimmed string.
args := object.get(object.get(input, "payload", {}), "args", {})
to_id(x) := trim_space(x) if is_string(x)
to_id(x) := sprintf("%v", [x]) if is_number(x)
# Target file IDs: single `file_id`, multi-file `items[].id`, and a flat
# `file_ids[]` array (defensive — multi-file AI schemas are partially
# unverified).
requested_file_ids contains id if {
id := to_id(object.get(args, "file_id", ""))
id != ""
}
requested_file_ids contains id if {
some item in object.get(args, "items", [])
id := to_id(object.get(item, "id", ""))
id != ""
}
requested_file_ids contains id if {
some raw in object.get(args, "file_ids", [])
id := to_id(raw)
id != ""
}
requested_folder_id := to_id(object.get(args, "folder_id", ""))
# Search folder scoping: `ancestor_folder_ids` as an array of IDs or a
# comma-separated string (the official server's exact field name is partially
# unverified — see Known limitations).
ancestor_ids contains id if {
raw := object.get(args, "ancestor_folder_ids", [])
is_array(raw)
some x in raw
id := to_id(x)
id != ""
}
ancestor_ids contains id if {
raw := object.get(args, "ancestor_folder_ids", "")
is_string(raw)
some part in split(raw, ",")
id := trim_space(part)
id != ""
}
# ---------------------------------------------------------------------------
# Fence checks.
blocked_file_access if {
some id in requested_file_ids
group := object.get(fenced_files, id, "")
group != ""
not caller_has_group(group)
}
blocked_folder_access if {
group := object.get(fenced_folders, requested_folder_id, "")
group != ""
not caller_has_group(group)
}
blocked_ancestor if {
some id in ancestor_ids
group := object.get(fenced_folders, id, "")
group != ""
not caller_has_group(group)
}
# ---------------------------------------------------------------------------
# Allow rules.
# Any tool this policy does not inspect passes through untouched.
allow if {
not is_fenced_scope_tool
}
allow if {
is_file_tool
not blocked_file_access
}
allow if {
is_folder_tool
not blocked_folder_access
}
# Scoped community search: allowed when every fenced ancestor ID maps to a
# group the caller holds (or no fenced ID is present).
allow if {
is_community_search_tool
count(ancestor_ids) > 0
not blocked_ancestor
}
# Unscoped community search can surface content from any fenced tree, so it
# is reserved for callers holding every fenced group.
allow if {
is_community_search_tool
count(ancestor_ids) == 0
privileged_search_caller
}
# Official search: fence check applies only when the scoping argument is
# present (unscoped official search passes — documented residual).
allow if {
is_official_search_tool
not blocked_ancestor
}
# ---------------------------------------------------------------------------
# Deny reasons.
reasons contains msg if {
is_file_tool
some id in requested_file_ids
group := object.get(fenced_files, id, "")
group != ""
not caller_has_group(group)
msg := sprintf("Box file %s is fenced as sensitive and requires the '%s' IdP group. Ask your Box administrator for access, or contact InfoSec if this fence looks wrong.", [id, group])
}
reasons contains msg if {
is_folder_tool
group := object.get(fenced_folders, requested_folder_id, "")
group != ""
not caller_has_group(group)
msg := sprintf("Box folder %s is a fenced sensitive tree and requires the '%s' IdP group. Ask your Box administrator for access, or contact InfoSec if this fence looks wrong.", [requested_folder_id, group])
}
reasons contains msg if {
is_search_tool
some id in ancestor_ids
group := object.get(fenced_folders, id, "")
group != ""
not caller_has_group(group)
msg := sprintf("Searching inside Box folder %s requires the '%s' IdP group. Remove it from ancestor_folder_ids, or ask your Box administrator for access.", [id, group])
}
reasons contains "Unscoped Box search is restricted while sensitive folders are fenced. Re-run the search with ancestor_folder_ids scoped to folders you may access, or ask your Box administrator for the fenced groups." if {
is_community_search_tool
count(ancestor_ids) == 0
not privileged_search_caller
}
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