Platform Integrations

Reverse-ETL and Secure Data Share

Read curated analytics back out of your Snowflake warehouse into Versus — governed query pushdown for UI panels and allow-listed device enrichment write-back — and hand a consumer account a read-only, zero-copy Secure Data Share of your modeled data.

Versus can export your inventory, tickets, and vulnerability data to a Snowflake warehouse, model it downstream, and then bring the results back — either into Versus records as enrichment, or out to another account as a governed data share. This page covers the two "read" directions that sit on top of the export foundation: reverse-ETL (query pushdown and enrichment write-back) and Secure Data Share.

The whole warehouse integration is gated by the Data Warehouse (Snowflake) module entitlement and stays inert until you create a connection and point it at an account. Nothing runs until a connection is explicitly exercised.


Where this fits

The warehouse integration moves data in four independently-toggleable directions, configured per connection as flows:

FlowDirectionWhat it does
exportOutboundIncrementally ships canonical rows (devices, ITSM tickets, CVE catalog) to Snowflake.
queryInbound (read)Runs governed, named catalog queries against your modeled analytics views.
enrichInbound (write-back)Updates a strict allow-list of device fields from a Snowflake source.
inventory_importInboundReconciles managed devices from a Snowflake CMDB/asset table (dry-run gated).

Reverse-ETL is the query and enrich flows — reading data back out of the warehouse. Secure Data Share is a property of the connection's mode rather than a flow: it grants another Snowflake account read-only access to the data Versus has already modeled. Both build on the same export foundation, so it helps to understand that first.

The export foundation

Export runs are incremental and fail-closed. Each run reads rows changed since the last confirmed watermark, tenant-scoped by an explicit tenant_id filter (with row-level security as defense-in-depth), maps each row to a canonical shape, stamps an envelope, serializes to NDJSON in batches, and stages the files to object storage (S3/R2, with a local-disk fallback). The watermark advances only on a successful run, so a crashed run safely re-emits from the last commit — at-least-once delivery plus an idempotent downstream MERGE yields effectively-once semantics.

Every exported row carries four envelope columns that the reverse paths and downstream modeling rely on:

ColumnMeaning
tenant_idOwning tenant (null for global dimensions like the CVE catalog).
_versus_opupsert or delete — drives the downstream merge and delete tombstones.
_versus_tsSource change timestamp — the last-writer-wins key.
_ingested_atWhen the row landed.

Inside Snowflake, data is isolated schema-per-tenant: raw VARIANT landing tables in RAW_<tenant>, modeled tables in STAGING_<tenant>, and curated views in ANALYTICS_<tenant>. Native Streams and serverless Tasks move RAW → STAGING with a MERGE keyed on each domain's natural key, honoring _versus_op/_versus_ts.


Reverse-ETL

Reverse-ETL is reading data back from the warehouse. Both capabilities are governed and fail-closed — Versus never runs arbitrary client SQL against your warehouse, and write-back can only touch an explicit allow-list.

Query pushdown

Query pushdown lets a Versus UI panel run a named, parameterized, read-only query against the connection's ANALYTICS schema. There is no free-text SQL surface: the catalog is a fixed set of vetted queries, and every parameter is bound (never string-formatted into the SQL), so a device ID coming from the UI can't alter the query.

Two catalog queries ship today:

NameReturns
vuln_trend_12moMonthly vulnerability counts for a device over the last 12 months.
ticket_volume_90dTicket volume by type over the last 90 days.

Run one against a configured connection:

POST /api/warehouse/connections/{id}/named_query/
Authorization: Bearer …
Content-Type: application/json

{
  "name":   "vuln_trend_12mo",
  "params": { "device_id": "6f2c…" }
}
200 OK
{ "rows": [ { "MONTH": "2026-07-01", "N": 12 }, … ] }

An unknown query name is refused with 400 before anything reaches Snowflake — the response lists the names that are allowed. If the warehouse itself errors (auth, reachability), you get a 502 with the cause. The {view} placeholder in each catalog entry is resolved to the calling tenant's own analytics schema, so a connection can only ever read its own modeled data.

Enrichment write-back

