Incident response playbook

Detect → contain → remediate → validate — one playbook per chain, keyed to the SQL the SOC actually runs.

Scope. Each playbook below covers the Snowflake-side actions a SOC takes in the first 60 minutes of an active incident on the named chain. Cloud-side (S3/Azure/GCS bucket-policy actions), IdP-side (Okta/Entra token revocation), and endpoint-side (EDR isolation) steps are referenced but not spelled out — those belong in the parallel cloud / IdP / endpoint IR runbooks. The Snowflake-specific containment SQL is what this page contributes.
Before running containment SQL. Take the forensic snapshot first. SHOW and DESCRIBE commands are non-destructive; capture their output to evidence storage before running any ALTER, REVOKE, or DROP. Once a user is disabled or a share is dropped, the audit record of what the attacker reached may become harder to reconstruct. Snowflake's SNOWFLAKE.ACCOUNT_USAGE retention is 365 days; for OCR-grade reconstruction (six years), forensic capture goes to the long-retention SIEM sink documented in the recommendations timeline.

Chain A — Credential theft → bulk exfil

Trigger: bulk_exfil_baseline.yml or bulk_exfil_baseline_trail.yml fires; or a COPY INTO @external_stage to an unrecognised destination shows up in QUERY_HISTORY.

Detect
  • Pull the offending QUERY_HISTORY rows for the firing session — query text, bytes written, external stage URL, user/role/client IP/session ID.
  • Pull LOGIN_HISTORY for the user across the last 30 days — identify the session's FIRST_AUTHENTICATION_FACTOR, source IP, and any prior anomalous logins from the same IP.
  • Identify every CREATE STAGE by the user in the last 30 days; the attacker's exfil destination may have been created earlier as a staging step.
-- Forensic snapshot (run first, capture output)
SELECT user_name, role_name, query_id, query_text, bytes_written_to_result,
       external_stage_url, client_ip, session_id, start_time
  FROM snowflake.account_usage.query_history
 WHERE user_name = '<user>'
   AND start_time > dateadd(hour, -24, current_timestamp());

SELECT login_history.* FROM snowflake.account_usage.login_history
 WHERE user_name = '<user>'
   AND event_timestamp > dateadd(day, -30, current_timestamp())
 ORDER BY event_timestamp DESC;
Contain
-- Disable the user immediately; preserve the audit attribution.
ALTER USER <user> SET DISABLED = TRUE;

-- Suspend any task the user owns (the attacker may have scheduled persistence).
-- Enumerate first, then suspend each named task.
SHOW TASKS IN ACCOUNT;
ALTER TASK <db>.<schema>.<task_name> SUSPEND;

-- Tighten network policy to lock to a single placeholder address so any
-- token still active (key-pair, PAT) cannot reach Snowflake until cleared.
CREATE OR REPLACE NETWORK POLICY ir_lockdown_<user>
  ALLOWED_IP_LIST = ('127.0.0.1/32');
ALTER USER <user> SET NETWORK_POLICY = ir_lockdown_<user>;

If the user was authenticating by key-pair, also clear the public key so the cached private key cannot re-authenticate even if the network policy is later relaxed:

ALTER USER <user> SET RSA_PUBLIC_KEY = NULL, RSA_PUBLIC_KEY_2 = NULL;

If the user has active PATs, revoke them:

SHOW USER PROGRAMMATIC ACCESS TOKENS FOR USER <user>;
ALTER USER <user> REMOVE PROGRAMMATIC ACCESS TOKEN <pat_name>;
Remediate
  • Rotate the credential: password reset (with MFA re-enrollment) for human users; new key-pair (with passphrase) for service users; new PAT with reduced scope.
  • Drop attacker-controlled stages: DROP STAGE <db>.<schema>.<stage>;
  • Review and tighten the user's role grants to enforce minimum-necessary for the post-incident posture.
  • If the user is a service identity, audit every CI / orchestration host that holds the credential and rotate on each host.
Validate
-- Confirm no recent COPY INTO external_stage by the user.
SELECT count(*) FROM snowflake.account_usage.query_history
 WHERE user_name = '<user>'
   AND query_type = 'COPY'
   AND query_text ILIKE '%@%'
   AND start_time > dateadd(hour, -1, current_timestamp());

-- Confirm no logins from non-allowlisted IPs.
SELECT client_ip, count(*) FROM snowflake.account_usage.login_history
 WHERE user_name = '<user>'
   AND event_timestamp > dateadd(hour, -1, current_timestamp())
 GROUP BY 1;

Both queries should return 0 / empty. Once verified, re-enable the user with the new credential and the production network policy.

Chain B — Cortex Code IPI → cred theft (CVE-2026-6442)

Trigger: cortex_code_pre_1_0_25.yml fires (endpoint EDR detected an unpatched CLI), or cortex_code_session_to_unknown_session.yml fires (developer host had a Cortex Code session immediately before a Snowflake login from a new IP).

