Skip to main content

Browser Security API

The Browser Security API provides a normalized event feed for browser-native activity. It is the shared layer that combines direct browser events with mirrored browser-beacon and SWG decisions.

It also includes the first extension-governance surfaces for observed third-party extension inventory and workspace policy state.

The managed browser extension can also sync SWG navigation warn/block rules from the compiled browser navigation-rule feed. Extension version 1.0.13 translates that feed into local Chrome/Edge declarativeNetRequest rules and an optional Firefox webRequest fallback path. Workspace block rules now enforce by default, while workspace monitor rules compile to warn. The rule feed includes enforcement_enabled, which is true unless workspace browser policy or managed/local extension config explicitly sets swg_navigation_enforcement=false / swgNavigationEnforcement=false to keep a rollout in dry-run.

When enforcement is enabled, block navigations redirect to #/blocked and warn navigations redirect to #/warn. The extension then posts the navigation event back to Dralvia, which resolves the rule again server-side and writes a normalized Browser Security event with source="swg_navigation", action_type="navigation", event_status="warn" or event_status="block", plus the server-side decision_coaching block. The authenticated rule-coaching lookup exposes the same resolution flow for diagnostics. The extension deduplicates that interstitial event POST once per (rule_id, host, policy_version, action) in the current browser session.

Firefox rollouts can enable the fallback with managed config swgNavigationFirefoxFallback=true or swg_navigation_firefox_fallback=true.

Browser beacons can include metadata.swg_nav_state; the normalized Browser Security event summary exposes this as summary.swg_navigation with dry-run/degraded/truncated state, rule counts, policy version, top blocked hosts, and top rule ids for operator dashboards.

Browser extension release artifacts

These endpoints back #/browser-extension-downloads for rollout operations:

  • GET /agents/browser/extension/latest
  • GET /agents/browser/extension/manifest
  • GET /agents/browser/extension/download
  • GET /agents/browser/extension/sha256
  • GET /agents/browser/extension/signature

latest returns the current bundle metadata, including:

  • version
  • filename
  • size_bytes
  • sha256
  • download_url
  • sha256_url
  • manifest_url
  • signature_url when a detached signature exists

The manifest endpoint returns the published release JSON written during packaging. The signature endpoint is only present when the current ZIP was signed during release packaging.

When force-install metadata is configured, both latest and manifest also include:

  • force_install_ready
  • distribution_mode
  • rollout.extension_id
  • rollout.update_url
  • rollout.chrome.value
  • rollout.edge.value

Those rollout values are only actionable after the extension is actually published with that stable ID/update URL.

Authentication

  • Workspace persona: use https://dralvia.tech/api/tenant
  • Internal persona: use https://dralvia.tech/api/internal
  • Prefer signed-in browser sessions for interactive portal use
  • API key auth is supported for programmatic workspace use

Event model

Current normalized fields:

  • tenant_id
  • source
  • action_type
  • event_status
  • url
  • domain
  • app_id
  • workspace_id
  • workspace_label
  • risk_level
  • policy_action
  • account_posture
  • actor_email
  • device_label
  • extension_version
  • summary
  • metadata
  • created_at

Workspace identity is inferred from the event URL plus app_id for supported multi-workspace SaaS patterns, including classic Slack workspace subdomains, modern app.slack.com/client/<team-id> routes, Zoom workspace subdomains, and Google Workspace account-index paths across supported *.google.com app hosts, Google Admin customer-domain paths from admin.google.com/<customer_domain>/..., Atlassian Administration org ids from admin.atlassian.com/o/<org-id>/..., plus GitHub organization paths from github.com/orgs/<org>/..., github.com/organizations/<org>/..., and repository-owner paths such as github.com/<owner>/<repo>/....

For browser DLP runtime events (metadata.trigger = "browser_dlp_runtime"), the metadata object can also include:

  • destination_scope
  • scope_reason
  • scope_limitation
  • sanctioned_status
  • control_action
  • decision_source
  • decision_reason
  • scope_apply
  • scan_apply
  • adaptive_apply
  • adaptive_reason
  • destination_risk_apply
  • destination_risk_reason
  • governance_apply
  • governance_reason
  • governance_account_domain_match
  • governance_workspace_match
  • governance_workspace_id
  • governance_workspace_label
  • governance_workspace_source
  • governance_sso_required
  • governance_tenant_bound
  • governance_rollback_note
  • governance_approved_account_domains
  • governance_approved_workspace_ids
  • runtime_account_posture
  • runtime_posture_confidence
  • runtime_posture_source
  • runtime_posture_domains
  • runtime_posture_signals
  • device_trust
  • device_ownership
  • workforce_type
  • device_posture_source
  • device_posture_signals
  • target_label
  • file_names
  • download_inspection_status
  • download_inspection_bytes
  • download_inspection_truncated
  • download_extracted_file_names
  • field_count
  • scan_risk_level
  • scan_risk_score
  • scan_impersonation_detected
  • scan_impersonated_brand
  • scan_pressure
  • scan_top_reasons
  • scan_flags
  • decision_coaching

For mirrored Web Access Protection decisions (source = swg, action_type = policy_decision), the metadata object can also include:

  • swg_event_id
  • policy
  • reason
  • shadow_ai
  • sanctioned_status
  • control_applied
  • exception_applied
  • decision_coaching

Allowed action_type values:

  • navigation
  • redirect
  • download
  • upload
  • clipboard
  • print
  • text_submission
  • policy_decision
  • extension_state

Read recent browser-security events (GET /agents/browser/events/recent)

Use this endpoint to power the Browser Protection activity feed or to drill into one app's posture history without querying the raw beacon or SWG tables directly.

Query params:

  • limit default 100, max 200
  • source optional
  • action_type optional
  • app_id optional; matches app_id and fallback domain
  • account_posture optional; one of unknown, corporate, personal, mixed
  • tenant_id optional for internal callers

Example response:

{
"items": [
{
"id": 14,
"source": "swg",
"action_type": "policy_decision",
"event_status": "block",
"app_id": "drive.google.com",
"account_posture": "personal",
"summary": "Blocked personal Google Drive upload",
"metadata": {
"swg_event_id": 44,
"policy": "saas_control",
"decision_coaching": {
"trigger": "SaaS control: block on Google Drive",
"data_risk": "storage web access involving Google Drive",
"safer_path": "Use a sanctioned alternative or request a SaaS control exception before continuing.",
"operator_action": "Open SaaS controls"
}
},
"created_at": "2026-04-10T08:20:00Z"
}
],
"limit": 25,
"summary": {
"total": 1,
"by_status": {
"block": 1
},
"by_source": {
"swg": 1
},
"filters": {
"app_id": "drive.google.com",
"account_posture": "personal"
},
"account_posture": {
"by_posture": {
"personal": 1
},
"personal_or_mixed": 1,
"top_personal_apps": [
{
"app_id": "drive.google.com",
"owner": "Security",
"review_status": "approved",
"recommended_action": "monitor",
"decision_coaching": {
"trigger": "Governance: personal posture on tenant-bound app",
"data_risk": "personal account activity involving Google Drive",
"safer_path": "Sign in with an approved corporate account before allowing continued use.",
"operator_action": "Review or tighten monitor control"
}
}
]
}
}
}

When SaaS governance metadata exists for an app, summary.account_posture.top_personal_apps can also include:

  • owner
  • review_status
  • review_note
  • review_updated_at
  • decision_coaching