Enrichment write-back reads columns you've modeled in Snowflake — owner, criticality, CMDB notes, location — and writes them back onto Versus Device records. It is deliberately narrow and safe:

  • Additive allow-list only. Only three device fields can be written: notes, environment, and location. A column_map entry that targets any other field is silently ignored (and reported back in ignored_fields). The allow-list expands by policy, never silently.
  • Never a control-plane action. Enrichment updates records through the ORM only. It never pushes config, upgrades, reboots, or discovers — it cannot change the state of a device.
  • Tenant-scoped and audited. Writes run under tenant_context, so row-level security and the auto-stamp trigger apply, and a row that can't be attributed to the connection's tenant is dropped rather than misapplied.
  • Match on a canonical key. Rows are matched to devices by device_id, mgmt_ip, serial_number, or hostname.
POST /api/warehouse/connections/{id}/enrich/
Authorization: Bearer …
Content-Type: application/json

{
  "source_object": "ANALYTICS_ab12.DEVICE_OWNERSHIP",
  "match_field":   "serial_number",
  "column_map":    { "OWNER_NOTE": "notes", "ENV": "environment" }
}
200 OK
{ "seen": 812, "updated": 47, "dropped": 3, "ignored_fields": [] }

The counts tell you exactly what happened: seen rows read from the source, updated devices changed, dropped rows skipped (no key, no matching device, or unattributable), and ignored_fields naming any mapped column that fell outside the allow-list. A value is only written when it actually differs from what's on the record, so re-running an unchanged source is a no-op.


Secure Data Share

Secure Data Share hands another Snowflake account — typically a customer's own account, or an internal analytics account — read-only access to the data Versus has already modeled, using Snowflake's native sharing. The consumer queries it without a copy leaving your account: no ETL, no file transfer, no second warehouse. This is the managed_share connection mode (Mode B).

A share is created through the connection's provisioning action alongside the raw landing objects, pipes, and ELT:

POST /api/warehouse/connections/{id}/provision/
Authorization: Bearer …
Content-Type: application/json

{
  "raw":   true,
  "elt":   true,
  "share": "CONSUMER_ORG.CONSUMER_ACCOUNT"
}
200 OK
{ "provisioned": ["raw", "elt", "share"] }

Provisioning is idempotent — safe to re-run — and requires a reachable Snowflake account (you get a 502 with the cause otherwise). When you pass a share target, Versus creates a share over that tenant's modeled STAGING schema, grants read-only SELECT on its tables, and adds the consumer account to the share. Because it's tenant-scoped, a consumer only ever sees the one tenant's data — the share never spans the schema-per-tenant boundary.

PropertyBehavior
AccessRead-only (SELECT) on the tenant's modeled tables.
CopyZero-copy — the consumer queries in place.
ScopeOne tenant's STAGING schema; never cross-tenant.
ConsumerThe Snowflake account you name in share.

Before provisioning anything, validate the connection with the test action — it checks authentication and reachability and reports the role and warehouse it connected as, without ever returning the secret:

POST /api/warehouse/connections/{id}/test/
→ { "ok": true, "account": "ab12345", "detail": "connected as role=… warehouse=…" }

How connections are secured

A SnowflakeConnection stores the account coordinates (account locator, region, warehouse, database, role) and the behavior (flows, enabled domains, sync mode, PII policy) — but never the credential. Authentication supports key-pair (RSA JWT) or OAuth, and the connection holds only a reference to the secret in the Versus Vault. The connector resolves that reference at use-time and fails closed if it can't; the value is never persisted to the connection row, never logged, and never echoed back through the API.

SettingNotes
modecustomer_push (Mode A), managed_share (Mode B), internal (Mode C).
auth_methodkeypair (RSA JWT), oauth_cc, or oauth_authcode.
networkPublic (TLS + network policy) or PrivateLink.
pii_policyMask, hash, or include — applied to exported data.
secret_refVault path to the key or OAuth secret. Write-only; the API never returns it.

The Snowflake driver is loaded lazily, so an install without the SDK — or a connection that's never exercised — carries no runtime dependency at all. The read paths described here only do work when you invoke them against a configured, reachable connection.


Related