Detect

The host is the primary forensic surface; pull the developer endpoint's Cortex Code session log at ~/.cortex/sessions/*.log and shell history. Snowflake side: pull LOGIN_HISTORY for the developer across the post-Cortex-session window and check for KEY_PAIR logins from IPs outside the developer's historic range.

Contain

Endpoint side: EDR isolation of the developer host (parallel runbook). Snowflake side:

-- Disable the developer user and clear cached-key authentication.
ALTER USER <developer> SET DISABLED = TRUE;
ALTER USER <developer> SET RSA_PUBLIC_KEY = NULL, RSA_PUBLIC_KEY_2 = NULL;

-- Revoke any active PAT for the developer (Cortex Code may have cached one).
SHOW USER PROGRAMMATIC ACCESS TOKENS FOR USER <developer>;
ALTER USER <developer> REMOVE PROGRAMMATIC ACCESS TOKEN <pat_name>;
Remediate
  • Force-upgrade Cortex Code CLI to ≥ 1.0.25 across all developer endpoints (package-manager rollout + EDR check).
  • Rotate every cached credential on the affected host: Snowflake tokens, GitHub tokens, cloud SDK credentials — assume anything Cortex Code could read is compromised.
  • Run the retrospective hunt against the pre-patch exposure window for every developer who ran Cortex Code before 2026-02-28.
Validate

EDR confirms Cortex Code CLI version ≥ 1.0.25 across all in-scope endpoints. LOGIN_HISTORY shows no KEY_PAIR or OAUTH logins for the developer from non-allowlisted IPs in the post-rotation window.

Chain C — Native App supply-chain

Trigger: native_app_unexpected_version_bump.yml, native_app_privilege_bump.yml, or native_app_dependency_drift.yml fires on an app installed in the consumer account.

Detect & Contain
-- Identify the upgraded app + its current privilege scope.
SELECT application_name, current_version, previous_version, manifest_diff_added,
       auto_upgrade
  FROM snowflake.account_usage.application_history
 WHERE event_type = 'APP_VERSION_INSTALLED'
   AND application_name = '<app>'
 ORDER BY event_timestamp DESC LIMIT 5;

-- Suspend the application immediately (preserves grants for forensics).
ALTER APPLICATION <app> SUSPEND;

-- If suspension is not enough (e.g., the app's grants are actively being
-- exercised by a stored procedure on a Task), drop the application:
DROP APPLICATION <app>;
Remediate
  • Audit every other consumer-side grant the app received; revoke any wide-scope grants that were not in the consumer's original approval.
  • If the app integrates with EAIs, drop any EAI the app introduced via the new version.
  • Notify the provider through their published security contact; gather forensic evidence to support the report.
  • Add the app to the OPS.SECURITY.NATIVE_APP_QUARANTINE watchlist so re-install attempts are caught.
Validate

Confirm the application no longer appears in SHOW APPLICATIONS IN ACCOUNT; confirm no Tasks or stored procedures from the app's database are still scheduled. Run native_app_dependency_drift.yml against the rollback to confirm no orphaned dependencies remain.

Chain D — Federated IdP compromise

Trigger: federated_login_anomaly.yml fires — Snowflake login with no corresponding IdP sign-in event after the watermark window has elapsed.

Detect

