Incident response playbook
Detect → contain → remediate → validate — one playbook per chain, keyed to the SQL the SOC actually runs.
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.
- Pull the offending
QUERY_HISTORYrows for the firing session — query text, bytes written, external stage URL, user/role/client IP/session ID. - Pull
LOGIN_HISTORYfor the user across the last 30 days — identify the session'sFIRST_AUTHENTICATION_FACTOR, source IP, and any prior anomalous logins from the same IP. - Identify every
CREATE STAGEby 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;
-- 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>;
- 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.
-- 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).
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.
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>;
- 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.
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.
-- 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>;
- 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_QUARANTINEwatchlist so re-install attempts are caught.
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.
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;
-- 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;
- 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_minutesso 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.
-- 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;
- 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_listmatching the documented orchestration egress range. - Run
pat_discovery.pyagainst 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.
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;
-- 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>;
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 SHAREorREPLICATIONADMINgrants and is implicated. - Audit
OPS.SECURITY.APPROVED_SHARE_CONSUMERSand 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.
-- 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;
- 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.
-- 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>;
- 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'sbound_network_policyfield 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.