For metadata.trigger = browser_dlp_runtime, the server now also persists metadata.decision_coaching using the workspace DLP intervention CTA plus the runtime governance, destination-risk, adaptive, scan, scope, and classifier signals already attached to the event.

Download a browser-event EvidencePack (GET /agents/browser/events/<event_id>/evidencepack)

Use this endpoint to export a signed browser-native incident pack from the normalized browser event plane.

Query params:

  • tenant_id optional for internal callers
  • pretty optional boolean; when truthy, returns indented JSON

Behavior:

  • workspace callers are restricted to their own BrowserSecurityEvent rows
  • internal callers may scope with tenant_id
  • the response is an attachment with EvidencePack (Dralvia's exportable evidence report) headers plus X-EvidencePack-Hash and X-Transparency-Index
  • the signed artifact preserves the stored incident payload and metadata even when the routine recent feed is redacted for operator views

Example response shape:

{
"surface": "browser_security",
"inputs": {
"event_id": 51,
"action_type": "upload",
"event_status": "block",
"app_id": "wetransfer.com"
},
"metadata": {
"event_id": 51,
"risk_level": "high",
"tags": ["browser_security", "ticketbridge"]
},
"explanation": {
"summary": {
"recommended_action": "block",
"recommended_action_reason": "tenant_policy_enforced"
}
}
}

Download a browser-event decision trail (GET /agents/browser/events/<event_id>/decision-trail)

Returns one canonical record per browser event, built from the existing event metadata, so SIEM / ticketing pipelines can ingest the full chain without re-deriving stages per workspace.

Query params:

  • tenant_id optional for internal callers

Example response (abridged):

{
"event_id": 4017,
"tenant_id": "tenant-live",
"decision": {
"outcome": "block",
"policy_action": "block",
"risk_level": "high",
"action_type": "upload",
"summary": "Blocked personal Google Drive upload",
"occurred_at": "2026-05-16T13:55:18Z"
},
"destination": {
"url": "https://drive.google.com/personal/upload",
"domain": "drive.google.com",
"app_id": "drive.google.com",
"workspace_id": "tenant-live",
"workspace_label": "tenant-live",
"sanctioned_status": "sanctioned",
"control_action": "block",
"tenant_bound": true,
"sso_required": true
},
"policy_chain": [
{"stage": "destination_scope", "matched": true, "scope": "personal_or_unsanctioned", "reason": "personal_or_unsanctioned"},
{"stage": "destination_risk", "matched": true, "score": 92, "rule": "high_risk_personal_saas"},
{"stage": "adaptive", "matched": true, "reason": "byod_device"},
{"stage": "scan", "matched": true, "risk_level": "avoid", "impersonation_detected": true, "impersonated_brand": "Microsoft"},
{"stage": "classifier", "matched": true, "categories": ["secrets"], "hits": [{"pattern_id": "aws_access_key", "redacted_sample": "AKIA... (20 chars)"}]},
{"stage": "governance", "matched": true, "reason": "approved_workspace_mismatch_runtime", "workspace_state": "mismatch"}
],
"runtime_posture": {
"posture": "personal",
"confidence": "high",
"source": "workspace_binding"
},
"decision_coaching": {
"trigger": "Backend trigger",
"data_risk": "Backend risk",
"safer_path": "Backend safer path",
"operator_action": "Backend action"
},
"audit": {
"actor": "[email protected]",
"device_label": "managed-macbook",
"source": "extension",
"extension_version": "1.0.20",
"enforcement_outcome": ""
},
"trail_schema_version": "1"
}

Notes:

  • the policy_chain only contains stages that actually contributed to the decision; an event blocked purely by destination scope will not include a governance entry
  • trail_schema_version lets downstream consumers pin to a stable shape; bumping the schema is reserved for breaking changes only
  • the route is workspace-scoped via the same helper as /evidencepack and /ticketbridge, so cross-workspace calls return 404

Escalate a browser event to TicketBridge (POST /agents/browser/events/<event_id>/ticketbridge)

Use this endpoint to create a workspace-bound TicketBridge case from a normalized browser event. The backend stores the matching EvidencePack and attaches its hash/log index to the escalation reasons.

Query params:

  • tenant_id optional for internal callers

Request body:

{}

The body can stay empty for workspace callers. Internal callers may also send tenant_id in the JSON body if they prefer body-scoped routing.

Example response:

{
"status": "queued",
"ticket_id": "tb-51",
"evidencepack_hash": "d4b41f...",
"evidencepack_log_index": 128
}

Notes:

  • artifact type is derived from the event subject in priority order: url, then domain, then app_id
  • the escalation records a PlatformResponseAction with surface=browser_security

Read browser DLP policy (GET /agents/browser/dlp/policy)

Use this endpoint to load the workspace-scoped browser data-movement policy rendered in the Browser Protection Data Movement section.

Query params:

  • tenant_id optional for internal callers

Example response:

{
"tenant_id": "tenant-alpha",
"scope": "tenant",
"item": {
"rollout_mode": "warn",
"destination_scope": "personal_or_unsanctioned",
"classifiers": {
"secrets": true,
"source_code": true,
"credentials": true,
"regulated_data": false
},
"actions": {
"upload": {
"enabled": true,
"mode": "block"
},
"clipboard": {
"enabled": true,
"mode": "warn"
},
"print": {
"enabled": true,
"mode": "warn"
}
},
"evidence_capture": {
"include_page_context": true,
"include_actor_identity": true,
"retain_warn_and_block": true
},
"adaptive_access": {
"unmanaged_device": {
"enabled": true,
"mode": "warn"
},
"byod_device": {
"enabled": true,
"mode": "block"
},
"contractor_session": {
"enabled": true,
"mode": "warn"
}
},
"destination_risk": {
"enabled": true,
"warn_threshold": 50,
"block_threshold": 90,
"rules": {
"tenant_control_block": {
"enabled": true,
"score": 100,
"mode": "block",
"require_risk_note": false
},
"tenant_control_monitor": {
"enabled": true,
"score": 80,
"mode": "warn",
"require_risk_note": false
},
"unsanctioned_app": {
"enabled": true,
"score": 70,
"mode": "warn",
"require_risk_note": true
},
"unknown_app": {
"enabled": true,
"score": 55,
"mode": "warn",
"require_risk_note": true
}
}
},
"swg_navigation_enforcement": true,
"intervention_copy": {
"warn_title": "Review risky browser data movement",
"warn_body": "Check the destination before continuing.",
"block_title": "Data movement blocked",
"block_body": "Move this workflow into a sanctioned tenant-bound app.",
"help_cta": "Request exception"
},
"analyst_note": "Browser data movement defaults",
"updated_by": "[email protected]",
"updated_at": "2026-04-10T10:00:00Z"
}
}

Notes:

  • This endpoint returns defaults even before the workspace saves a custom policy.
  • Browser-side last-mile enforcement now exists for high-signal browser actions, including explicit download clicks, and the runtime can blend destination scope with the current page scan verdict.
  • adaptive_access lets workspaces elevate enforcement for unmanaged devices, BYOD ownership, and contractor sessions before content classification or account-posture certainty is perfect.
  • destination_risk lets workspaces tune SaaS governance destination scoring. The default preserves the built-in behavior: workspace block controls block, workspace monitor controls warn, and risky unsanctioned or unknown destinations warn when a risk note exists.
  • swg_navigation_enforcement also controls whether workspace SWG block rules stay live in the browser. It now defaults to true; set it to false only when you want dry-run validation without page-load redirects.

Save browser DLP policy (POST /agents/browser/dlp/policy)

Use this endpoint to update the workspace-scoped browser data-movement policy.

Allowed top-level values:

  • rollout_mode: observe, warn, block
  • destination_scope: all_activity, unsanctioned_only, personal_or_unsanctioned
  • swg_navigation_enforcement: true or false

Allowed per-action values:

  • actions: upload, download, clipboard, print, text_submission
  • action mode: inherit, observe, warn, block

Allowed adaptive access values:

  • adaptive_access.unmanaged_device.mode: observe, warn, block
  • adaptive_access.byod_device.mode: observe, warn, block
  • adaptive_access.contractor_session.mode: observe, warn, block

Allowed destination-risk values:

  • destination_risk.enabled: true or false
  • destination_risk.warn_threshold: integer from 0 to 100
  • destination_risk.block_threshold: integer from 0 to 100; must be greater than or equal to warn_threshold
  • destination-risk rules: tenant_control_block, tenant_control_monitor, unsanctioned_app, unknown_app
  • destination-risk rule mode: observe, warn, block
  • destination-risk rule score: integer from 0 to 100
  • destination-risk rule require_risk_note: true or false

Example request:

{
"rollout_mode": "warn",
"destination_scope": "unsanctioned_only",
"classifiers": {
"secrets": true,
"source_code": true,
"credentials": true,
"regulated_data": true
},
"actions": {
"upload": {
"enabled": true,
"mode": "block"
},
"clipboard": {
"enabled": true,
"mode": "warn"
},
"print": {
"enabled": false,
"mode": "observe"
}
},
"evidence_capture": {
"include_page_context": true,
"include_actor_identity": true,
"retain_warn_and_block": false
},
"adaptive_access": {
"unmanaged_device": {
"enabled": true,
"mode": "block"
},
"byod_device": {
"enabled": true,
"mode": "block"
},
"contractor_session": {
"enabled": false,
"mode": "warn"
}
},
"destination_risk": {
"enabled": true,
"warn_threshold": 45,
"block_threshold": 85,
"rules": {
"unsanctioned_app": {
"enabled": true,
"score": 90,
"mode": "block",
"require_risk_note": false
},
"unknown_app": {
"enabled": true,
"score": 55,
"mode": "warn",
"require_risk_note": true
}
}
},
"swg_navigation_enforcement": false,
"intervention_copy": {
"warn_title": "Confirm external upload",
"warn_body": "Uploads to unsanctioned destinations need validation.",
"block_title": "Action blocked",
"block_body": "Move this workflow into a tenant-bound app.",
"help_cta": "Request exception"
},
"analyst_note": "Browser data movement controls."
}

Validation:

  • invalid rollout_mode, destination_scope, or per-action mode values return 400
  • invalid adaptive access mode values return 400
  • invalid destination-risk modes or thresholds return 400
  • internal callers can set tenant_id in the request body to write another workspace's policy

Resolve browser DLP destination context (GET /agents/browser/dlp/context)

Use this endpoint from the managed browser runtime when you need the workspace's current browser DLP policy plus destination-scope evaluation for one URL.

Query params:

  • url required
  • tenant_id optional for internal callers
  • device_label optional
  • device_trust optional; managed, unmanaged
  • device_ownership optional; corporate, byod
  • workforce_type optional; employee, contractor

Example response:

{
"tenant_id": "tenant-alpha",
"scope": "tenant",
"policy": {
"rollout_mode": "warn",
"destination_scope": "unsanctioned_only"
},
"destination": {
"url": "https://wetransfer.com/upload",
"domain": "wetransfer.com",
"app_visibility": {
"app_id": "wetransfer.com",
"name": "WeTransfer",
"vendor": "WeTransfer",
"category": "file_transfer",
"sanctioned_status": "unsanctioned",
"sso_required": false,
"tenant_bound": false,
"rollback_note": "",
"approved_workspace_ids": [],
"governance_owner": "",
"governance_review_status": ""
},
"device_posture": {
"device_label": "managed-macbook",
"device_trust": "managed",
"device_ownership": "byod",
"workforce_type": "contractor",
"source": "device_beacon",
"signals": [
"device_agent_state:managed",
"device_beacon_ownership:byod",
"device_beacon_workforce:contractor"
]
},
"scope_match": true,
"scope_reason": "unsanctioned_destination",
"scope_limitation": null
}
}

Notes:

  • This endpoint does not inspect clipboard contents, file contents, or typed text.

  • It answers both the destination-scope decision and any currently-known device/session posture context available at request time.

  • Managed browser rollouts can provide deviceTrust, deviceOwnership, and workforceType through dralviaExtensionConfig; those hints are blended with any matching Device Agent / Device Beacon context before the response is returned.

  • When governance metadata exists for the matched SaaS app, destination.app_visibility also carries sso_required, tenant_bound, rollback_note, approved_account_domains, approved_workspace_ids, governance_owner, and governance_review_status so browser-time decisions can reuse the same binding expectations the operator already sees in the console.

  • Browser-time DLP also uses destination.app_visibility.control_action, sanctioned_status, and risk_note as a destination-risk overlay. The workspace policy can tune destination-risk rule scores, warn/block thresholds, and whether unknown or unsanctioned apps require a risk note before elevating. Matching events carry destination_risk_apply, destination_risk_reason, destination_risk_score, and destination_risk_rule.

  • For personal_or_unsanctioned, current runtime matching is still strongest for unsanctioned destinations. Personal-vs-corporate account posture remains more heuristic than the new unmanaged/BYOD/contractor adaptive controls, but approved sanctioned-account domains now reduce false positives for workspace-approved alias domains.

  • If approved_account_domains exist but the current browser page exposes neither a visible approved account-domain match nor any inferable workspace, browser-time governance now emits approved_account_domain_unresolved_runtime: warning by default, or block when the sanctioned app is workspace-bound and already configured with a blocking workspace control action.

  • For supported multi-workspace SaaS patterns, browser-time governance can now also compare the inferred workspace/account index against approved_workspace_ids so a sanctioned app can still warn or block the wrong workspace/workspace even when the visible account domain is corporate.

  • If approved_workspace_ids exist but no workspace can be inferred from the current SaaS URL shape, browser-time governance now emits approved_workspace_unresolved_runtime: warning by default, or block when the sanctioned app is workspace-bound and already configured with a blocking workspace control action.

  • Analyst evidence now keeps unresolved sanctioned-account binding distinct from mismatch: governance_account_domain_match / governance_workspace_match are only boolean when a match or mismatch was actually proven, and unresolved cases are represented by the governance reason instead of a synthetic false.

  • Runtime event metadata now also carries explicit sanctioned-binding state strings:

    • governance_account_domain_state
    • governance_workspace_state

    Values are matched, mismatch, or unresolved.

  • Browser event ingestion normalizes and backfills those fields from older governance metadata when possible. If an event only carries boolean match flags or an unresolved governance reason, the recent-events API still returns the explicit binding-state contract.

  • The Browser Protection surfaces the same workspace-binding evidence from recent events, including approved workspace list, inferred workspace id, inference source, and workspace match vs mismatch.

Report observed extension inventory (POST /agents/browser/extensions/report)

Use this endpoint from managed browsers or browser-side workers that can enumerate installed extension metadata.

Example:

{
"actor_email": "[email protected]",
"device_label": "security-laptop-01",
"extension_version": "1.0.7",
"items": [
{
"id": "bbb222",
"name": "Side Loader",
"version": "0.8.1",
"enabled": true,
"installType": "sideload",
"permissions": ["cookies", "webRequestBlocking", "tabs"],
"hostPermissions": ["<all_urls>"],
"homepageUrl": "https://extensions.example/risky",
"updateUrl": "https://clients2.google.com/service/update2/crx"
}
]
}

Additional reputation inputs accepted on each item:

  • homepageUrl
  • updateUrl

The API uses those fields to distinguish marketplace-backed extensions, vendor-matched custom update channels, and suspicious update-source mismatches.

Response:

{
"status": "ok",
"tenant_id": "tenant-alpha",
"created": 1,
"updated": 0,
"observed_extensions": 1
}

Read extension inventory (GET /agents/browser/extensions/inventory)

Query params:

  • limit default 100, max 250
  • risk_level optional
  • policy_action optional, use unset for rows without policy state
  • q optional text filter
  • tenant_id optional for internal callers

Example response:

{
"tenant_id": "tenant-alpha",
"scope": "tenant",
"limit": 100,
"summary": {
"total": 12,
"by_risk_level": {
"high": 2,
"medium": 4,
"low": 6
},
"by_policy_action": {
"warn": 1,
"unset": 11
},
"risky_enabled": 2
},
"items": [
{
"extension_id": "bbb222",
"name": "Side Loader",
"risk_level": "high",
"risk_score": 74,
"permission_scope": "critical",
"host_access_level": "broad",
"publisher_trust": "low",
"policy_action": "warn",
"risk_reasons": [
"Critical browser permissions: webRequestBlocking",
"Extension requests broad host access across most sites"
]
}
]
}

Set extension policy state (POST /agents/browser/extensions/policies)

Use this endpoint to attach workspace/operator policy state to an observed extension.

Allowed policy_action values:

  • allow
  • warn
  • monitor
  • block
  • prevent_install
  • unset

Example:

{
"extension_id": "bbb222",
"policy_action": "warn",
"policy_note": "Coach the pilot cohort before rollout."
}

Read extension policy assignments (GET /agents/browser/extensions/policies)

Returns policy entries for the workspace. By default only entries with a policy action are returned.

Query params:

  • tenant_id optional for internal callers
  • include_all default false (set true to include unset entries)
  • updated_after optional ISO timestamp to fetch deltas

Example response:

{
"tenant_id": "tenant-alpha",
"policy_version": "2026-04-09T15:20:08Z",
"items": [
{
"extension_id": "bbb222",
"name": "Side Loader",
"risk_level": "high",
"policy_action": "block",
"policy_note": "Block until vendor review completes.",
"policy_updated_at": "2026-04-09T15:19:52Z",
"updated_at": "2026-04-09T15:19:52Z",
"install_type": "sideload",
"enabled": true
}
]
}

Read managed browser policy bundle (GET /agents/browser/extensions/managed-policy-bundle)

Use this endpoint from the browser-extension rollout workspace when you want Chrome or Edge enterprise-policy payloads generated from Dralvia extension policy assignments.

Authentication:

  • Signed-in workspace session
  • Workspace context comes from the signed-in workspace
  • Dralvia support can assist with workspace-scoped troubleshooting when requested

Response shape:

{
"tenant_id": "tenant-alpha",
"generated_at": "2026-04-09T18:05:00Z",
"dralvia_extension_rollout": {
"ready": true,
"extension_id": "abcdefghijklmnopabcdefghijklmnop",
"update_url": "https://clients2.google.com/service/update2/crx",
"chrome": {
"policy_name": "ExtensionInstallForcelist",
"value": [
"abcdefghijklmnopabcdefghijklmnop;https://clients2.google.com/service/update2/crx"
]
}
},
"summary": {
"total_items": 3,
"by_policy_action": {
"block": 1,
"remove": 1,
"warn": 1
},
"by_installation_mode": {
"blocked": 1,
"removed": 1,
"allowed": 1
}
},
"items": [
{
"extension_id": "bbb222",
"policy_action": "block",
"managed_installation_mode": "blocked",
"rationale": "Managed browser policy disables use and blocks future installation on supported browsers."
}
],
"browsers": {
"chrome": {
"policy_name": "ExtensionSettings",
"value": {
"bbb222": {
"installation_mode": "blocked"
}
}
},
"edge": {
"policy_name": "ExtensionSettings",
"value": {
"bbb222": {
"installation_mode": "blocked"
}
}
}
}
}

Mapping rules:

  • block and prevent_install map to managed-browser installation_mode = blocked
  • remove maps to managed-browser installation_mode = removed
  • allow, warn, and monitor map to installation_mode = allowed

Caveats:

  • This endpoint generates governance policy for third-party extensions already observed by Dralvia.
  • The downloadable Dralvia ZIP is still a manual/unpacked artifact. Force-installing the Dralvia extension itself requires a signed CRX or store-hosted package with a stable extension ID and update URL.

Report extension enforcement outcomes (POST /agents/browser/extensions/enforcement/report)

Use this endpoint to log enforcement attempts from managed browsers. Each item records an attempted action and outcome.

Example:

{
"items": [
{
"extension_id": "bbb222",
"policy_action": "block",
"outcome": "disabled"
},
{
"extension_id": "ccc333",
"policy_action": "block",
"outcome": "permission_missing",
"detail": "management_permission_required"
}
]
}

Ingest direct browser events (POST /agents/browser/events)

Use this endpoint when the browser-side client already knows the event details and wants to write directly into the normalized stream.

Example:

{
"action_type": "upload",
"event_status": "block",
"url": "https://drive.google.com/file",
"app_id": "drive.google.com",
"policy_action": "block",
"risk_level": "high",
"account_posture": "personal",
"actor_email": "[email protected]",
"device_label": "analyst-macbook",
"extension_version": "2.0.0",
"summary": "Blocked personal Google Drive upload",
"metadata": {
"trigger": "dlp_upload_guard",
"file_name": "wire-transfer.xlsx"
}
}

Response:

{
"status": "ok",
"id": 42
}

Read recent browser-security activity (GET /agents/browser/events/recent)

Query params:

  • limit default 100, max 200
  • source optional
  • action_type optional
  • tenant_id optional for internal callers

Example response:

{
"items": [
{
"id": 43,
"tenant_id": "tenant-alpha",
"source": "extension",
"action_type": "extension_state",
"event_status": "error",
"app_id": "bbb222",
"extension_id": "bbb222",
"policy_action": "remove",
"summary": "Extension policy remove enforcement permission_missing",
"enforcement_outcome": "permission_missing",
"enforcement_detail": "management permission missing",
"created_at": "2026-04-09T08:50:00+00:00"
},
{
"id": 42,
"tenant_id": "tenant-alpha",
"source": "swg",
"action_type": "policy_decision",
"event_status": "block",
"domain": "drive.google.com",
"app_id": "drive.google.com",
"risk_level": "high",
"policy_action": "block",
"account_posture": "personal",
"summary": "Blocked personal Google Drive upload",
"metadata": {
"swg_event_id": 99,
"policy": "saas_control"
},
"created_at": "2026-04-09T09:10:00+00:00"
}
],
"limit": 100,
"summary": {
"total": 2,
"by_status": {
"block": 1,
"error": 1
},
"by_source": {
"swg": 1,
"extension": 1
},
"account_posture": {
"by_posture": {
"personal": 1,
"unknown": 1
},
"personal_or_mixed": 1,
"top_personal_apps": [
{
"app_id": "drive.google.com",
"name": "Google Drive",
"vendor": "Google",
"category": "storage",
"sanctioned_status": "controlled",
"control_action": "monitor",
"control_note": "Monitor personal Google Drive usage",
"recommended_action": "monitor",
"recommended_reason": "A monitor control is already active. Review repeated posture drift and escalate if needed.",
"pressure_level": "medium",
"count": 1,
"postures": ["personal"],
"last_seen_at": "2026-04-09T09:10:00+00:00"
}
]
},
"extension_enforcement": {
"total": 1,
"by_outcome": {
"permission_missing": 1
},
"failures": 1
},
"data_movement": {
"total": 1,
"by_action": {
"upload": 1
},
"by_status": {
"block": 1
},
"blocked": 1,
"warned_or_monitored": 0,
"dominant_trigger": {
"label": "Destination risk: tenant control block destination",
"count": 1
},
"top_triggers": [
{
"label": "Destination risk: tenant control block destination",
"count": 1
}
],
"dominant_data_risk": {
"label": "secrets and source code detected in the browser action",
"count": 1
},
"top_data_risks": [
{
"label": "secrets and source code detected in the browser action",
"count": 1
}
],
"runtime_posture": {
"corporate": 1,
"personal": 0,
"mixed": 0,
"unknown": 0
},
"scope": {
"matched": 0,
"out_of_scope": 0,
"coverage_gaps": 0,
"scan_linked": 0,
"impersonation_linked": 0,
"top_limitations": []
}
}
}
}

Notes:

  • extension_id, enforcement_outcome, and enforcement_detail are populated for action_type = extension_state events written by the extension enforcement loop.
  • summary.account_posture is a pre-aggregated block for Browser Protection posture widgets and top personal/mixed app pressure.
  • summary.account_posture.top_personal_apps now includes backend-resolved app identity, sanctioned state, active control metadata, pressure level, a recommended next step, and decision_coaching so the UI does not need to recompute SaaS governance context client-side.
  • summary.extension_enforcement is a pre-aggregated block for Browser Protection widgets and recent failure triage.
  • summary.data_movement is a pre-aggregated block for Browser Protection DLP widgets. It exposes canonical action/status counts plus dominant trigger, dominant data-risk text, runtime posture mix, and scope/coverage pressure so the UI can reuse the backend summary instead of rebuilding every dashboard card client-side.
  • summary.extension_risk is a pre-aggregated block for the Browser Protection extension dashboard. It exposes total, distinct_extensions, high_or_critical_total, by_risk_level, by_policy_action, an enforcement sub-block (total, by_outcome, failures), and a top_risky_extensions list (extension id, latest risk level, policy action, enforcement outcome, install type, permission scope, host access level, publisher trust, count, last seen, summary). Counts are derived from action_type = extension_state events only.
  • summary.account_posture_violations is a pre-aggregated block for the corporate-account violation dashboard. It exposes total, by_posture, by_account_domain_state, by_workspace_state, by_status, top_reasons (top governance reason codes), and top_violation_apps (app id, name, vendor, category, sanctioned status, workspace-bound and SSO-required flags, per-status counts, top reason, last seen). A violation is any event where account_posture is personal/mixed on a workspace-bound or SSO-required sanctioned app, where governance_account_domain_state or governance_workspace_state is mismatch/unresolved, or where governance_reason is a known violation code.

Mirrored sources

This feed is not only for direct client writes.

It also includes:

  • browser-beacon activity mirrored from POST /agents/browser/beacon
  • Web Access Protection decisions mirrored from POST /swg/evaluate

That is why the Browser Protection can act as a unified page instead of making operators pivot across three separate views immediately.

Browser security event privacy and retention

The browser security event feed is workspace-controlled. Each workspace can configure how long events are retained and how much PII is redacted in operator-visible views.

Read browser privacy policy (GET /agents/browser/events/privacy)

The Browser Protection uses this endpoint to populate the Retention And Redaction Controls panel.

Query params:

  • tenant_id optional for internal callers

Example response:

{
"tenant_id": "tenant-alpha",
"scope": "tenant",
"item": {
"retention_days": 90,
"redact_actor_email": false,
"redact_device_label": false,
"redact_url_path": false,
"redact_metadata_keys": [
"api_key",
"authorization",
"cookie",
"password",
"secret",
"token"
],
"summary_max_chars": 500,
"updated_by": null,
"updated_at": null
}
}

Save browser privacy policy (POST /agents/browser/events/privacy)

The Browser Protection saves operator edits through this endpoint. No separate browser-console policy store exists.

Allowed ranges:

  • retention_days: 1–730
  • summary_max_chars: 40–2000
  • redact_metadata_keys: list of lowercase strings. Matched case-insensitively, recursively through nested dict and list metadata.

Example request:

{
"retention_days": 30,
"redact_actor_email": true,
"redact_device_label": true,
"redact_url_path": true,
"redact_metadata_keys": ["cookie", "authorization", "trigger"],
"summary_max_chars": 240
}

Validation:

  • Out-of-range retention or summary limits return 400 invalid_retention_days or 400 invalid_summary_max_chars.
  • Non-list or non-string metadata keys return 400 invalid_redact_metadata_keys.
  • Internal callers can set tenant_id in the request body to write another workspace's policy.

Effect on the recent event feed

When a workspace has redact_actor_email or redact_device_label enabled, those fields are replaced in the recent feed with a deterministic workspace-scoped sha1:xxxxxxxx pseudonym. The same actor still groups together inside the feed, but the raw email or hostname is not surfaced to operators. When redact_url_path is enabled, URLs are truncated to scheme and host only. When redact_metadata_keys contains a key, any matching entry (case-insensitive, recursive) is removed from the serialized event metadata before it is shown. Long summaries are truncated to summary_max_chars with a trailing ellipsis.

The recent feed response now also includes a summary.privacy block so the UI can show which redactions are active:

{
"summary": {
"privacy": {
"retention_days": 30,
"redact_actor_email": true,
"redact_device_label": true,
"redact_url_path": true,
"redact_metadata_keys": ["authorization", "cookie", "trigger"],
"summary_max_chars": 240,
"applied": true
}
}
}

Internal callers investigating a specific workspace can pass ?raw=1 on the recent feed to bypass redaction. This is an internal forensics affordance and is not honored for workspace-scoped API keys.

Purge expired events (POST /agents/browser/events/purge)

Use this endpoint to delete events older than the workspace's configured retention_days. Workspace callers purge their own events; internal callers can specify another workspace with tenant_id in the request body. The caller can also pass retention_days in the body to override the stored policy for a single purge run (for example when running a one-off compliance cleanup with a shorter window than the default).

Example request:

{
"tenant_id": "tenant-alpha"
}

Example response:

{
"status": "ok",
"tenant_id": "tenant-alpha",
"scope": "tenant",
"deleted": 124,
"cutoff": "2026-01-21T00:00:00Z",
"retention_days": 90
}

Notes:

  • Purge operates on events whose created_at is strictly earlier than the cutoff derived from retention_days. The cutoff timestamp is returned in the response so auditors can verify the window used.
  • Dralvia also runs a scheduled sweeper that applies every workspace's configured retention window automatically and emits an audit-ready report. Workspaces that have not saved a policy fall back to the 90-day default.

SaaS lifecycle history CSV export (GET /swg/saas/governance/lifecycle/history.csv)

Evidence-grade CSV export of SaaS lifecycle transitions for the resolved workspace. Powers the "Export history CSV" button in the Browser Protection lifecycle KPI strip.

Query parameters:

  • tenant_id: omitted for workspace callers; Dralvia support may use it during assisted troubleshooting.
  • app_id: narrow the export to a single governance record.
  • limit: clamp row count (default 500, capped at 5000).

Response: text/csv with a stable header row that is always present (empty exports return only the header). The header row is:

tenant_id,app_id,from_state,to_state,change_type,actor,source,created_at

Each row is one entry from the SaaS history feed where entity_type = lifecycle. The response also sets:

  • Content-Disposition: attachment; filename="saas_lifecycle_history_<tenant>.csv"
  • X-Lifecycle-History-Rows: <integer>: exact row count for audit verification, matching the body length.

Read-only and observe-only (no lifecycle admin role gate).

SaaS lifecycle policy (GET|POST /swg/saas/governance/lifecycle/policy)

Per-workspace lifecycle SLA configuration that drives the stale endpoint. Each workspace can set distinct thresholds for the two monitored states. Defaults are 14 days for both, clamped to [1, 365].

GET returns the resolved policy for the resolved workspace (falling back to defaults when nothing has been saved):

{
"tenant_id": "acme",
"threshold_days_discovered": 14,
"threshold_days_reviewing": 14
}

POST body accepts the threshold fields plus an optional escalation_after_sweeps integer. Out-of-range values are clamped: threshold days to [1, 365] and escalation count to [0, 100]. Setting escalation_after_sweeps to 0 disables auto-escalation. The endpoint requires the lifecycle admin role gate (tenant_admin, tenant_owner, internal, or admin); other workspace roles receive 403 lifecycle_admin_required. Successful writes append a lifecycle_policy entry to the SaaS history feed for audit.

When escalation_after_sweeps > 0 and the same (tenant_id, app_id, lifecycle_state) is observed by the sweeper that many times, the next sweep auto-escalates by opening a TicketBridge ticket via _create_ticketbridge_entry with artifact.type = "domain", the lifecycle context in risk.reasons, and the SLA-breach summary in user_note. The alert-state row records escalated_at and escalated_ticket_id so subsequent sweeps will not open duplicate tickets.

SaaS lifecycle stuck-in-state sweeper (POST /swg/saas/governance/lifecycle/sweep-stuck)

Runs the SaaS lifecycle stuck-in-state sweeper for the resolved workspace and fires saas.lifecycle.stuck Pro webhooks for each app that newly crossed the configured SLA threshold. The endpoint is mutating and gated to the lifecycle admin role (tenant_admin, tenant_owner).

Workspace callers always sweep their own workspace. If your support plan includes assisted operations, Dralvia support can help run the same workspace-scoped check during troubleshooting.

Response (200):

{
"swept_at": "2026-05-02T12:00:00+00:00",
"tenants_scanned": 2,
"tenants_dispatched": 1,
"events_fired": 3,
"events_skipped_already_alerted": 0,
"events_cleared": 1,
"tenants": [
{
"tenant_id": "acme",
"thresholds": { "discovered": 14, "reviewing": 14 },
"fired": [
{ "app_id": "drive.google.com", "lifecycle_state": "reviewing", "stale_for_days": 30 }
],
"skipped": []
}
]
}

Sweeper semantics:

  • The sweeper consults the per-workspace policy stored at GET|POST /swg/saas/governance/lifecycle/policy for each workspace; no global override is applied.
  • Each (tenant_id, app_id, lifecycle_state) triple fires once. The alert state is dropped automatically when the app advances out of the monitored states (discovered / reviewing) so a future regression pages again.
  • Records that no longer exist in the governance store also have their alert state cleared.
  • Dispatch failures are logged on the webhook row (last_error in the Pro Tier Console) but do not fail the sweep response. Other apps still get fired during the same run.
  • When the per-workspace policy sets escalation_after_sweeps > 0, the sweeper increments a sweep_count on every observation of the same stuck triple. Once the count reaches the threshold and no prior escalation exists for that triple, the sweeper opens a TicketBridge ticket and stamps the alert row with escalated_at and escalated_ticket_id. Subsequent sweeps still bump sweep_count but never open a duplicate ticket. The per-workspace report exposes an escalated array and an events_escalated counter alongside events_fired.

The fired event payload matches the saas.lifecycle.stuck schema documented in Pro Tier Integrations.

Drive the sweeper from your own scheduler (cloud scheduler, Lambda timer, GitHub Actions cron, etc.). The endpoint is idempotent within a window, so calling it more often than the SLA window simply returns empty fired and escalated arrays until an app newly crosses the threshold.

Stuck-in-lifecycle SaaS apps (GET /swg/saas/governance/lifecycle/stale)

Surfaces SaaS governance records currently in discovered or reviewing whose last lifecycle change is older than the configured threshold. Powers the Stuck-in-lifecycle review callout in the Browser Protection.

Query parameters:

  • tenant_id: omitted for workspace callers; Dralvia support may use it during assisted troubleshooting.
  • threshold_days: staleness threshold in days (default 14, capped at 365).

Response (200):

{
"tenant_id": "acme",
"threshold_days": 14,
"monitored_states": ["discovered", "reviewing"],
"total": 2,
"counts": { "discovered": 1, "reviewing": 1 },
"items": [
{
"tenant_id": "acme",
"app_id": "notion.so",
"name": "notion.so",
"lifecycle_state": "discovered",
"lifecycle_state_changed_at": "2026-03-03T08:00:00+00:00",
"as_of": "2026-03-03T08:00:00+00:00",
"stale_for_days": 60,
"owner": "Security",
"review_status": "pending"
}
]
}

Items are sorted oldest-first so the worst offenders surface at the top. Records in terminal states (sanctioned, deprecated, retired) are excluded; once an app advances past the monitored states it disappears from the alert. The endpoint is observe-only.

The threshold each workspace uses comes from the lifecycle policy store above. The response includes:

  • thresholds: the resolved per-state thresholds (discovered, reviewing)
  • policy_source: policy when the saved policy was used, query when the request supplied an explicit override, or default when no policy was saved and the rollup endpoint fell back to the global default

Each item also carries the threshold_days that triggered its inclusion so the front-end can call out which SLA was breached.

SaaS lifecycle timeseries (GET /swg/saas/governance/lifecycle/timeseries)

Daily bucketed counts of SaaS lifecycle transitions for the resolved workspace. Drives the lifecycle sparkline in the Browser Protection KPI strip.

Query parameters:

  • tenant_id: omitted for workspace callers; Dralvia support may use it during assisted troubleshooting.
  • days: window length in days (default 30, capped at 180).

Response (200):

{
"tenant_id": "acme",
"days": 30,
"window_start": "2026-04-03",
"window_end": "2026-05-02",
"total": 7,
"series": [
{
"date": "2026-05-02",
"total": 2,
"by_state": {
"discovered": 0,
"reviewing": 1,
"sanctioned": 1,
"deprecated": 0,
"retired": 0
}
}
]
}

Every UTC day in the window has a bucket, including empty days, so front-ends can render a continuous timeline without gap stitching. Each bucket's by_state map is keyed by every value in LIFECYCLE_STATES (zero-filled). The endpoint is observe-only.

Browser operator trend (GET /agents/browser/events/operator-trend)

Daily extension-risk and account-posture-violation trend for the resolved workspace. Drives the sparklines that sit under the Extension Risk Telemetry and Account Posture Violations panels in the Browser Security Console.

Query parameters:

  • tenant_id: omitted for workspace callers; internal callers may pass a workspace id to look up a specific scope.
  • days: window length in days (default 14, capped at 90).

Response (200):

{
"tenant_id": "acme",
"days": 14,
"window_start": "2026-04-28",
"window_end": "2026-05-11",
"extension_risk": {
"total": 3,
"high_or_critical_total": 2,
"series": [
{
"date": "2026-05-11",
"new_events": 2,
"high_or_critical": 1,
"by_risk_level": {
"low": 0, "medium": 1, "high": 0, "critical": 1, "unknown": 0
}
}
]
},
"account_posture_violations": {
"total": 4,
"blocked": 1,
"warned": 2,
"series": [
{
"date": "2026-05-11",
"violations": 3,
"blocked": 1,
"warned": 1,
"monitored": 1
}
]
}
}

Both series have one bucket per UTC day in the window, zero-filled for empty days so the front-end can render a continuous sparkline without gap stitching. Extension-risk buckets are sourced from action_type = extension_state events grouped by risk_level. Account posture violation buckets count events where the governance binding state (governance_account_domain_state or governance_workspace_state) is mismatch/unresolved, where governance_reason matches a known violation code, or where personal/mixed posture was observed on a workspace-bound or SSO-required sanctioned app. Results are capped at 20 000 underlying events to bound memory usage.

Shadow SaaS adoption trend (GET /swg/saas/adoption/trend)

Daily bucketed count of net-new SaaS apps discovered for the resolved workspace. Drives the Shadow SaaS Adoption Trend panel in the Browser Security Console. Discovery is sourced from lifecycle_discovered SaaS history entries, which are written the first time a normalized app_id arrives in a browser security event for the workspace.

Query parameters:

  • tenant_id: omitted for workspace callers; internal callers may pass * to roll up across all workspaces.
  • days: window length in days (default 30, capped at 180).
  • top_limit: maximum entries in top_new_apps (default 5, capped at 50).

Response (200):

{
"tenant_id": "acme",
"days": 30,
"window_start": "2026-04-12",
"window_end": "2026-05-11",
"total_new_apps": 4,
"shadow_ai_new": 1,
"by_sanctioned": {
"sanctioned": 1,
"controlled": 1,
"unsanctioned": 1,
"unknown": 1
},
"series": [
{
"date": "2026-05-11",
"new_apps": 2,
"shadow_ai": 0,
"by_sanctioned": {
"sanctioned": 1,
"controlled": 1,
"unsanctioned": 0,
"unknown": 0
}
}
],
"top_new_apps": [
{
"app_id": "chatgpt.example",
"tenant_id": "acme",
"name": "ChatGPT",
"vendor": null,
"category": "ai",
"sanctioned_status": "unsanctioned",
"shadow_ai": true,
"control_action": null,
"first_seen_at": "2026-05-10T10:00:00Z",
"in_window": true
}
],
"lifecycle_transitions": {
"total": 3,
"by_state": {
"discovered": 0,
"reviewing": 2,
"sanctioned": 1,
"deprecated": 0,
"retired": 0
},
"recent": [
{
"tenant_id": "acme",
"app_id": "slack.com",
"from": "discovered",
"to": "reviewing",
"at": "2026-05-10T08:00:00Z",
"by": "[email protected]"
}
]
}
}

Every UTC day in the window has a bucket, including empty days, so the front-end can render a continuous sparkline without gap stitching. Each bucket's by_sanctioned map is keyed by every status in (sanctioned, controlled, unsanctioned, unknown) (zero-filled). Re- discovery of an already-known app_id is deduplicated to the earliest lifecycle_discovered entry per (tenant_id, app_id) so the trend counts true net-new apps. top_new_apps is ordered by most-recent first-seen and enriched via the SWG catalog plus workspace SaaS controls. The endpoint is observe-only.

The lifecycle_transitions block (added 2026-05-16) reports how discovered apps actually resolved within the same window. by_state is keyed by every value in LIFECYCLE_STATES; discovered is always zero in this block because discoveries are already counted as new_apps above. recent is capped at 25 entries and ordered by observation time. This block surfaces both the SaaS inflow (new_apps) and the resolution outflow (lifecycle_transitions) from one payload.

Run the content classifier on a sample (POST /agents/browser/dlp/classifier/test)

Operator-facing endpoint that runs the matcher described below against a supplied sample and returns redacted hits. Used by the Browser Protection "Classifier Test" affordance and by operator curl/CI smoke checks to verify pattern coverage before flipping a workspace's policy toggles. The request body is never persisted; only redacted hits are echoed back.

Body:

{
"sample": "deploy uses key AKIAIOSFODNN7EXAMPLE in the script",
"tenant_id": "acme",
"classifiers": {
"secrets": true,
"credentials": true,
"source_code": false,
"regulated_data": false
}
}
  • sample: required. The text to scan. Capped at 200 000 characters (413 sample_too_large when exceeded).
  • tenant_id: omitted for workspace callers. Workspace callers always run against their own resolved workspace.
  • classifiers: optional explicit override. When omitted the endpoint pulls your workspace's saved DLP policy classifiers toggles so the test matches what the live runtime would do.

Response:

{
"tenant_id": "acme",
"toggles": { "secrets": true, "credentials": true, "source_code": false, "regulated_data": false },
"sample_length": 51,
"result": {
"hits": [
{
"pattern_id": "aws_access_key",
"category": "secrets",
"label": "AWS access key id",
"redacted_sample": "AKIA... (20 chars)",
"start": 16,
"end": 36
}
],
"matched_categories": ["secrets"],
"total": 1
}
}

The redacted_sample always follows the <prefix>... (<n> chars) shape. The raw matched value is not returned, not logged, and not retained.

Content classifier hits on browser security events

The browser-side runtime now emits metadata.content_classifier_hits for supported browser DLP actions (clipboard paste plaintext, typed form submission values, print-page text, bounded upload extraction, and bounded download response samples) on normalized POST /agents/browser/events payloads. The server-side normalizer at _normalize_browser_security_event_content_classifier_hits sanitises the array before persistence:

  • Drops entries whose category is not one of secrets, credentials, source_code, or regulated_data.
  • Caps the array at 50 hits per event so a runaway runtime cannot push the metadata payload past its 24KB budget.
  • Wipes any redacted_sample that does not follow the matcher's <prefix>... (<n> chars) contract. The rest of the hit (pattern_id, category, label, start, end) still lands so analysts see what was caught, but a possibly-raw value is not persisted.
  • Derives a sorted metadata.content_classifier_categories list from the hit set so analyst evidence cards can render it without re-iterating the full hit array.
  • Browser Protection now consumes both fields directly in the Data Movement analyst-evidence card: operators see Detected data classes plus the redacted hit list (category, label, pattern_id, redacted_sample) without opening a separate artifact.

The browser DLP policy payload now also accepts optional custom_patterns, each shaped as:

  • id: stable workspace-local detector id
  • label: analyst-facing detector name
  • category: one of secrets, credentials, source_code, regulated_data
  • pattern: regex source string
  • flags: optional subset of i and m

The policy normalizer caps the list at 12 patterns per workspace and 240 characters per regex before the runtime sees it.

On the browser side, upload DLP now also attempts bounded text extraction from selected readable files before classification:

  • up to 3 files
  • up to 80 000 characters per file
  • up to 200 000 characters total
  • includes archive-based office XML uploads such as .docx, .docm, .xlsx, .xlsm, .pptx, .pptm, .odt, .ods, and .odp
  • includes bounded PDF literal/hex text recovery and printable-string recovery for deeper binary uploads such as .pdf, .doc, .xls, and .ppt

This applies to text-like files the browser can safely read from the selected File objects.

For explicit download actions, the managed browser runtime now also attempts bounded response-body sampling when the target is an http(s) URL and browser fetch/CORS allows the response to be read:

  • requests up to 512 KiB with Range
  • enforces the same byte cap if the remote server ignores Range
  • uses same-origin credentials only for same-origin downloads
  • omits credentials for cross-origin download sampling
  • reports download_inspection_status, download_inspection_bytes, download_inspection_truncated, and download_extracted_file_names

This is still a bounded browser-side sample. PDF handling is limited to literal-string, hex-string, and printable-string recovery; it does not imply guaranteed full-body download inspection for non-fetchable targets, OCR, font-aware PDF layout extraction, or deep parsing of every binary document format.

Content classifier categories

The browser DLP policy classifiers toggles (secrets, credentials, source_code, regulated_data) are backed by a content matcher. Each toggle controls one category:

CategoryWhat it matchesNotes
secretsLong-lived API tokens (AWS, Azure, GCP, GitHub PAT + fine-grained, GitLab, Slack, OpenAI, Anthropic), JWTs, PEM private keys, and high-entropy secret= / api_key= assignments. Added 2026-05-16: npm publish tokens (npm_token), PyPI tokens (pypi_token), crates.io tokens (cargo_token), Stripe live + restricted keys (stripe_live_secret, stripe_restricted), MongoDB connection strings (mongodb_connection_string), AWS STS session tokens (aws_session_token), Kubernetes service-account JWTs (kubernetes_sa_token).Each hit reports its pattern id (aws_access_key, github_pat, jwt, stripe_live_secret, mongodb_connection_string, etc.) so your analysts can identify what was caught. Browser-side runtime and server-side normalizer share the same pattern ids.
credentialsURLs with embedded basic-auth (https://user:pass@host), password=... / passwd: assignments with values ≥ 6 characters, username= / email= assignments containing an email value.Skips short heuristic matches (≤5 char passwords) so prose text doesn't false-positive.
source_codeHigh-density code-shaped content. Flags the payload only when ≥4 lines look like code (import, from … import, function, def, class, JSX/HTML tags, package, #include, return/throw/etc.) and those lines are at least 40% of the non-empty lines, so a paragraph of prose with one stray import line stays clean.Emits a single aggregate hit covering the matched range, never per-line noise.
regulated_dataLuhn-validated credit card numbers (13–19 digits with optional spaces/dashes), US SSN-shape (excluding reserved 000 / 666 / 0000 groups), IBAN-shape.Cards that don't pass Luhn are dropped before reaching the result.

Every hit returns a redacted_sample of the form <prefix>... (<n> chars) so the original sensitive value never leaves the matcher. That redacted sample is what flows into browser-event analyst evidence, EvidencePack metadata, and the browser-security activity feed; the raw value is not retained.

A disabled category short-circuits before any pattern matching, so the cost of turning a category off is zero.

Assisted SaaS lifecycle rollup

Both GET /swg/saas/governance/lifecycle/distribution and GET /swg/saas/governance/lifecycle/timeseries accept tenant_id=* when the caller has the internal or admin global role. In that mode the response aggregates lifecycle records across every workspace instead of scoping to one.

For distribution, the rollup payload also includes a by_tenant array sorted by descending app count:

{
"tenant_id": "*",
"total": 4,
"counts": { "discovered": 1, "reviewing": 2, "sanctioned": 1, "deprecated": 0, "retired": 0 },
"by_tenant": [
{
"tenant_id": "acme",
"total": 2,
"counts": { "discovered": 0, "reviewing": 1, "sanctioned": 1, "deprecated": 0, "retired": 0 }
},
{
"tenant_id": "globex",
"total": 2,
"counts": { "discovered": 1, "reviewing": 1, "sanctioned": 0, "deprecated": 0, "retired": 0 }
}
],
"recent_transitions": [ { "tenant_id": "acme", "...": "..." } ]
}

recent_transitions entries now also carry a tenant_id field so operators can attribute each transition without re-querying. Workspace callers cannot use tenant_id=*: passing it falls back to their resolved identity.

SaaS lifecycle distribution (GET /swg/saas/governance/lifecycle/distribution)

Per-state counts and recent transitions for SaaS governance lifecycle. Drives the Browser Protection lifecycle KPI strip.

Query parameters:

  • tenant_id: omitted for workspace callers; workspace callers always read their resolved identity.
  • recent_limit: clamp the recent_transitions list (default 5, capped at 50).

Response (200):

{
"tenant_id": "acme",
"total": 4,
"counts": {
"discovered": 1,
"reviewing": 2,
"sanctioned": 1,
"deprecated": 0,
"retired": 0
},
"last_changed_at": "2026-05-02T12:00:00Z",
"recent_transitions": [
{
"app_id": "slack.com",
"from": "reviewing",
"to": "sanctioned",
"at": "2026-05-02T12:00:00Z",
"by": "[email protected]",
"source": "swg_saas_governance_lifecycle"
}
]
}

counts always contains every lifecycle state, zero-filled when no record sits in that state. The endpoint is observe-only and does not enforce the lifecycle admin role gate that mutating endpoints use.

Bulk SaaS lifecycle transition (POST /swg/saas/governance/lifecycle/bulk)

Move multiple SaaS governance records to one lifecycle state in a single request. The endpoint backs the bulk action bar in the Browser Security Console Account Posture Pressure section.

Request body:

{
"lifecycle_state": "reviewing",
"app_ids": ["drive.google.com", "slack.com"],
"tenant_id": "<optional assisted-support scope>"
}

Validation:

  • lifecycle_state must be one of discovered, reviewing, sanctioned, deprecated, retired. Unknown values return 400 invalid_lifecycle_state with the allowed list.
  • app_ids must be a non-empty array of strings (after deduplication and whitespace trimming). An empty list returns 400 missing_app_ids.
  • A batch may contain at most 100 ids; larger requests are rejected with 400 too_many_app_ids.
  • Workspace callers fall back to their resolved workspace identity and are rejected with 403 if it is missing.
  • Lifecycle transitions are gated to elevated roles. Authenticated workspace callers must hold tenant_admin or tenant_owner. tenant_analyst, tenant_member, and tenant_billing are observe-only and receive 403 lifecycle_admin_required with the allowed-role list. The per-app POST /swg/saas/governance/<app_id>/lifecycle endpoint enforces the same rule.

Response (200):

{
"target_state": "reviewing",
"applied": [
{"app_id": "drive.google.com", "lifecycle_state": "reviewing", "lifecycle_state_changed_at": "..."},
{"app_id": "slack.com", "lifecycle_state": "reviewing", "lifecycle_state_changed_at": "..."}
],
"failed": [
{"app_id": "never-seen.example", "error": "governance_not_found"}
],
"applied_count": 2,
"failed_count": 1
}

Per-app behavior:

  • Each id is processed independently against the same allowed-transition map as POST /swg/saas/governance/<app_id>/lifecycle. Disallowed jumps (for example reviewing → retired) are reported with error: "transition_not_allowed", the previous from state, and the requested to state, instead of failing the whole batch.
  • Successful transitions append a lifecycle_<target> SaaS history entry tagged with source: "swg_saas_governance_lifecycle_bulk" and emit a governance_lifecycle_bulk response action.
  • The DB transaction commits once at the end so successful transitions are durable even when other ids in the batch failed.