Cross-organization threat sharing
An opt-in shared pool of indicators on Enterprise: the four actions, the three-reporter floor, what the platform does and does not do about hashing, and the dashboard tab.
Overview
Organizations on PasskeyBridge can pool indicators of compromise in one shared pool. An indicator surfaces in the feed only once several organizations have reported the same value independently, so reading the feed tells you that at least that many organizations saw it, and not which ones.
Who can use it. Enterprise only: the function calls the Enterprise plan gate and answers 403 with code: "plan_required" otherwise, and the dashboard tab shows an upgrade panel. contribute and subscribe additionally require the admin role on the organization; feed and status are open to any member.
Auth lane. Console-only, POST with a dashboard session. shield-threat-sharing is absent from the worker allowlist, so there is no public URL. The function resolves your organization from your own membership rows and ignores any tenant_id you put in the body; a user who belongs to several organizations cannot choose which one acts here.
Opt in first. Both contribute and feed require a subscription with sharing enabled, otherwise they answer 403 Enable threat sharing before contributing or 403 Enable threat sharing to access the feed. Turning sharing off stops both directions.
Four actions: contribute, feed, subscribe, status. Anything else answers 400 Unknown action, and a non-POST method answers 405 Method not allowed.
Actions and responses
contribute. Submit up to 100 indicators in one call. Admin only.
{
"action": "contribute",
"indicators": [
{ "indicator_hash": "9f2c...at least 16 characters", "indicator_type": "phone_hash", "severity": "high", "source_signal_type": "sim_swap" }
]
}Each entry needs indicator_hash (string, 16 characters or more) and indicator_type from the ten values below. severity is optional and defaults to medium; source_signal_type is optional free text. A new value inserts a row with a contributor count of 1; a value already in the pool increments the count atomically unless your organization already reported it, in which case it is skipped. The reply is {"contributed": 3, "skipped": 1}, and a threat_sharing.contribute audit entry records the totals.
indicator_type | Dashboard label |
|---|---|
ip_hash | IP address |
email_hash | |
device_hash | Device |
credential_hash | Credential |
phone_hash | Phone |
sim_hash | SIM signal |
agent_hash | Agent ID |
domain_hash | Other |
fingerprint_hash | Other |
session_hash | Other |
The last three are accepted by the API but have no label in the dashboard table, so they display as "Other".
feed. Returns indicators at or above your reporter threshold, newest activity first.
{ "action": "feed", "limit": 100, "severity": "critical", "indicator_type": "phone_hash" }limit defaults to 50 and is capped at 200. Both filters are optional and validated against the same value lists. There is no age filter: the feed is the whole qualifying pool, ordered by last_seen_at.
{
"indicators": [
{ "id": "<uuid>", "indicator_hash": "9f2c...", "indicator_type": "phone_hash", "severity": "high", "contributor_count": 5, "first_seen_at": "...", "last_seen_at": "...", "metadata": {} }
],
"own_contribution_ids": ["<uuid>"],
"threshold": 3
}subscribe. Admin only. {"action":"subscribe","sharing_enabled":true,"auto_contribute":false,"min_contributor_threshold":5} answers {"ok":true,"sharing_enabled":true} and writes a threat_sharing.subscribe or threat_sharing.unsubscribe audit entry. auto_contribute is stored, but no code reads it: nothing contributes on your behalf today, and every indicator in the pool got there through an explicit contribute call.
status. {"action":"status"} answers with your subscription, your own contribution count, the number of indicators currently above your threshold, and a severity breakdown of them.
| Status | Body | Cause |
|---|---|---|
| 400 | {"error":"indicators array required"} | empty or missing array |
| 400 | {"error":"Max 100 indicators per request"} | more than 100 entries |
| 400 | {"error":"Each indicator must have a valid indicator_hash (min 16 chars)"} | short or missing value |
| 400 | {"error":"Invalid indicator_type: ... Allowed: ..."} | type outside the ten |
| 400 | {"error":"Invalid severity: ... Allowed: critical, high, medium, low"} | unknown severity |
| 401 | {"error":"Missing authorization"} or {"error":"Invalid auth token"} | no session |
| 403 | {"error":"Admin role required"} | member without the admin role on contribute or subscribe |
| 403 | {"error":"No tenant membership"} | the caller belongs to no organization |
Reporter threshold
An indicator is withheld until enough separate organizations have reported it. The floor is 3 and it cannot be lowered: the feed filter uses the larger of your min_contributor_threshold and 3, so setting 1 or 2 has no effect while setting 5 makes your own feed stricter than the floor.
The reason for the floor is what a smaller number would leak. At 1, the feed would show that one organization saw a specific value. At 2, an organization that knows it contributed can deduce that exactly one other did. At 3, a reader learns only that at least three organizations saw it.
contributor_count on each feed row tells you how many organizations reported that indicator, and own_contribution_ids tells you which rows are yours, which is how the dashboard shows the "You" badge. Neither reveals who the others are; no identifier of a contributing organization is returned in any response.
Your own contributions are always visible to you in your contribution count, whether or not the indicator has reached the threshold.
Hashing and privacy
Hashing is the contributor's job, and the platform does not check it. The function validates that indicator_hash is a string of at least 16 characters and that the type is known. It does not hash anything, and it cannot tell a digest from a plaintext value. Anything you submit is stored and shown to other organizations exactly as you sent it.
That makes the digest you choose the whole of the privacy guarantee, and a plain SHA-256 is not enough for these fields. Phone numbers, email addresses and IP addresses are drawn from small, enumerable spaces: an attacker with the feed can hash every number in a country's range and recover the original in minutes. Submit a keyed digest instead, computed under a secret your organization holds, for example HMAC-SHA-256 of the normalized value under a shared pepper agreed with the organizations you exchange with. That is the same discipline PasskeyBridge applies to identifiers it stores itself, described in Zero-PII compliance.
Practical rules for a contributor:
- Normalize before you digest (E.164 for phones, lower-case for email) so two organizations reporting the same subject produce the same value.
- Never submit a raw identifier, a reversible encoding of one, or a plain digest of a low-entropy field.
- Keep
source_signal_typegeneric. It is free text, it is stored, and it is not shown in the dashboard feed today. - Remember that a contribution cannot be withdrawn through the API: there is no delete action, and the reporter count of an indicator you contributed to does not go down.
What is shared with other organizations: the value you submitted, its type, its severity, the number of reporting organizations, and the first and last time it was seen. What is not: your organization's identity, your event rows, or any field of the signal the indicator came from.
Threat sharing tab
Dashboard > Threat sharing, in the Security group, on Enterprise.
- Sharing switch in the tab header turns the subscription on and off. Everything else stays empty until it is on.
- Four tiles: Shared indicators (the pool above your threshold), Contributed (your own count), Critical (critical-severity indicators above the threshold), Minimum reporters (your threshold).
- Sharing settings, a collapsed section with an Automatic contribution switch and the Minimum reporters value. The switch stores the preference; no code acts on it today, so contribution stays manual.
- Shared feed, a collapsed section with severity filter buttons (All, Critical, High, Medium, Low), a refresh control, and a table of Type, Indicator hash, Severity, Reporters, First seen, Last seen and Source. Rows you contributed carry a "You" badge; the rest show "Network". The table has no free-text search and no pagination beyond the 100-row fetch.
There is no contribute form in the dashboard. Submitting indicators means calling the contribute action from your own code with an admin session. The tab is for reading the feed and managing the subscription.
Confirm it worked. After a contribute call, the Contributed tile increases by the number reported as contributed, and Dashboard > Audit log shows a threat_sharing.contribute entry with the contributed and skipped counts.