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/latestGET /agents/browser/extension/manifestGET /agents/browser/extension/downloadGET /agents/browser/extension/sha256GET /agents/browser/extension/signature
latest returns the current bundle metadata, including:
versionfilenamesize_bytessha256download_urlsha256_urlmanifest_urlsignature_urlwhen 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_readydistribution_moderollout.extension_idrollout.update_urlrollout.chrome.valuerollout.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_idsourceaction_typeevent_statusurldomainapp_idworkspace_idworkspace_labelrisk_levelpolicy_actionaccount_postureactor_emaildevice_labelextension_versionsummarymetadatacreated_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_scopescope_reasonscope_limitationsanctioned_statuscontrol_actiondecision_sourcedecision_reasonscope_applyscan_applyadaptive_applyadaptive_reasondestination_risk_applydestination_risk_reasongovernance_applygovernance_reasongovernance_account_domain_matchgovernance_workspace_matchgovernance_workspace_idgovernance_workspace_labelgovernance_workspace_sourcegovernance_sso_requiredgovernance_tenant_boundgovernance_rollback_notegovernance_approved_account_domainsgovernance_approved_workspace_idsruntime_account_postureruntime_posture_confidenceruntime_posture_sourceruntime_posture_domainsruntime_posture_signalsdevice_trustdevice_ownershipworkforce_typedevice_posture_sourcedevice_posture_signalstarget_labelfile_namesdownload_inspection_statusdownload_inspection_bytesdownload_inspection_truncateddownload_extracted_file_namesfield_countscan_risk_levelscan_risk_scorescan_impersonation_detectedscan_impersonated_brandscan_pressurescan_top_reasonsscan_flagsdecision_coaching
For mirrored Web Access Protection decisions (source = swg, action_type = policy_decision), the metadata object can also include:
swg_event_idpolicyreasonshadow_aisanctioned_statuscontrol_appliedexception_applieddecision_coaching
Allowed action_type values:
navigationredirectdownloaduploadclipboardprinttext_submissionpolicy_decisionextension_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:
limitdefault100, max200sourceoptionalaction_typeoptionalapp_idoptional; matchesapp_idand fallbackdomainaccount_postureoptional; one ofunknown,corporate,personal,mixedtenant_idoptional 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:
ownerreview_statusreview_notereview_updated_atdecision_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_idoptional for internal callersprettyoptional boolean; when truthy, returns indented JSON
Behavior:
- workspace callers are restricted to their own
BrowserSecurityEventrows - internal callers may scope with
tenant_id - the response is an attachment with EvidencePack (Dralvia's exportable evidence report) headers plus
X-EvidencePack-HashandX-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_idoptional 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_chainonly contains stages that actually contributed to the decision; an event blocked purely by destination scope will not include agovernanceentry trail_schema_versionlets 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
/evidencepackand/ticketbridge, so cross-workspace calls return404
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_idoptional 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, thendomain, thenapp_id - the escalation records a
PlatformResponseActionwithsurface=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_idoptional 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_accesslets workspaces elevate enforcement for unmanaged devices, BYOD ownership, and contractor sessions before content classification or account-posture certainty is perfect.destination_risklets 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_enforcementalso controls whether workspace SWGblockrules stay live in the browser. It now defaults totrue; set it tofalseonly 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,blockdestination_scope:all_activity,unsanctioned_only,personal_or_unsanctionedswg_navigation_enforcement:trueorfalse
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,blockadaptive_access.byod_device.mode:observe,warn,blockadaptive_access.contractor_session.mode:observe,warn,block
Allowed destination-risk values:
destination_risk.enabled:trueorfalsedestination_risk.warn_threshold: integer from0to100destination_risk.block_threshold: integer from0to100; must be greater than or equal towarn_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 from0to100 - destination-risk rule
require_risk_note:trueorfalse
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-actionmodevalues return400 - invalid adaptive access
modevalues return400 - invalid destination-risk modes or thresholds return
400 - internal callers can set
tenant_idin 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:
urlrequiredtenant_idoptional for internal callersdevice_labeloptionaldevice_trustoptional;managed,unmanageddevice_ownershipoptional;corporate,byodworkforce_typeoptional;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, andworkforceTypethroughdralviaExtensionConfig; 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_visibilityalso carriessso_required,tenant_bound,rollback_note,approved_account_domains,approved_workspace_ids,governance_owner, andgovernance_review_statusso 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, andrisk_noteas 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 carrydestination_risk_apply,destination_risk_reason,destination_risk_score, anddestination_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_domainsexist but the current browser page exposes neither a visible approved account-domain match nor any inferable workspace, browser-time governance now emitsapproved_account_domain_unresolved_runtime: warning by default, orblockwhen 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_idsso a sanctioned app can still warn or block the wrong workspace/workspace even when the visible account domain is corporate. -
If
approved_workspace_idsexist but no workspace can be inferred from the current SaaS URL shape, browser-time governance now emitsapproved_workspace_unresolved_runtime: warning by default, orblockwhen 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_matchare only boolean when a match or mismatch was actually proven, and unresolved cases are represented by the governance reason instead of a syntheticfalse. -
Runtime event metadata now also carries explicit sanctioned-binding state strings:
governance_account_domain_stategovernance_workspace_state
Values are
matched,mismatch, orunresolved. -
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:
homepageUrlupdateUrl
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:
limitdefault100, max250risk_leveloptionalpolicy_actionoptional, useunsetfor rows without policy stateqoptional text filtertenant_idoptional 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:
allowwarnmonitorblockprevent_installunset
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_idoptional for internal callersinclude_alldefaultfalse(settrueto include unset entries)updated_afteroptional 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:
blockandprevent_installmap to managed-browserinstallation_mode = blockedremovemaps to managed-browserinstallation_mode = removedallow,warn, andmonitormap toinstallation_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:
limitdefault100, max200sourceoptionalaction_typeoptionaltenant_idoptional 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, andenforcement_detailare populated foraction_type = extension_stateevents written by the extension enforcement loop.summary.account_postureis a pre-aggregated block for Browser Protection posture widgets and top personal/mixed app pressure.summary.account_posture.top_personal_appsnow includes backend-resolved app identity, sanctioned state, active control metadata, pressure level, a recommended next step, anddecision_coachingso the UI does not need to recompute SaaS governance context client-side.summary.extension_enforcementis a pre-aggregated block for Browser Protection widgets and recent failure triage.summary.data_movementis 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_riskis a pre-aggregated block for the Browser Protection extension dashboard. It exposestotal,distinct_extensions,high_or_critical_total,by_risk_level,by_policy_action, anenforcementsub-block (total,by_outcome,failures), and atop_risky_extensionslist (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 fromaction_type = extension_stateevents only.summary.account_posture_violationsis a pre-aggregated block for the corporate-account violation dashboard. It exposestotal,by_posture,by_account_domain_state,by_workspace_state,by_status,top_reasons(top governance reason codes), andtop_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 whereaccount_postureis personal/mixed on a workspace-bound or SSO-required sanctioned app, wheregovernance_account_domain_stateorgovernance_workspace_stateismismatch/unresolved, or wheregovernance_reasonis 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_idoptional 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–730summary_max_chars: 40–2000redact_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_daysor400 invalid_summary_max_chars. - Non-list or non-string metadata keys return
400 invalid_redact_metadata_keys. - Internal callers can set
tenant_idin 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_atis strictly earlier than the cutoff derived fromretention_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 (default500, capped at5000).
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/policyfor 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_errorin 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 asweep_counton 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 withescalated_atandescalated_ticket_id. Subsequent sweeps still bumpsweep_countbut never open a duplicate ticket. The per-workspace report exposes anescalatedarray and anevents_escalatedcounter alongsideevents_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 (default14, capped at365).
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:policywhen the saved policy was used,querywhen the request supplied an explicit override, ordefaultwhen 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 (default30, capped at180).
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 (default14, capped at90).
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 (default30, capped at180).top_limit: maximum entries intop_new_apps(default5, capped at50).
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_largewhen 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 policyclassifierstoggles 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
categoryis not one ofsecrets,credentials,source_code, orregulated_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_samplethat 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_categorieslist 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 classesplus 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 idlabel: analyst-facing detector namecategory: one ofsecrets,credentials,source_code,regulated_datapattern: regex source stringflags: optional subset ofiandm
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, anddownload_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:
| Category | What it matches | Notes |
|---|---|---|
secrets | Long-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. |
credentials | URLs 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_code | High-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_data | Luhn-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 therecent_transitionslist (default5, capped at50).
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_statemust be one ofdiscovered,reviewing,sanctioned,deprecated,retired. Unknown values return400 invalid_lifecycle_statewith the allowed list.app_idsmust be a non-empty array of strings (after deduplication and whitespace trimming). An empty list returns400 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
403if it is missing. - Lifecycle transitions are gated to elevated roles. Authenticated
workspace callers must hold
tenant_adminortenant_owner.tenant_analyst,tenant_member, andtenant_billingare observe-only and receive403 lifecycle_admin_requiredwith the allowed-role list. The per-appPOST /swg/saas/governance/<app_id>/lifecycleendpoint 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 examplereviewing → retired) are reported witherror: "transition_not_allowed", the previousfromstate, and the requestedtostate, instead of failing the whole batch. - Successful transitions append a
lifecycle_<target>SaaS history entry tagged withsource: "swg_saas_governance_lifecycle_bulk"and emit agovernance_lifecycle_bulkresponse action. - The DB transaction commits once at the end so successful transitions are durable even when other ids in the batch failed.