The Snowflake side is downstream of the compromise. Coordinate with the IdP IR team immediately; the IdP-side actions (revoke the user's sessions, force re-auth with FIDO/passkey, rotate the IdP signing key if Golden SAML suspected) are the load-bearing containment.

Snowflake-side forensic snapshot:

SELECT user_name, authentication_method, client_ip, client_app_id, event_timestamp
  FROM snowflake.account_usage.login_history
 WHERE user_name = '<user>'
   AND event_timestamp > dateadd(day, -7, current_timestamp())
 ORDER BY event_timestamp DESC;
Contain (Snowflake side)
-- Disable the user at Snowflake regardless of IdP-side status; the user's
-- forged assertion is what reached Snowflake, and disabling the user
-- locks out further federated logins until the IdP side is cleaned.
ALTER USER <user> SET DISABLED = TRUE;

-- If the user holds ACCOUNTADMIN/SECURITYADMIN, immediately revoke and
-- reassign to a known-trusted operator.
REVOKE ROLE ACCOUNTADMIN FROM USER <user>;
REVOKE ROLE SECURITYADMIN FROM USER <user>;

If a Golden-SAML-class attack is suspected, the SAML integration itself may be untrusted until the IdP signing key is rotated. Optionally suspend the integration:

SHOW SECURITY INTEGRATIONS;
ALTER SECURITY INTEGRATION <saml_integration> SET ENABLED = FALSE;
Remediate
  • Coordinate with the IdP IR team to confirm IdP-side compromise scope and rotate the IdP signing key.
  • Re-enable the SAML integration only after the IdP confirms the signing key has been rotated and the affected sessions revoked.
  • Audit every other user the IdP-side compromise could have authenticated as — particularly high-privileged roles.
  • Increase the rule's correlation strictness for the post-incident window (lower idp_correlation_window_minutes so any further anomalies fire faster).

Chain F — Key-pair JWT auth abuse

Trigger: snowflake_keypair_auth_abuse.yml or snowflake_keypair_auth_abuse_trail.yml fires — a key-pair user signs in from a source outside the documented orchestration egress range.

Detect & Contain
-- Forensic snapshot of the service user's login history.
SELECT login_history.*, network_policy.network_policy_name
  FROM snowflake.account_usage.login_history
  LEFT JOIN snowflake.account_usage.network_policies network_policy
    ON login_history.user_name = network_policy.user_name
 WHERE login_history.user_name = '<service_user>'
   AND event_timestamp > dateadd(day, -30, current_timestamp())
 ORDER BY event_timestamp DESC;

-- Clear the public key so the leaked private key cannot sign new JWTs.
ALTER USER <service_user> SET RSA_PUBLIC_KEY = NULL, RSA_PUBLIC_KEY_2 = NULL;

-- Disable the user as a belt-and-suspenders step.
ALTER USER <service_user> SET DISABLED = TRUE;
Remediate
  • Identify every CI runner, dbt orchestration host, Airflow worker, and developer laptop that holds the leaked private key; rotate on each host with a new passphrase-protected key.
  • Bind a network policy to the user with allowed_ip_list matching the documented orchestration egress range.
  • Run pat_discovery.py against the lab fixture-root equivalents on the affected hosts to confirm no other credentials live in plain text at the same paths.
  • Migrate the credential to a managed secret store (Vault, AWS Secrets Manager, Azure Key Vault) so future rotation is centralized.

Chain G — Direct Share / Replication exfil

Trigger: snowflake_share_creation_unknown_consumer.yml or snowflake_replication_group_unknown_target.yml fires — an ALTER SHARE … ADD ACCOUNTS or replication-group event targets an account not on the approved-consumer watchlist.

Detect
SELECT share_name, accounts_added, owner_role, event_timestamp
  FROM snowflake.account_usage.shares
 WHERE event_timestamp > dateadd(day, -7, current_timestamp())
 ORDER BY event_timestamp DESC;

SELECT replication_group_name, target_account_name, event_timestamp
  FROM snowflake.account_usage.replication_group_usage_history
 WHERE event_timestamp > dateadd(day, -7, current_timestamp())
 ORDER BY event_timestamp DESC;
Contain
-- Remove the unauthorized consumer from the share.
ALTER SHARE <share_name> REMOVE ACCOUNTS = ('<attacker_org>.<attacker_account>');

-- If the entire share is suspect, drop it.
DROP SHARE <share_name>;

-- For replication groups, suspend before dropping.
ALTER REPLICATION GROUP <group_name> SUSPEND;
DROP REPLICATION GROUP <group_name>;

-- Revoke ROLE OWNERSHIP and the share-create privilege from the actor.
REVOKE CREATE SHARE ON ACCOUNT FROM ROLE <actor_role>;
REVOKE OWNERSHIP ON SHARE <share_name> FROM ROLE <actor_role>;
Remediate & Validate

Critical: source-side QUERY_HISTORY does not log the byte motion through the share. Forensic reconstruction of what the consumer read requires the consumer-side ACCESS_HISTORY — which the consumer owns. Invoke the pre-arranged BAA acquisition path (see the recommendations timeline 91–180-day section) to obtain that audit.

  • Disable or rotate any service account that held CREATE SHARE or REPLICATIONADMIN grants and is implicated.
  • Audit OPS.SECURITY.APPROVED_SHARE_CONSUMERS and confirm the watchlist accurately reflects the post-incident state.
  • For OCR reporting, treat the entire shared dataset as breached unless the consumer-side audit can definitively bound the reads.

Chain I — Cortex Agent MCP poisoning

Trigger: any cortex_agent_* rule fires — a tool-call follow-up, SQL execution from tool output, or rank anomaly indicates planner-steering. Rule fires require the Cortex Agent per-step trace sidecar; without it the trigger comes from manual review of agent outputs.

Detect & Contain
-- Disable the affected Cortex Agent immediately.
ALTER CORTEX AGENT <agent_name> SUSPEND;

-- Or, if the agent is one of several, revoke the role-level grant that
-- allows users to invoke it.
REVOKE USAGE ON CORTEX AGENT <agent_name> FROM ROLE <invoker_role>;

-- Identify any SQL executed by the agent that reads PHI tables.
SELECT user_name, role_name, query_id, query_text, bytes_written_to_result, start_time
  FROM snowflake.account_usage.query_history
 WHERE warehouse_name = '<agent_warehouse>'
   AND start_time > dateadd(hour, -24, current_timestamp())
   AND query_text ILIKE '%FROM <phi_table>%'
 ORDER BY start_time DESC;
Remediate
  • Identify the poisoning source: which Cortex Search index, which MCP tool output, which document contained the injection.
  • Remove the poisoning artifact (un-index the document, remove the MCP server from the agent's registry, etc.).
  • Confirm row-access policies on PHI tables are in force — the agent's effective RBAC is the load-bearing control, not the agent-side guardrails.
  • Enable Cortex Guardrails healthcare-tier policy if not already in force.
  • For the affected agent, review the semantic model and tool set; remove any DML-capable tool from an agent that also reads PHI.

Chain J — Partner-integration credential replay

Trigger: partner_integration_credential_replay.yml or partner_integration_credential_replay_trail.yml fires — a partner user signs in from a source outside the partner's documented egress range. Or: the partner notifies the customer of a compromise.

Detect & Contain
-- Forensic snapshot of partner-user activity.
SELECT user_name, client_ip, client_app_id, authentication_method, event_timestamp
  FROM snowflake.account_usage.login_history
 WHERE user_name = '<partner_user>'
   AND event_timestamp > dateadd(day, -30, current_timestamp())
 ORDER BY event_timestamp DESC;

-- Disable the partner user immediately and revoke the credential.
ALTER USER <partner_user> SET DISABLED = TRUE;
ALTER USER <partner_user> SET RSA_PUBLIC_KEY = NULL, RSA_PUBLIC_KEY_2 = NULL;

-- Revoke any PATs the partner held.
SHOW USER PROGRAMMATIC ACCESS TOKENS FOR USER <partner_user>;
ALTER USER <partner_user> REMOVE PROGRAMMATIC ACCESS TOKEN <pat_name>;
Remediate
  • Coordinate with the partner's IR team; the partner's BAA establishes the customer's right to audit.
  • Rotate the partner credential and re-issue with a bound network policy if the partner can publish stable egress CIDRs.
  • If the partner cannot publish stable egress CIDRs, escalate to the architectural remediation in the recommendations timeline 91–180-day section: move to a scoped Direct Share with the partner as consumer rather than the partner holding the customer's credential.
  • For OCR reporting: the partner's discovery date starts the 60-day notification clock; track the chain carefully because the customer is the OCR-side reporting party even if the compromise is at the partner.

Cross-cutting actions

Forensic-capture template

Run before any containment SQL on any chain. Captures the user, role, session, query, and login state at the moment of detection.

CREATE OR REPLACE TABLE OPS.IR.SNAPSHOT_<incident_id> AS
SELECT current_timestamp() AS snapshot_at, *
  FROM snowflake.account_usage.query_history
 WHERE user_name = '<user>'
   AND start_time > dateadd(day, -30, current_timestamp());

CREATE OR REPLACE TABLE OPS.IR.LOGIN_SNAPSHOT_<incident_id> AS
SELECT current_timestamp() AS snapshot_at, *
  FROM snowflake.account_usage.login_history
 WHERE user_name = '<user>'
   AND event_timestamp > dateadd(day, -30, current_timestamp());

Account-wide enumeration

For incidents whose scope is unclear at trigger time, enumerate the actor's reachable surface.

-- All roles granted to the user.
SHOW GRANTS TO USER <user>;

-- All grants those roles received.
SELECT grantee_name, role_name, granted_on, name, privilege
  FROM snowflake.account_usage.grants_to_roles
 WHERE grantee_name IN (SELECT role FROM table(result_scan(last_query_id())));

-- All tasks the user owns.
SELECT name, database_name, schema_name, state, owner
  FROM snowflake.information_schema.task_history
 WHERE owner IN (SELECT role FROM table(result_scan(last_query_id())));

Watchlist hygiene during IR

During an active incident, the relevant watchlists should be quarantined rather than updated:

  • OPS.SECURITY.APPROVED_EXFIL_STAGES — do not add the attacker's stage to the watchlist as a noise-suppression measure; treat the alert as load-bearing.
  • OPS.SECURITY.APPROVED_SHARE_CONSUMERS — same. Do not pre-approve the unknown consumer to silence the rule.
  • OPS.SECURITY.PARTNER_REGISTRY — review the partner's entry; the entry's bound_network_policy field may need tightening post-incident.

Recovery and lessons learned

After containment and remediation, run the recommendations timeline gap analysis: which control in the 30 / 60 / 90 / 180 day plan was missing or partially deployed at the time of the incident? Use the gap as the prioritization input for the next sprint — retrospectives that produce just an "improve detection" item are not actionable; the timeline gives the missing artifact a name.