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.
The warehouse integration moves data in four independently-toggleable directions, configured per connection as flows:
| Flow | Direction | What it does |
|---|---|---|
export | Outbound | Incrementally ships canonical rows (devices, ITSM tickets, CVE catalog) to Snowflake. |
query | Inbound (read) | Runs governed, named catalog queries against your modeled analytics views. |
enrich | Inbound (write-back) | Updates a strict allow-list of device fields from a Snowflake source. |
inventory_import | Inbound | Reconciles 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.
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:
| Column | Meaning |
|---|---|
tenant_id | Owning tenant (null for global dimensions like the CVE catalog). |
_versus_op | upsert or delete — drives the downstream merge and delete tombstones. |
_versus_ts | Source change timestamp — the last-writer-wins key. |
_ingested_at | When 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 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 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:
| Name | Returns |
|---|---|
vuln_trend_12mo | Monthly vulnerability counts for a device over the last 12 months. |
ticket_volume_90d | Ticket 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 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:
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.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.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 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.
| Property | Behavior |
|---|---|
| Access | Read-only (SELECT) on the tenant's modeled tables. |
| Copy | Zero-copy — the consumer queries in place. |
| Scope | One tenant's STAGING schema; never cross-tenant. |
| Consumer | The 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=…" }
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.
| Setting | Notes |
|---|---|
mode | customer_push (Mode A), managed_share (Mode B), internal (Mode C). |
auth_method | keypair (RSA JWT), oauth_cc, or oauth_authcode. |
network | Public (TLS + network policy) or PrivateLink. |
pii_policy | Mask, hash, or include — applied to exported data. |
secret_ref | Vault 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.