<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kevin Keller on Medium]]></title>
        <description><![CDATA[Stories by Kevin Keller on Medium]]></description>
        <link>https://medium.com/@kellerkev?source=rss-51541d7e9f0------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*e6sJ5cBomXAAlW1p</url>
            <title>Stories by Kevin Keller on Medium</title>
            <link>https://medium.com/@kellerkev?source=rss-51541d7e9f0------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 16 Oct 2026 11:26:28 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kellerkev/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Query Your On-Premise DataLake Through a Private Tunnel with Snowflake]]></title>
            <link>https://medium.com/@kellerkev/query-your-on-premise-datalake-through-a-private-tunnel-with-snowflake-df437b4fc813?source=rss-51541d7e9f0------2</link>
            <guid isPermaLink="false">https://medium.com/p/df437b4fc813</guid>
            <dc:creator><![CDATA[Kevin Keller]]></dc:creator>
            <pubDate>Tue, 10 Dec 2024 12:49:08 GMT</pubDate>
            <atom:updated>2024-12-11T15:31:41.258Z</atom:updated>
            <content:encoded><![CDATA[<h4>Hybrid Cloud Architecture Series with Snowflake Part 1</h4><p>There are several way to achieve a “Hybrid Cloud Architecure”, meaning to connect Snowflake to your on-premise or private Cloud resources and also the other way around.</p><p><a href="https://www.snowflake.com/engineering-blog/secure-communications-outbound-private-link/">You can use Snowflake External Access Privatelink</a> or you can expose an identity aware reverse proxy like <a href="https://github.com/oauth2-proxy/oauth2-proxy">Oauth2-Proxy</a> which Snowflake functions can access using a bearer token.</p><p>However, this post is particulary about how you can setup (hands-on) an SSH tunnel from Snowflake to your companies DMZ (demilitarized zone) in order to let Snowflake access resources behind your firewall such as on-premise data lakes, databases, APIs, files or AI models etc.</p><p>These resources may also be in a VPC/ VNET in a private Cloud network.</p><p>For those of you who can not wait to see the code, here is the Github to all the code including a Docker compose file to test locally:</p><p><a href="https://github.com/KellerKev/snowflake-ssh-tunnel.git">https://github.com/KellerKev/snowflake-ssh-tunnel.git</a></p><p><strong>Use cases:</strong></p><p>This hybrid architecture can tremendously accelerate the value which you can get out of Snowflake during your transition to the Cloud by accessing resources from the Cloud that are not yet migrated.</p><p>Hence you can use the benefits of Elastic Cloud Computing today even with legacy services.</p><p>Notably on the security front there are also huge benefts, since there are some APIs, AI models or data that will never move to the Cloud for compliance reasons.</p><p>This services as well as on-premis data can now be accessed using Snowflake and this access can be cut anytime.</p><p><strong>Reference Architecture:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*flFrCwrSvqdhyg6qINRgXw.png" /></figure><p>In essence the on-premise or private Cloud resources firewall will not open any ports, but merely create an outbound connection via VPN (or other type of TCP/SSH tunnels).</p><p>It may be connected to the DMZ via peering or just by being generally reachable in the same network (2).</p><p>On the DMZ the firewall will need to open an SSH port in order to be able to esablish an SSH tunnel or several SSH tunnels with Snowflake (1).</p><p>The DMZ will act as a bridge between the on-premise or private Cloud network and the the traffic connecting via the tunnel with Snowflake in order to keep all ingress ports on the firewall closed.</p><p>The SSH tunnel from Snowflake will be established in this demo using Snowflake Container Services (SPCS).</p><p>The container will use an SSH privatekey to establish the tunnel during start up.</p><p><strong>A note on security:</strong></p><p><strong>In production, SSH private keys should be avoided</strong> and it is advisable to use <strong>short lived SSH certificates instead.</strong></p><p>You can setup an automated process that spins the container up when needed, in order to retrieve a short lived SSH certificate from your backend and use <a href="https://docs.snowflake.com/en/developer-guide/snowflake-cli/index">Snowflake Snow-CLI t</a>o spin up the container and re-establish a tunnel using that certificate.</p><p>In the same manner, the way hard coded secrets are used in this demo in the notebooks and code,<strong> should not be done like this in production.</strong></p><p><strong>I would advise to setup proper credential brokering</strong>.</p><p>For example implement a Device Code OAuth Flow for the Jupyter notebooks to have users obtain OAuth tokens via SSO (Single Sign On) so that they can access the Iceberg Catalog.</p><p>Another recommendation is to use more Snowflake aligned open source Iceberg catalog such as<strong> </strong><a href="https://polaris.apache.org/"><strong>Apache Polaris</strong></a><strong> </strong>that implements role based access and policy based access controls similiar to Snowflake.</p><p>You may also want to consider other flows which will let the container obtain secrets from a secret manager in the backend, especially for the Streamlit example.</p><p>Secrets in code must be avoided in production.</p><p>The secrets provided via the environment variables (SSH key, user, host) will not be accessible from within the container by the way.</p><p>They will only be accessible when the container spins up and establishes the connection, so that part is solved in the demo.</p><p>However, as can be concluded, this demo is intented as a proof of concept and shows the technical feasability to access private resources behind a firewall using different query engines from Snowflake.</p><p><strong>It is not a full fledged production setup</strong>, it however provides the technical key elements needed to make such an architecture work.</p><p>Hopefully saving you valuable time in the process and unlocking new use cases.</p><p>By the way, why are we using SSH and not OpenVPN or Wireguard etc. ?</p><p>Those VPN services require the NET_ADMIN privilege for a container, which are not given in Snowflake container services today.</p><p><strong>The Demo:</strong></p><p>We will run a container in Snowflake that has two query engines (DuckDB + Spark) available to query an Iceberg Datalake with an Iceberg Rest Catalog behind a firewall.</p><p>The query engines can be used in a provided Jupyter notebook or VSCode instance (both provided in the container), in a Streamlit application (also in the container) plus a REST API that can be called from within Snowflake as a UDF.</p><p>Let’s have a quick look at these video snippets for the things you can expect and what we will be building.</p><ol><li><strong>Query an Iceberg Datalake and Catalog which are both hosted on-premise behind a firewall from Snowflake</strong> using the PRIVATE IP addresses of the self hosted Iceberg REST Catalog and Iceberg S3 Minio Datalake storage within a Jupyter notebook hosted in Snowflake SPCS.</li></ol><p>One notebook is using DuckDB (the first) and the second notebook is using Apache Spark as a query engine.</p><p>You can create additional dataframes querying Snowflake data and join this data with the on-premise data frames in the notebook.</p><p>Nice thing to note is, because we are using Spark or DuckDB as query engines in the container and not a Snowflake virtual warehouse there will be no result set cache in Snowflake from the on-premise data being queried.</p><p>Once the tunnel connection is cut there is no more data access to the on-premise data from Snowflake or anything cached in Snowflake’s internal stages.</p><p><strong>a) DuckDB query engine (with PyIceberg) to query the on-premise Iceberg data lake from Snowflake via SSH tunnel in a Jupyter notebook:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lrUrGLxxTQ67aMgO-zs0zA.gif" /></figure><p><strong>b) Apache SPARK query engine to query the on-premise Iceberg data lake from Snowflake via SSH tunnel in a Jupyter notebook:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P5VteBPnqjg5bxTkJQYhvg.gif" /></figure><p><strong>2. Query the On-Premise Data Lake directly from a Snowflake UDF:</strong></p><p>You can create a Snowflake UDF that talks to a REST API in the container, which executes the query (using DuckDB) against the Iceberg on-premise data lake directly from within Snowflake (in the bounds of the private container network) and use the UDF to join data directly with Snowflake tables in a Snowflake workbook or notebook.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Kum_TLUe-XQgmo4IADpiNQ.gif" /></figure><p>Note, that in thise case, the results of the UDF will be cached in the Snowflake internal stage result set cache.</p><p>UDFs that call container functions do not support the ablity to return tables, but only scalar results in Snowflake today.</p><p>Meaning the resulting payload can not be bigger than 16MB and needs to be normalized / flattened into a table.</p><p>I am sure that the Snowflake team will however address this limitation in the future.</p><p>You do not have any limitations joining data with Snowflake and on-premise data in the Jupyter notebook provided however.</p><p>3. <strong>Embed the on-premises Iceberg query in a Streamlit Application in the container.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Pt7-lzTascW28A7P9OBsjA.gif" /></figure><p>Here you can also see VSCode is also running in the Snowflake container for some spontanous edits.</p><p>You can also use VSCode notebooks and the VSCode Snowflake extension by the way.</p><p><strong>Building the Demo:</strong></p><p>So how can we set this up?</p><p>Before we setup anything in Snowflake itself, you will need an Iceberg S3 data lake and Iceberg REST Catalog with sample data.</p><p>For that you need get yourself a Linux server that is accessible via SSH from the internet and that you can use to install the open source Iceberg Rest Catalog and S3 Minio storage using Docker compose.</p><p>If you dont have access to a Linux server you can play with you can get 2 free ones from Oracle Cloud:</p><p><a href="https://www.oracle.com/cloud/">Explore Oracle Cloud Infrastructure</a></p><p>While in this demo will be hosting the Iceberg Catalog and data lake directoy on the DMZ, with ability to get 2 free servers you can also setup the reference architecture described earlier, but using one free Linux VM as the DMZ for the SSH tunnel and the other one to install the Iceberg data lake on.</p><p>Either way, once you have a Linux VM you will need to do at least 4things:</p><p>a) Create a non-root user</p><p>b) generate an SSH keypair for the user</p><p>c) install Docker, Docker compose and run the Iceberg data lake sample</p><p>d) For the intial setup make sure you can reach ports 9000,9001,8888,8889 temporary via http for the setup. (in Oracle Cloud you have open those ports in the security group for your Linux servers).</p><p>We will close down those ports later again.</p><p>You usually also have to open the ports with the Linux iptables firewall:</p><pre>sudo iptables -I INPUT 2 -p tcp --dport 8888 -j ACCEPT<br>sudo iptables -I INPUT 2 -p tcp --dport 8889 -j ACCEPT<br>sudo iptables -I INPUT 2 -p tcp --dport 9001 -j ACCEPT<br>sudo iptables -I INPUT 2 -p tcp --dport 9000 -j ACCEPT<br>sudo iptables-save &gt; /etc/iptables/rules.v4</pre><p>While for points a) and b) I am sure you can find all the information you need on the internet, but to get the Iceberg data lake example tu run for this demo you have to do the following:</p><p>Git clone from my adapted Duckberg (DuckDB to query Iceberg) <a href="https://github.com/KellerKev/duckberg">Git repository fork</a>:</p><pre>git clone https://github.com/KellerKev/duckberg</pre><p>Now change directory in the following folder:</p><pre>cd duckberg/duckberg/playground</pre><p>Now in the playground folder you can start:</p><pre>docker compose up -d</pre><p>Once everysthing is up and running go to this URL of your Linux VM and port to access the Jupyter notebook provided so that you can create and populate your Iceberg data lake with sample data:</p><p><a href="http://10.0.0.11:8889/notebooks//000%20Init%20Iceberg%20data.ipynb">http://x.x.x.x:8889/notebooks//000%20Init%20Iceberg%20data.ipynb</a></p><p>Where x.x.x.x is the ip address of your Linux VM.</p><p>You should see this notebook:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/986/1*P80Y1LXk0FFxKE1D9BabPg.png" /></figure><p>Now go ahead and execute every cell.</p><p>To test if everything is ok you can go to:</p><p><a href="http://10.0.0.11:8888/notebooks/001%20-%20Duckberg%20simple%20query%20-%20REST%20Iceberg%20catalog.ipynb">http://x.x.x.x:8888/notebooks/001%20-%20Duckberg%20simple%20query%20-%20REST%20Iceberg%20catalog.ipynb</a></p><p>And query your data lake with Duckberg locally:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/969/1*uY_PKrLNXVbOGViMFzEOLg.png" /></figure><p>You will also be able to access the s3 mino storage to see your parquet files and metadata files in the object storage:</p><p><a href="http://10.0.0.11:9001/">http://x.x.x.x:9001/</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XVhSCyP61CCtVAU7N_VTLQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/999/1*CsaU1od3pF10POFZ8p_qYg.png" /></figure><p>Now that everything is up and running in your Linux VM you can close the ports 9000,9001,8888 and 8889.</p><p>You can do that in the security groups in Oracle Cloud (or AWS, Azure etc.) or your firewall and you can also close them down again with iptables:</p><pre>sudo iptables -I INPUT 2 -p tcp --dport 8888 -j DROP<br>sudo iptables -I INPUT 2 -p tcp --dport 8889 -j DROP<br>sudo iptables -I INPUT 2 -p tcp --dport 9001 -j DROP<br>sudo iptables -I INPUT 2 -p tcp --dport 9000 -j DROP<br>sudo iptables-save &gt; /etc/iptables/rules.v4</pre><p>The only port reachable going forward should be port 22 for SSH.</p><p>With this in place we can not setup the Snowflake container to create the SSH tunnel and query the data behind the firewall.</p><p>You will need the Docker image to run in Snowflake container services that I have prepared for the demo.</p><p>You can Git clone the Dockerfile to create the Docker image from my Git repository <a href="https://github.com/KellerKev/snowflake-ssh-tunnel.git">here</a>, or pull a readily prepared image from my Docker Hub to your server or workstation.</p><p>You will need to have Docker installed for this.</p><p>I assume you are going for the Docker pull route as it is easier and quicker:</p><pre>docker pull kbkeller/snowflakesshtunnel:latest</pre><p>Now setup a database, schema, warehouse and image repository in Snowflake in your Snowflake notebook:</p><p>(You can find the full SQL workbook <a href="https://github.com/KellerKev/snowflake-ssh-tunnel/blob/main/sql_workbook_spcs_snowflake_ssh_tunnels.sql">here</a> by the way)</p><pre>use role accountadmin;<br><br>CREATE OR REPLACE WAREHOUSE spcs_tunnel_wh WITH WAREHOUSE_SIZE=&#39;X-SMALL&#39;;<br><br>create role spcs_tunnel_role;<br><br>grant usage on WAREHOUSE spcs_tunnel_wh  to role spcs_tunnel_role;<br>grant operate on  WAREHOUSE spcs_tunnel_wh to role spcs_tunnel_role;<br><br>grant create database on account to role spcs_tunnel_role;<br><br><br>grant role spcs_tunnel_role to user xxx;<br><br>use role spcs_tunnel_role;<br><br><br><br><br>create database spcs_tunnel_db;<br>create schema spcs_tunnel_schema;<br><br>--temporarily <br>grant all privileges on database spcs_tunnel_db to role accountadmin;<br>grant all privileges on schema spcs_tunnel_schema to role accountadmin;<br><br>CREATE IMAGE REPOSITORY IF NOT EXISTS spcs_tunnel_images;<br><br>USE ROLE ACCOUNTADMIN;<br>alter user xxxx set mins_to_bypass_mfa=180;<br><br>SHOW IMAGE REPOSITORIES IN SCHEMA;</pre><p>Then tag the image according to the naming convention of your Snowflake image repository:</p><pre>docker tag kbkeller/snowflakesshtunnel xxxx.registry.snowflakecomputing.com/spcs_tunnel_db/spcs_tunnel_schema/spcs_tunnel_images/spcs_tunnel-image:latest</pre><p>Make sure to disable MFA for a bit before uploading:</p><pre><br>alter user xxx set mins_to_bypass_mfa=180;</pre><p>Now login and push your image to your Snowflake repository:</p><pre>docker login xxxx.registry.snowflakecomputing.com<br>docker push xxxx.registry.snowflakecomputing.com/spcs_tunnel_db/spcs_tunnel_schema/spcs_tunnel_images/spcs_tunnel-image:latest<br></pre><p>Back in the Snowflake worksheet we can now setup the compute pool and prepare everything to create the service:</p><p>Note that you have replace x.x.x.x with the IP address of your DMZ / data lake server.</p><pre><br>USE ROLE ACCOUNTADMIN;<br>CREATE  COMPUTE POOL IF NOT EXISTS SPCS_TUNNEL_COMPUTE_POOL<br>  MIN_NODES = 1<br>  MAX_NODES = 1<br>  INSTANCE_FAMILY = CPU_X64_S<br>  AUTO_RESUME = true;<br><br>  DESCRIBE COMPUTE POOL SPCS_TUNNEL_COMPUTE_POOL;<br>  drop compute pool SPCS_TUNNEL_COMPUTE_POOL;<br>  <br>grant usage on compute pool SPCS_TUNNEL_COMPUTE_POOL to role spcs_tunnel_role;<br>grant operate on compute pool SPCS_TUNNEL_COMPUTE_POOL to role spcs_tunnel_role;<br>grant monitor on compute pool SPCS_TUNNEL_COMPUTE_POOL to role spcs_tunnel_role;<br>grant modify on compute pool SPCS_TUNNEL_COMPUTE_POOL to role spcs_tunnel_role;<br><br><br>GRANT BIND SERVICE ENDPOINT ON ACCOUNT TO ROLE spcs_tunnel_role;<br><br><br>use database spcs_tunnel_db;<br>use schema spcs_tunnel_schema;<br>CREATE NETWORK RULE allow_tunnel_rule<br>  TYPE = &#39;HOST_PORT&#39;<br>  MODE= &#39;EGRESS&#39;<br>  VALUE_LIST = (&#39;x.x.x.x:22&#39;,&#39;0.0.0.0:443&#39;);<br><br>use database spcs_tunnel_db;<br>use schema spcs_tunnel_schema;<br>  <br>CREATE EXTERNAL ACCESS INTEGRATION allow_tunnel_eai<br>  ALLOWED_NETWORK_RULES=(allow_tunnel_rule)<br>  ENABLED=TRUE;<br><br>GRANT USAGE ON INTEGRATION allow_tunnel_eai TO ROLE spcs_tunnel_role;<br><br>use role spcs_tunnel_role;<br><br>revoke all privileges on database spcs_tunnel_db from role accountadmin;<br>revoke all privileges on schema spcs_tunnel_schema from role accountadmin;<br><br>  DESCRIBE COMPUTE POOL SPCS_TUNNEL_COMPUTE_POOL;<br><br>   drop  COMPUTE POOL SPCS_TUNNEL_COMPUTE_POOL;<br>  <br>CREATE or replace SECRET encodedstring<br>  TYPE=generic_string<br>  SECRET_STRING=&#39;IyEvYmluL2Jhc2ggCi9ob21lL3NwY3N0dW5uZWwvbWFtYmEvYmluL3B5dGhvbiAtbSB2ZW52IGRlZmF1bHRfcHlfZW52Ci4gL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vYWN0aXZhdGUgJiYgcGlwIGluc3RhbGwgdXYKY2QgL2hvbWUvc3Bjc3R1bm5lbCAmJiBnaXQgY2xvbmUgaHR0cHM6Ly9naXRodWIuY29tL0tlbGxlcktldi9kdWNrYmVyZwpjZCAvaG9tZS9zcGNzdHVubmVsL2R1Y2tiZXJnICYmIC4gL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vYWN0aXZhdGUgJiYgdXYgcGlwIGluc3RhbGwgLWUgLgoKbWtkaXIgL2hvbWUvc3Bjc3R1bm5lbC9qdXB5dGVyX25vdGVib29rcwpjaG93biBzcGNzdHVubmVsIC9ob21lL3NwY3N0dW5uZWwvanVweXRlcl9ub3RlYm9va3MKY3AgL2hvbWUvc3Bjc3R1bm5lbC9zYW1wbGVzL2ljZWJlcmdfbWluaW8uaXB5bmIgL2hvbWUvc3Bjc3R1bm5lbC9qdXB5dGVyX25vdGVib29rcwoKLiAvaG9tZS9zcGNzdHVubmVsL2RlZmF1bHRfcHlfZW52L2Jpbi9hY3RpdmF0ZSAmJiAgdXYgcGlwIGluc3RhbGwganVweXRlcmxhYgouIC9ob21lL3NwY3N0dW5uZWwvZGVmYXVsdF9weV9lbnYvYmluL2FjdGl2YXRlICYmIHV2IHBpcCBpbnN0YWxsIGh0dHB4PT0wLjI3LjIKLiAvaG9tZS9zcGNzdHVubmVsL2RlZmF1bHRfcHlfZW52L2Jpbi9hY3RpdmF0ZSAmJiB1diBwaXAgaW5zdGFsbCBmYXN0YXBpIHV2aWNvcm4KCiNjZCAvaG9tZS9zcGNzdHVubmVsICYmIGdpdCBjbG9uZSBodHRwczovL2dpdGh1Yi5jb20vanVweXRlcmxhYi9qdXB5dGVybGFiLmdpdAojY2QgL2hvbWUvc3Bjc3R1bm5lbC9qdXB5dGVybGFiICYmIC4gL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vYWN0aXZhdGUgJiYgdXYgcGlwIGluc3RhbGwgLWUgLiAmCiNjZCAvaG9tZS9zcGNzdHVubmVsL2p1cHl0ZXJsYWIgJiYgL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vamxwbSBydW4gYnVpbGQ6Y29yZQojY2QgL2hvbWUvc3Bjc3R1bm5lbC9qdXB5dGVybGFiICYmIC9ob21lL3NwY3N0dW5uZWwvZGVmYXVsdF9weV9lbnYvYmluL2p1cHl0ZXIgbGFiIGJ1aWxkCgouIC9ob21lL3NwY3N0dW5uZWwvZGVmYXVsdF9weV9lbnYvYmluL2FjdGl2YXRlICYmIHV2IHBpcCBpbnN0YWxsIGlweWtlcm5lbAoKLiAvaG9tZS9zcGNzdHVubmVsL2RlZmF1bHRfcHlfZW52L2Jpbi9hY3RpdmF0ZSAmJiBqdXB5dGVyIGxhYiAtLWlwICcqJyAtLXBvcnQgMzAwMCAtLW5vdGVib29rLWRpcj0vaG9tZS9zcGNzdHVubmVsL2p1cHl0ZXJfbm90ZWJvb2tzIC0tbm8tYnJvd3NlciAtLUlkZW50aXR5UHJvdmlkZXIudG9rZW49IiIgJgoKCgoKCmVjaG8gJ3NvdXJjZSAvaG9tZS9zcGNzdHVubmVsL2VudgpQT1JUMT0kKHNodWYgLWkgMjAwMC02NTAwMCAtbiAxKQpQT1JUMj0kKHNodWYgLWkgMjAwMC02NTAwMCAtbiAxKQpQT1JUMz0kKHNodWYgLWkgMjAwMC02NTAwMCAtbiAxKQpQT1JUND0kKHNodWYgLWkgMjAwMC02NTAwMCAtbiAxKQpQT1JUNT0kKHNodWYgLWkgMjAwMC02NTAwMCAtbiAxKQplY2hvICRQT1JUMyA+IC9ob21lL3NwY3N0dW5uZWwvcG9ydC50eHQKZWNobyAkUE9SVDMKZWNobyAkUE9SVDUgPiAvaG9tZS9zcGNzdHVubmVsL291dHNpZGVwb3J0LnR4dAplY2hvICRQT1JUNQpERUNPREVES0VZPSIkKHNvdXJjZSAvaG9tZS9zcGNzdHVubmVsL2VudiAmJiBlY2hvICRrZXkxIHwgYmFzZTY0IC0tZGVjb2RlKSIKZWNobyAkREVDT0RFREtFWQplY2hvICRTU0hfVVNFUgojRFlOQU1JQyBQT1JUIEZPUldBUkQgRlJPTSBDT05UQUlORVIgVE8gRE1aCgpzc2gtYWdlbnQgYmFzaCAtYyAic3NoLWFkZCA8KGVjaG8gIFwiJERFQ09ERURLRVlcIikgJiYgYXV0b3NzaCAgIC1NICRQT1JUMSAtbyBTdHJpY3RIb3N0S2V5Q2hlY2tpbmc9bm8gLW8gU2VydmVyQWxpdmVJbnRlcnZhbD02MCAtbyBFeGl0T25Gb3J3YXJkRmFpbHVyZT15ZXMgLWduTlQgIC1EIDEwODAgLUMgLU4gJFNTSF9VU0VSQCRETVpfSVAiICYKCiNPUFRJT05BTExZICBQT1JUIEZPUldBUkQgMzAwMSAoU3RyZWFtbGl0KSB0byBETVoKI3NzaC1hZ2VudCBiYXNoIC1jICJzc2gtYWRkIDwoZWNobyAgXCIkREVDT0RFREtFWVwiKSAmJiAgYXV0b3NzaCAgIC1NICRQT1JUNCAtbyBTdHJpY3RIb3N0S2V5Q2hlY2tpbmc9bm8gLW8gU2VydmVyQWxpdmVJbnRlcnZhbD02MCAtbyBFeGl0T25Gb3J3YXJkRmFpbHVyZT15ZXMgLWduTlQgLUMgLU4gIC1SIGxvY2FsaG9zdDokUE9SVDU6bG9jYWxob3N0OjMwMDIgICAkU1NIX1VTRVJAJERNWl9JUCIgJgoKI09QVElPTkFMTFkgIFBPUlQgRk9SV0FSRCAyMiAoU1NIKSB0byBETVoKI3NzaC1hZ2VudCBiYXNoIC1jICJzc2gtYWRkIDwoZWNobyAgXCIkREVDT0RFREtFWVwiKSAmJiAgYXV0b3NzaCAgIC1NICRQT1JUMiAtbyBTdHJpY3RIb3N0S2V5Q2hlY2tpbmc9bm8gLW8gU2VydmVyQWxpdmVJbnRlcnZhbD02MCAtbyBFeGl0T25Gb3J3YXJkRmFpbHVyZT15ZXMgLWduTlQgLUMgLU4gIC1SIGxvY2FsaG9zdDokUE9SVDM6bG9jYWxob3N0OjIyICAgJFNTSF9VU0VSQCRETVpfSVAiICYnID4gL2hvbWUvc3Bjc3R1bm5lbC90dW5uZWwuc2gKY2htb2QgK3ggL2hvbWUvc3Bjc3R1bm5lbC90dW5uZWwuc2gKL2hvbWUvc3Bjc3R1bm5lbC90dW5uZWwuc2ggJgpzbGVlcCA1Ci4gL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vYWN0aXZhdGUgJiYgY2QgL2hvbWUvc3Bjc3R1bm5lbC8gJiYgdXZpY29ybiByZXN0YXBpX2ljZWJlcmc6YXBwIC0taG9zdCAwLjAuMC4wIC0tcG9ydCAzMDAzIC0tcmVsb2FkICYKCmNkIC9ob21lL3NwY3N0dW5uZWwgJiYgbWtkaXIgc3RyZWFtbGl0X2FwcHMKY3AgL2hvbWUvc3Bjc3R1bm5lbC9zYW1wbGVzL3N0cmVhbWxpdC9tYWluLnB5IC9ob21lL3NwY3N0dW5uZWwvc3RyZWFtbGl0X2FwcHMKY2QgL2hvbWUvc3Bjc3R1bm5lbC8gJiYgLiAvaG9tZS9zcGNzdHVubmVsL2RlZmF1bHRfcHlfZW52L2Jpbi9hY3RpdmF0ZSAmJiB1diBwaXAgaW5zdGFsbCBzdHJlYW1saXQgbWl0b3NoZWV0IG51bXB5CiNjZCAvaG9tZS9zcGNzdHVubmVsL3N0cmVhbWxpdF9hcHBzICYmIC4gL2hvbWUvc3Bjc3R1bm5lbC9kZWZhdWx0X3B5X2Vudi9iaW4vYWN0aXZhdGUgJiYgc3RyZWFtbGl0IHJ1biBtYWluLnB5IC0tc2VydmVyLnBvcnQgMzAwMiAtLXNlcnZlci5hZGRyZXNzIDAuMC4wLjAgLS1zZXJ2ZXIucnVuT25TYXZlIHRydWUgICBlbmFibGVYc3JmUHJvdGVjdGlvbj1mYWxzZSBlbmFibGVDT1JTPWZhbHNlICYKY2QgL2hvbWUvc3Bjc3R1bm5lbC9zdHJlYW1saXRfYXBwcyAmJiAuIC9ob21lL3NwY3N0dW5uZWwvZGVmYXVsdF9weV9lbnYvYmluL2FjdGl2YXRlICYmIHN0cmVhbWxpdCBydW4gbWFpbi5weSAtLXNlcnZlci5wb3J0IDMwMDIgLS1zZXJ2ZXIuYWRkcmVzcyAwLjAuMC4wIC0tc2VydmVyLnJ1bk9uU2F2ZSB0cnVlIGVuYWJsZUNPUlM9ZmFsc2UgJgouIC9ob21lL3NwY3N0dW5uZWwvZGVmYXVsdF9weV9lbnYvYmluL2FjdGl2YXRlICYmIHV2IHBpcCBpbnN0YWxsIC1lIC9ob21lL3NwY3N0dW5uZWwvc3RyZWFtbGl0X2FwcHMvc3BhcmstMy41LjMtYmluLWhhZG9vcDMvcHl0aG9u&#39;;</pre><p>The “encodedstring” secret is a Base64 encoded shell script that is executed when the container spins up.</p><p>This is probably an un-usual way to initilaze the container, but it helps tremendously for testing as I can change the script easily and thereby changing the behaviour of the container.</p><p>We will explore this in my next blog post how this can speed up development when you create apps in container services.</p><p>For now feel free to go to <a href="https://www.base64decode.org/">https://www.base64decode.org/</a> and decode the Base64 encoded script to see what it does.</p><p>It’s mainly installing Jupyterlab, Spark but also creating the SSH tunnel from an environment variable.</p><p>This is the part where the magic happens:</p><pre><br>ssh-agent bash -c &quot;ssh-add &lt;(echo  \&quot;$DECODEDKEY\&quot;) &amp;&amp; autossh   -M $PORT1 -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -gnNT  -D 1080 -C -N $SSH_USER@$DMZ_IP&quot; &amp;</pre><p>You might have noticed at this stage that we not supplied the private key for the startup script yet.</p><p>So let’s continue in the Snowflake worksheet and add those secrets.</p><p>The following SQL will create the secrets necessary for the container.</p><pre>CREATE or replace SECRET servicesecrets<br>  TYPE=generic_string<br>SECRET_STRING=&#39;{<br>    &quot;SSH_USER&quot;: &quot;xxxx&quot;,<br>    &quot;DMZ_IP&quot;: &quot;x.x.x.x&quot;,<br>    &quot;SSHKEYS&quot;: [<br>        {<br>            &quot;key1&quot;: &quot;LS0tLS1CRUdJTiBPU.....&quot;<br>        }<br>    ]<br>}&#39;;</pre><p>Replace SSH user with the username of your Linux user of your DMZ / Iceberg data lake server and DMZ_IP with the corresponding IP.</p><p>In order to pass the SSH key to the container we need to Base64 encode it .</p><p>So take the private SSH key you created for the DMZ / Iceberg data lake server and head over to:</p><p><a href="https://www.base64encode.org/">Base64 Encode and Decode - Online</a></p><p>Paste the Base64 encoded key in the “key1” json key and as the value.</p><p>Now that you have supplied the user, IP and SSH key in the “servicessecrets” Snowflake secret and we have the startup script ready, we can finally start the container in Snowflake and start playing.</p><p>Here is how you start the container now (replace the image path url):</p><pre>CREATE SERVICE<br>  spcs_tunnel_service<br>  IN COMPUTE POOL SPCS_TUNNEL_COMPUTE_POOL<br>  EXTERNAL_ACCESS_INTEGRATIONS = (allow_tunnel_eai)<br>  MIN_INSTANCES=1<br>  MAX_INSTANCES=1<br>  FROM SPECIFICATION<br>  $$<br>    spec:<br>        containers:<br>         - name: &quot;spcstunnelservice&quot;<br>           image: &quot;xxx.xxxx.registry.snowflakecomputing.com/spcs_tunnel_db/spcs_tunnel_schema/spcs_tunnel_images/spcs_tunnel-image:latest&quot;<br>           secrets:<br>            - snowflakeSecret: encodedstring<br>              envVarName: ENCODEDRUNSCRIPT<br>            - snowflakeSecret: servicesecrets<br>              envVarName: SERVICESECRETS<br>           env:<br>               SSHKEY: &quot;False&quot;<br>               ENVNAME: &quot;snowtunnel&quot; <br>               VSVERSION: &quot;1.95.2&quot;<br>                   <br>        endpoints:<br>        - name: jupyter<br>          port: 3000<br>          public: true<br>        - name: vscode<br>          port: 3001<br>          public: true<br>        - name: streamlit<br>          port: 3002<br>          public: true<br>        - name: restapi<br>          port: 3003<br>  $$;</pre><p>This will spin up the service, install everything and build the tunnel to your DMZ.</p><p>You can follow the process with the following SQLs:</p><pre><br>describe service spcs_tunnel_service;<br><br><br>--alter service spcs_tunnel_service suspend;<br>--alter service spcs_tunnel_service resume;<br><br>--drop service spcs_tunnel_service;<br><br>SHOW SERVICE CONTAINERS IN SERVICE spcs_tunnel_service;<br><br><br><br>CALL SYSTEM$GET_SERVICE_LOGS(&#39;spcs_tunnel_service&#39;, &#39;0&#39;, &#39;spcstunnelservice&#39;, 1000);<br>SHOW ENDPOINTS IN SERVICE spcs_tunnel_service;</pre><p>Most importantly the “SHOW ENDPOINTS” command which will show you the endpoints for the Jupyter notebook, Streamlit app and VSCode web instance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ymuT7XQ1_iRwMhph8F5HIQ.png" /></figure><p>You should now be able to easily access the Jupyter notebooks to query the data via the tunnel with DuckDB or Apache Spark:</p><p><strong>NOTE: You will have to change the private IP address to match the one of your DMZ / Iceberg Data Lake server!</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/280/1*UsnzW6tOqD0GfmKUdBTTig.png" /></figure><p>If you want to query the data through the tunnel via Snowflake UDF, here is how you can setup the UDF:</p><pre><br>CREATE OR REPLACE FUNCTION query_taxis_udf(sql_query STRING)<br>returns variant<br>SERVICE=spcs_tunnel_service<br>ENDPOINT=restapi<br>AS &#39;/snowflake_function&#39;;</pre><p>Now you can test it:</p><pre>SELECT query_taxis_udf(&#39;SELECT * FROM &quot;nyc.taxis&quot; LIMIT 10&#39;);</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ma1hi7WxubdXBrL83B6SKw.png" /></figure><p><strong>If if the UDF does not work you have to change the private IP address to match the one of your DMZ / Iceberg Data Lake server in the REST API code. You can use the VSCode provided in the container to quickly adapt the REST API code or update the container image.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v9efqWHrmWbxlAfdFdJF_A.png" /></figure><p>Update these IPs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/809/1*MwXsQIsypdWeL_D45fHsEA.png" /></figure><p>If all is working let’s transform the UDF scalar results, which look a bit hard to read into a flattend table:</p><pre><br><br><br>WITH udf_output AS (<br>    SELECT<br>        query_taxis_udf(&#39;SELECT * FROM &quot;nyc.taxis&quot; LIMIT 100&#39;) AS json_result<br>),<br>flattened AS (<br>    SELECT<br>        VALUE:DOLocationID::INTEGER AS DOLocationID,<br>        VALUE:PULocationID::INTEGER AS PULocationID,<br>        VALUE:RatecodeID::FLOAT AS RatecodeID,<br>        VALUE:VendorID::INTEGER AS VendorID,<br>        VALUE:airport_fee::FLOAT AS airport_fee,<br>        VALUE:congestion_surcharge::FLOAT AS congestion_surcharge,<br>        VALUE:extra::FLOAT AS extra,<br>        VALUE:fare_amount::FLOAT AS fare_amount,<br>        VALUE:improvement_surcharge::FLOAT AS improvement_surcharge,<br>        VALUE:mta_tax::FLOAT AS mta_tax,<br>        VALUE:passenger_count::FLOAT AS passenger_count,<br>        VALUE:payment_type::INTEGER AS payment_type,<br>        VALUE:store_and_fwd_flag::STRING AS store_and_fwd_flag,<br>        VALUE:tip_amount::FLOAT AS tip_amount,<br>        VALUE:tolls_amount::FLOAT AS tolls_amount,<br>        VALUE:total_amount::FLOAT AS total_amount,<br>        VALUE:tpep_dropoff_datetime::TIMESTAMP AS tpep_dropoff_datetime,<br>        VALUE:tpep_pickup_datetime::TIMESTAMP AS tpep_pickup_datetime,<br>        VALUE:trip_distance::FLOAT AS trip_distance<br>    FROM udf_output, LATERAL FLATTEN(INPUT =&gt; json_result)<br>)<br>SELECT * FROM flattened;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G4TzSVThFHK-gSvXydE_IA.png" /></figure><p>You can further simplify this by creating a view (that should be easy) or a stored procedure such as this:</p><pre>CREATE OR REPLACE PROCEDURE flatten_query_taxis_sp(<br>    p_col_list VARCHAR,<br>    p_table_name VARCHAR,<br>    p_row_limit NUMBER<br>)<br>RETURNS TABLE (<br>    DOLocationID NUMBER,<br>    PULocationID NUMBER,<br>    RatecodeID NUMBER,<br>    VendorID NUMBER,<br>    airport_fee NUMBER,<br>    congestion_surcharge NUMBER,<br>    extra NUMBER,<br>    fare_amount NUMBER,<br>    improvement_surcharge NUMBER,<br>    mta_tax NUMBER,<br>    passenger_count NUMBER,<br>    payment_type NUMBER,<br>    store_and_fwd_flag VARCHAR,<br>    tip_amount NUMBER,<br>    tolls_amount NUMBER,<br>    total_amount NUMBER,<br>    tpep_dropoff_datetime TIMESTAMP_NTZ,<br>    tpep_pickup_datetime TIMESTAMP_NTZ,<br>    trip_distance NUMBER<br>)<br>LANGUAGE SQL<br>AS<br>$$<br>DECLARE<br>    result_sql STRING;<br>    result_set RESULTSET;<br>BEGIN<br>    -- Construct the SQL query<br>    result_sql :=<br>        &#39;WITH udf_output AS (<br>            SELECT query_taxis_udf(&#39;&#39;SELECT &#39; || p_col_list || &#39; FROM &quot;&#39; || p_table_name || &#39;&quot; LIMIT &#39; || p_row_limit || &#39;&#39;&#39;) AS json_result<br>        ),<br>        flattened AS (<br>            SELECT<br>                VALUE:DOLocationID::NUMBER,<br>                VALUE:PULocationID::NUMBER,<br>                VALUE:RatecodeID::NUMBER,<br>                VALUE:VendorID::NUMBER,<br>                VALUE:airport_fee::NUMBER,<br>                VALUE:congestion_surcharge::NUMBER,<br>                VALUE:extra::NUMBER,<br>                VALUE:fare_amount::NUMBER,<br>                VALUE:improvement_surcharge::NUMBER,<br>                VALUE:mta_tax::NUMBER,<br>                VALUE:passenger_count::NUMBER,<br>                VALUE:payment_type::NUMBER,<br>                VALUE:store_and_fwd_flag::VARCHAR,<br>                VALUE:tip_amount::NUMBER,<br>                VALUE:tolls_amount::NUMBER,<br>                VALUE:total_amount::NUMBER,<br>                VALUE:tpep_dropoff_datetime::TIMESTAMP_NTZ,<br>                VALUE:tpep_pickup_datetime::TIMESTAMP_NTZ,<br>                VALUE:trip_distance::NUMBER<br>            FROM udf_output, LATERAL FLATTEN(INPUT =&gt; json_result)<br>        )<br>        SELECT * FROM flattened&#39;;<br><br>    -- Execute the dynamic SQL query and store the result in a RESULTSET variable<br>    result_set := (EXECUTE IMMEDIATE :result_sql);<br><br>    -- Return the RESULTSET<br>    RETURN TABLE(result_set);<br>END;<br>$$;</pre><p>And now you can execute the flattening query much simpler:</p><pre><br>SELECT *<br>FROM TABLE(<br>    flatten_query_taxis_sp(<br>        &#39;DOLocationID, PULocationID, fare_amount, trip_distance&#39;,<br>        &#39;nyc.taxis&#39;,<br>        10<br>    )<br>);</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2TmnvK9nIzA0roY5GcKxrQ.png" /></figure><p>The null values are hard to remove as the schema has to be defined up front for flattening.</p><p>Once Snowflake container services UDFs support table return types this in-convinience should be solved by itself.</p><p>Finally you can also check out the Streamlit app that you can use to query your on-premise data and make it available via Streamlit.</p><p>Head over to the Streamlit URL endpoint from the container services and check it out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9VOphr1kDik9jUVdrNpoaQ.png" /></figure><p>If it does not work, you can change the Streamlit app codeon the fly using the provided VSCode instance to edit code.</p><p>You probably just need to adapt the private IP address to your DMZ / Private Iceberg Data Lake server.</p><p>Paste the VSCode endpoint URL in your browser to access it and then open the Streamlit app:</p><p>Open the folder “streamlit_apps” in the container:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rY-2Pq_VnzD_0tbWMDiJnA.png" /></figure><p>And check out and play with the code and adapt the IP addreses if necessary as we did with the UDF REST API previously.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cCMgTQ7ARHexUTvhHVV4vw.png" /></figure><p>You can also open a terminal and see what else is in the container.</p><p>There are some more Spark command line sample scripts and Jupyter notebooks.</p><p><strong>Conclusion and further steps:</strong></p><p>I have picked an Iceberg data lake as an example, but nobody can stop you from using this to access other resources like on-premise databases, APIs, AI models.. you name it..</p><p>I can even see that it should be possible to direclty conect to a z/OS mainframe 3270 terminal and execute automated commands using this library: <a href="https://github.com/IBM/tnz?tab=readme-ov-file">https://github.com/IBM/tnz?tab=readme-ov-file</a></p><p>Although z/OS and other usual on-premise resources have plenty of options to create REST or other APIs on top of them these days.</p><p>In part 2 of this post, which I will link to from here asap, we will explore how we can reverse the process and expose services running in Snowflake container services to apps running outside of Snowflake directly.</p><p>That opens the door to do things like SFTP, FTP uploads to Snowflake or access an S3 Minio API directly in Snowflake.</p><p>If you reverse the process you can also expose Streamlit apps and other web apps to non-Snowflake users.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df437b4fc813" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ingest Opentelemetry Logs, Traces and Metrics Directly into Snowflake to Build your Next…]]></title>
            <link>https://medium.com/@kellerkev/ingest-opentelemetry-logs-traces-and-metrics-directly-into-snowflake-to-build-your-next-generation-b562680bd1d6?source=rss-51541d7e9f0------2</link>
            <guid isPermaLink="false">https://medium.com/p/b562680bd1d6</guid>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[security-data-lake]]></category>
            <category><![CDATA[snowflak]]></category>
            <category><![CDATA[iceberg-table]]></category>
            <dc:creator><![CDATA[Kevin Keller]]></dc:creator>
            <pubDate>Sat, 09 Nov 2024 17:06:03 GMT</pubDate>
            <atom:updated>2025-04-01T10:27:35.837Z</atom:updated>
            <content:encoded><![CDATA[<h3>Get Started Building Your Next Generation Security and Observability Iceberg Data Lake</h3><h4>Ingest Opentelemetry Logs, Traces and Metrics Directly into Snowflake as an APM and SIEM Replacement or Augumentation</h4><p>Let me show you one example how you can ingest traces, logs and metrics natively into Snowflake using the Opentelemetry receiver I put together that is able to receive logs, metrics and traces using the OTLP/gRPC protocol via HTTP(S).</p><p>You can find all my code <a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver/tree/main">here</a> to get this to work in your own Snowflake account.</p><p>But let’s look at the big picture what we can do here.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qjjgZKruE68-1KTrBH-NMA.gif" /></figure><p>If you consider that you can have <a href="https://www.youtube.com/watch?v=NHn1tzzrp5o">Snowflake write into Apache Iceberg</a> tables and you can query that data from any Iceberg compatible query engine you can create a completely open observability and security data lake without worrying about storage, storage costs or scalability.</p><p>The benefits here are that you leverage the <a href="https://opentelemetry.io/docs/collector/">Opentelemetry ecosystem</a> of connectors and plug those natively into Snowflake in order to receive logs, traces and metrics so that you can build your own observability and security data lake if you also use Snowflakes ability to create and manage Iceberg tables in your own object storage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*YgfTXG4F6HE0WKzl9Uh03w.png" /></figure><p>You can also have Snowflake natively<strong> notify </strong>you or<strong> trigger automated actions for automated incident and response management.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wtB-lT6LSfe9apmW" /></figure><p>I showed how you can do that in one of my other <strong>blog posts </strong><a href="https://medium.com/@kellerkev/how-to-let-snowflake-raise-security-incidents-in-your-siem-or-xdr-automatically-fb2ee4bea27b"><strong>here</strong></a><strong>.</strong></p><p>If you check out the <a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver/blob/main/10_mins_log_table.sql">Github repo</a>, you also see that there is a table that contains only the last 10 mins of logs. This is great for time senstive incident raising.</p><p>With <a href="https://docs.snowflake.com/en/user-guide/ui-snowsight/notebooks">Snowflake notebooks</a> you can also go ahead and do some <a href="https://www.snowflake.com/guides/threat-hunting-strengthen-your-security-stance/">threat hunting</a> or discover any bottle necks in your application code or archictecture.</p><p>Need some real time dashboards in Snowflake?</p><p>I have added an example Streamlit in Snowflake app (SiS) to the Github Repository <a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver/tree/main/streamlit_in_snowflake">here</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/866/1*bV_Zz40rwezB5pRLdGY6pA.png" /></figure><p>You can also run PromQL queries (experimental)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AKUZ6VU_82JWncrahXnZfg.png" /></figure><p>There is also a <a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver/tree/main/streamlit_in_snowflake">sample</a> for Splunk SPL queries:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8_sRcWO_E1zL3sW962sg4g.png" /></figure><pre>search metric_name=&quot;http_requests_total&quot; | table @timestamp, metric.value</pre><p>Need further information from other systems and partners?</p><p>Easy! Try the Snowflake marketplace:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OsQXXe0GwtJvphfhkWNh6Q.png" /></figure><p>You can also ingest information from Workday (or other HR systems) or Entra AD (or Okta or other IDPs) to enrich the context, here is a sample for Workday:</p><p><a href="https://medium.com/@mrwoodford7/how-to-load-workday-data-into-snowflake-using-external-network-access-25fa46733cdb">How to Load Workday Data into Snowflake Using External Network Access</a></p><p>Bottom line:</p><p>You can centralise SIEM &amp; APM, logs,traces, metrics, exceptions &amp; alerts in Snowflake.</p><p>The Opentelemetery eco-system is vast and standarized, allowing you to fetch / ingest , batch, transform and export (forward) data from many sources with ease.</p><p>You can also point Jeager and Prometheus sources to an Opentelemetry Collector.</p><p><strong>The sweet spot of this setup is probably the augementation play in which you keep using your current SIEM and APM solution for the most recent data and augement your setup by moving data to Snowflake instead of putting it into a long term storage, so all data is always ready to be queried and does not need to be hydrated first.</strong></p><p>You can point your current SIEM and APM solution to the Opentelemetry Collector which will deliver the data to Snowflake.</p><p>Enough talk.. let’s setup a demo.</p><p>For that we will use a Flask sample application that sends logs, traces and metrics to an Opentelemtry collector which forwards it to a container in Snowflake that receives it and writes this data into the respective tables.</p><p>We will use Iceberg tables for the final target tables as last step of this tutorial.</p><p>The final target tables will also be transformed to adhere to the <a href="https://www.elastic.co/guide/en/ecs/current/index.html">Elastic Common Schema</a> format in order to standarize all logs, metrics and traces from different sources in a standard schema. That makes it easier to work with the data and scale.</p><p>Here is the data flow of the demo:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jqPYadV_gaqvJT9wuqS8aw.png" /></figure><p>So lets go ahead and get this running.</p><p>We will do it in 2 steps.</p><p>Step 1: Let’s run all components locally first with Docker Compose.</p><p>Step 2: We will run the Snowflake OTEL Receiver in Snowflake Container Services while only the OpenTelemetry Collector and the Flask app will stay running outside of Snowflake.</p><p>In a next blog post I will also show how you can have all containers running in Snowflake container services.</p><p>So for Step 1:</p><p>1a) Clone this Git Repo: <a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver">https://github.com/KellerKev/snowflake-opentelemetry-receiver</a></p><p>1b) Copy and paste the contents of <strong>otel_spcs_prep.sql</strong> into your Snowflake SQL workbook.</p><p>1c) Work through the workbook until the comment “STOP Here to prepare for Step 1)”.</p><p>Make sure you replace the user with your username for this line:</p><pre>grant role oteltest to user xxx;</pre><p>Here are the full statements we should do for now as well if you want to quickly copy and paste from here:</p><pre>use role accountadmin;<br><br>CREATE OR REPLACE WAREHOUSE otelwh WITH WAREHOUSE_SIZE=&#39;X-SMALL&#39;;<br><br>create role oteltest;<br><br>grant usage on WAREHOUSE otelwh  to role oteltest;<br>grant operate on  WAREHOUSE otelwh to role oteltest;<br><br>grant create database on account to role oteltest;<br><br><br>grant role oteltest to user xxx;<br><br>use role oteltest;<br><br><br><br>create database otel;<br>create schema otelschema;<br><br>CREATE or replace TABLE metrics (<br>      timestamp TIMESTAMP_NTZ,<br>      metric_name STRING,<br>      value DOUBLE,<br>      attributes VARCHAR<br>);<br><br>CREATE or replace TABLE logs (<br>      timestamp TIMESTAMP_NTZ,<br>      log_level STRING,<br>      message STRING,<br>      attributes varchar<br>);<br><br><br>CREATE or replace TABLE  traces (<br>      trace_id STRING,<br>      span_id STRING,<br>      name STRING,<br>      start_time TIMESTAMP_NTZ,<br>      end_time TIMESTAMP_NTZ,<br>      attributes varchar<br>);<br><br>select * from metrics;<br>select * from logs;<br>select * from traces;</pre><p>1d) Now that you have the warehouse, the role, the database, schema and tables setup in Snowflake you can spin up the dockercompose.Yaml with Docker Compose which will run the Flask sample app, the Opentelemtry Collector as will as the Snowflake Opentelemetry Receiver all locally, but connect to your Snowflake account via the Python driver.</p><p>In order to do that we need to supply the docker-compose.yaml files with your credential first:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EWeiZ3ixPoCIK1gkZ5tHFg.png" /></figure><p>Make sure to put your right credentials here for account, user, password.</p><p>The rest should work as is.</p><p>If you run Docker Compose on Windows make sure remove the Linux/Mac path line and adapt the one that is prepared for Windows and point it to the OTEL config file that is required that you got from the Git Repository.</p><p>1e) Now in the Terminal in the folder of the cloned Git Repository on your PC/Mac or Linux workstation/server enter the command:</p><pre>docker compose up</pre><p>In the following demo video is what you should be able to see now.</p><p>The 3 containers start up, with the Opentelemetry Snowflake Receiver connecting to your Snowflake instance.</p><p>Navigate with your browser to the Flask endpoints of the Flask sample app that sends traces, logs and metrics:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dNQf59-AlO1Z_6NIzX5uWg.gif" /></figure><p><a href="http://localhost:5000/greet/kevin">http://localhost:5000/greet/kevin</a></p><p>You can change the name “kevin” to your name or the name of your pet or anything you like.</p><p>And also go to:</p><p><a href="http://localhost:5000/hello">http://localhost:5000/hello</a></p><p>In your Snowflake account you should now be able to see that the tables logs, traces and metrics have been populated with data.</p><p>We are now ready for Step 2) to let the Snowflake Opentelemetry Receiver run in Snowflake container services.</p><p>For that we need to upload the Snowflake Opentelemetry Receiver Docker image into your Snowflake accounts image registry.</p><p>In order to do that continue with the steps in the <strong>otel_spcs_prep.sql</strong> file</p><p>2a) Let me put the relavent steps for right now here as well if you quickly want to copy and paste from here:</p><pre><br>CREATE IMAGE REPOSITORY IF NOT EXISTS oteltestimages;<br><br>SHOW IMAGE REPOSITORIES IN SCHEMA;<br><br>--before uploading image, if you use a username/password to authenticate and MFA is enabled for your user, you better disable it<br>-- before uploading for a bit so you dont get locked out and receive 10000 MFA messages:<br></pre><p>With this we have an an image repository.</p><p>2b) When you execute the command:</p><pre>SHOW IMAGE REPOSITORIES IN SCHEMA;</pre><p>You should be able to see how you need to tag (rename) the Docker image so we can upload it.</p><p>It should look something like this:</p><pre>xxxx.registry.snowflakecomputing.com/otel/otelschema/oteltestimages</pre><p>2c) Take the value from the repository_url column and copy it.</p><p>Now in your terminal on your local workstatation or server make sure you have the image of the Snowflake Opentelemetry Receiver available.</p><p>You should have it already if you have followed Step 1).</p><p>If not you can either build the image from the Dockerfile provided in the Github repository or pull it from my Dockerhub repository via:</p><pre>docker pull kbkeller/snowflakeotelreceiver:latest</pre><p>2d) Now lets give this image a tag suitable to upload into the Snowflake image registry:</p><pre>docker tag kbkeller/snowflakeotelreceiver:latest xxxx.registry.snowflakecomputing.com/otel/otelschema/oteltestimages/otel-image:latest</pre><p>Afterwards we can authenticate and upload.</p><p>Before you do the Docker push a word of <strong>warning:</strong></p><p>Do NOT push yet, read this:</p><p>If you have MFA for the user enabled and you are using a password to auth then disable MFA RIGHT NOW. Just for a couple of minutes.</p><p>Otherwise you will get a ton of MFA prompts on your phone and you will be locked out of your Snowflake account for 15 minutes and that is all you will achieve.</p><p>So to avoid this, go back to your Snowflake account and disable MFA for 10 minutes or so for your user:</p><pre>USE ROLE ACCOUNTADMIN;<br>alter user xxxx set mins_to_bypass_mfa=10</pre><p>2e) Ok back in our terminal we can login and push the image now like so</p><p>(first login, then push)</p><pre>docker login xxxx.registry.snowflakecomputing.com<br><br>docker push xxxx.registry.snowflakecomputing.com/otel/otelschema/oteltestimages/otel-image:latest</pre><p>You will be asked to login with username and password and then you should be able to upload the image to the Snowflake image registry.</p><p>2f) Once the upload is done, let’s create a service / container that we run based on on the image in your Snowflake account.</p><p>Here are the following useful commands to execute now.</p><p>We will create a compute pool, give access to the compute pool to our custom role and then run the container in that compute pool.</p><p>We can also check the logs and see the status of everything:</p><pre><br>USE ROLE ACCOUNTADMIN;<br><br>GRANT BIND SERVICE ENDPOINT ON ACCOUNT TO ROLE oteltest;<br><br>CREATE  COMPUTE POOL IF NOT EXISTS OTEL_COMPUTE_POOL<br>  MIN_NODES = 1<br>  MAX_NODES = 1<br>  INSTANCE_FAMILY = CPU_X64_S<br>  AUTO_RESUME = true;<br><br>  DESCRIBE COMPUTE POOL OTEL_COMPUTE_POOL;<br>  <br>grant usage on compute pool OTEL_COMPUTE_POOL to role oteltest;<br>grant operate on compute pool OTEL_COMPUTE_POOL to role oteltest;<br>grant monitor on compute pool OTEL_COMPUTE_POOL to role oteltest;<br>grant modify on compute pool OTEL_COMPUTE_POOL to role oteltest;<br><br><br>use role oteltest;<br><br><br> CREATE SERVICE otel_service<br>  IN COMPUTE POOL OTEL_COMPUTE_POOL<br>  MIN_INSTANCES=1<br>  MAX_INSTANCES=1<br>  FROM SPECIFICATION<br>  $$<br>   spec:<br>     containers:<br>     - name: &quot;otel&quot;<br>       image: &quot;/otel/otelschema/oteltestimages/otel-image:latest&quot;<br>       env:<br>         SNOWFLAKE_DATABASE: &quot;otel&quot;<br>         SNOWFLAKE_WAREHOUSE: &quot;otelwh&quot;<br>         SNOWFLAKE_SCHEMA: &quot;otelschema&quot;<br>         SPCS: &quot;True&quot;<br>          <br>     endpoints:<br>     - name: otelhttp<br>       port: 4318<br>       public: true<br>  $$;<br><br>  <br>describe service otel_service;<br>SHOW SERVICE CONTAINERS IN SERVICE otel_service;<br>SHOW IMAGES IN IMAGE REPOSITORY oteltestimages;<br><br><br>CALL SYSTEM$GET_SERVICE_LOGS(&#39;otel_service&#39;, &#39;0&#39;, &#39;otel&#39;, 1000);<br>SHOW ENDPOINTS IN SERVICE otel_service;</pre><p>2g) After the upload you should the see the image in the repository in Snowflake with the “SHOW IMAGES..” SQL command:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Y4AdfHzkhWQlOFN0-RKRg.png" /></figure><p>2h) So the container should also run now, based on you having executed the SQL commands above in the right order. The relevant command is:</p><pre>CREATE SERVICE otel_service<br>  IN COMPUTE POOL OTEL_COMPUTE_POOL<br>  MIN_INSTANCES=1<br>  MAX_INSTANCES=1<br>  FROM SPECIFICATION<br>  $$<br>   spec:<br>     containers:<br>     - name: &quot;otel&quot;<br>       image: &quot;/otel/otelschema/oteltestimages/otel-image:latest&quot;<br>       env:<br>         SNOWFLAKE_DATABASE: &quot;otel&quot;<br>         SNOWFLAKE_WAREHOUSE: &quot;otelwh&quot;<br>         SNOWFLAKE_SCHEMA: &quot;otelschema&quot;<br>         SPCS: &quot;True&quot;<br>          <br>     endpoints:<br>     - name: otelhttp<br>       port: 4318<br>       public: true<br>  $$;</pre><p>2i)</p><p>You may have to be patient as compute pools need some time to start up:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XllrzLmlkk13MXeqHKQcHw.png" /></figure><p>Consquently the container may also still be in pending status if the pool is not started yet:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/923/1*X7zJnGvs8yuca1wemdmwKg.png" /></figure><p>You can check the status with:</p><pre>describe service otel_service;<br>DESCRIBE COMPUTE POOL OTEL_COMPUTE_POOL;<br></pre><p>2j) Once everything is in running and active state you can check the logs of the Snowflake Opentelemetry receiver:</p><pre>CALL SYSTEM$GET_SERVICE_LOGS(&#39;otel_service&#39;, &#39;0&#39;, &#39;otel&#39;, 1000);</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QEheQVoDMb9SPyhVAioqnQ.png" /></figure><p>2k) In order to send the logs to the receiver now, we need 2 things:</p><p>One is the endpoint url of the receiver and the other is the OAuth token in order to be able to authenticate programmatically to the running container in Snowflake.</p><p>Getting the URL is easy:</p><pre>SHOW ENDPOINTS IN SERVICE otel_service;</pre><p>That should show you something like this:</p><pre>agtubd-xxxx-up50179.snowflakecomputing.app</pre><p>Which means the full url that we need for the Opentelemetry Collector running on our server or workstation in the last step is something like;</p><pre>https://agtubd-xxxx-up50179.snowflakecomputing.app</pre><p>2l) So lets prepare the docker-compose.yaml file and the Opentelemetry Collector config to communicate with the receiver in Snowflake.</p><p>Go ahead and open the docker-compose.yaml file in your favourite editor and lets remove the the entry to start the receiver locally:</p><p>Just comment out the lines that would start it with the hash in front of each line:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mkRCvVIcoGZ9hTFN9FIsMQ.png" /></figure><p>Next we need to point the Opentelemetry collector to the Snowflake container URL:</p><p>In the collector-config.yaml replace the entry point to <a href="http://otel:4318">http://otel:4318</a> with the endpoint you have extracted from Snowflake in the previous step. Also eanble TLS.</p><p>So instead of this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/566/1*GdUskK0qUA5ClE3gizcgJQ.png" /></figure><p>We should now have this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/609/1*_Srj593CHvXKf2ErGEHLqw.png" /></figure><p>Where tls: insecure is FALSE and the endpoint is the HTTPS endpoint of the Snowflake container.</p><p>The last step in this file is to uncommend the headers section and add the right Snowflake OAuth token.</p><p>For that you need to generate a RSA keypair for the Snowflake user, assign the the public key to the user in Snowflake and then use the Python script that is part of the repository that is called “<a href="https://github.com/KellerKev/snowflake-opentelemetry-receiver/blob/main/snowflake_otel_receiver/access-via-keypair.py">access-via-keypair.py</a>”.</p><p>The detailed instruction can be found <a href="https://docs.snowflake.com/en/developer-guide/snowpark-container-services/tutorials/tutorial-1#optional-access-the-public-endpoint-programmatically">here</a>.</p><p>Make SURE to write the role of your user in UPPERCASE letters.</p><p>Once you have the token add it to the Opentelemetry Collector config like so:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/1*00lsTGViAvGeRmcUQomiow.png" /></figure><p>Replace the XXXX with your token, or better yet, put in a environment variable that can be updated. For the first test let’s just put in the token.</p><p>2m) Finally you should be able to run Docker compose again with</p><pre>docker compose up</pre><p>The Opentelemetry Collector is now collecting the logs, traces and metrics from the Flask sample app (remember to visit the localhost:5000/hello etc. to generate data) and is sending it straight to the Snowflake Opentelemetry Receiver running in Snowflake.</p><p>2n) Let’s transform the tables in the Elastic Common Schema into target tables via a Stored Procedure and Streams and Tasks.</p><pre>-- Create Streams on the source tables<br>CREATE OR REPLACE STREAM logs_stream ON TABLE logs APPEND_ONLY = TRUE;<br>CREATE OR REPLACE STREAM metrics_stream ON TABLE metrics APPEND_ONLY = TRUE;<br>CREATE OR REPLACE STREAM traces_stream ON TABLE traces APPEND_ONLY = TRUE;</pre><p>Create the stored procedure to transform the tables steams:</p><pre>CREATE OR REPLACE PROCEDURE ecs_transform_incremental()<br>RETURNS STRING<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = &#39;3.11&#39;<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;)<br>HANDLER = &#39;main&#39;<br>AS<br>$$<br>def main(session):<br>    from snowflake.snowpark.functions import col, parse_json, datediff, lit<br><br>    try:<br>        # Create the target schema if it doesn&#39;t exist<br>        session.sql(&quot;CREATE SCHEMA IF NOT EXISTS ecs_schema&quot;).collect()<br><br>        # ***********************<br>        # Process Logs<br>        # ***********************<br><br>        # Create the target &#39;logs&#39; table if it doesn&#39;t exist<br>        session.sql(&quot;&quot;&quot;<br>            CREATE TABLE IF NOT EXISTS ecs_schema.logs (<br>                &quot;@timestamp&quot; TIMESTAMP_NTZ,<br>                &quot;message&quot; STRING,<br>                &quot;log.level&quot; STRING,<br>                &quot;attributes&quot; VARIANT<br>            )<br>        &quot;&quot;&quot;).collect()<br><br>        # Read new data from the logs_stream<br>        logs_df = session.table(&quot;logs_stream&quot;)<br><br>        # Check if there is new data to process<br>        if logs_df.count() &gt; 0:<br>            # Transform and insert data into ecs_schema.logs<br>            logs_transformed = logs_df.select(<br>                col(&quot;timestamp&quot;).alias(&quot;@timestamp&quot;),<br>                col(&quot;message&quot;),<br>                col(&quot;log_level&quot;).alias(&quot;log.level&quot;),<br>                parse_json(col(&quot;attributes&quot;)).alias(&quot;attributes&quot;)<br>            )<br><br>            logs_transformed.write.mode(&quot;append&quot;).save_as_table(&quot;ecs_schema.logs&quot;)<br>        else:<br>            print(&quot;No new logs data to process.&quot;)<br><br>        # ***********************<br>        # Process Metrics<br>        # ***********************<br><br>        # Create the target &#39;metrics&#39; table if it doesn&#39;t exist<br>        session.sql(&quot;&quot;&quot;<br>            CREATE TABLE IF NOT EXISTS ecs_schema.metrics (<br>                &quot;@timestamp&quot; TIMESTAMP_NTZ,<br>                &quot;metricset.name&quot; STRING,<br>                &quot;metric.value&quot; FLOAT,<br>                &quot;attributes&quot; VARIANT<br>            )<br>        &quot;&quot;&quot;).collect()<br><br>        # Read new data from the metrics_stream<br>        metrics_df = session.table(&quot;metrics_stream&quot;)<br><br>        # Check if there is new data to process<br>        if metrics_df.count() &gt; 0:<br>            # Transform and insert data into ecs_schema.metrics<br>            metrics_transformed = metrics_df.select(<br>                col(&quot;timestamp&quot;).alias(&quot;@timestamp&quot;),<br>                col(&quot;metric_name&quot;).alias(&quot;metricset.name&quot;),<br>                col(&quot;value&quot;).alias(&quot;metric.value&quot;),<br>                parse_json(col(&quot;attributes&quot;)).alias(&quot;attributes&quot;)<br>            )<br><br>            metrics_transformed.write.mode(&quot;append&quot;).save_as_table(&quot;ecs_schema.metrics&quot;)<br>        else:<br>            print(&quot;No new metrics data to process.&quot;)<br><br>        # ***********************<br>        # Process Traces<br>        # ***********************<br><br>        # Create the target &#39;traces&#39; table if it doesn&#39;t exist<br>        session.sql(&quot;&quot;&quot;<br>            CREATE TABLE IF NOT EXISTS ecs_schema.traces (<br>                &quot;trace.id&quot; STRING,<br>                &quot;span.id&quot; STRING,<br>                &quot;span.name&quot; STRING,<br>                &quot;span.start&quot; TIMESTAMP_NTZ,<br>                &quot;span.end&quot; TIMESTAMP_NTZ,<br>                &quot;span.duration&quot; FLOAT,<br>                &quot;attributes&quot; VARIANT<br>            )<br>        &quot;&quot;&quot;).collect()<br><br>        # Read new data from the traces_stream<br>        traces_df = session.table(&quot;traces_stream&quot;)<br><br>        # Check if there is new data to process<br>        if traces_df.count() &gt; 0:<br>            # Calculate span duration<br>            traces_transformed = traces_df.with_column(<br>                &quot;span.duration&quot;,<br>                datediff(&quot;milliseconds&quot;, col(&quot;start_time&quot;), col(&quot;end_time&quot;))<br>            ).select(<br>                col(&quot;trace_id&quot;).alias(&quot;trace.id&quot;),<br>                col(&quot;span_id&quot;).alias(&quot;span.id&quot;),<br>                col(&quot;name&quot;).alias(&quot;span.name&quot;),<br>                col(&quot;start_time&quot;).alias(&quot;span.start&quot;),<br>                col(&quot;end_time&quot;).alias(&quot;span.end&quot;),<br>                col(&quot;span.duration&quot;),<br>                parse_json(col(&quot;attributes&quot;)).alias(&quot;attributes&quot;)<br>            )<br><br>            traces_transformed.write.mode(&quot;append&quot;).save_as_table(&quot;ecs_schema.traces&quot;)<br>        else:<br>            print(&quot;No new traces data to process.&quot;)<br><br>        return &#39;Incremental transformation completed successfully.&#39;<br>    except Exception as e:<br>        return &#39;Error: &#39; + str(e)<br>$$;</pre><p>Lets create the task:</p><pre>CREATE OR REPLACE TASK ecs_transform_task<br>WAREHOUSE = your_warehouse_name  -- Replace with your warehouse name<br>SCHEDULE = &#39;1 MINUTE&#39;<br>AS<br>CALL ecs_transform_incremental();</pre><p>And enable it:</p><pre>-- Enable the Task<br><br>use role accountadmin;<br>grant EXECUTE TASK on account to role oteltest;<br>use role oteltest;<br>ALTER TASK ecs_transform_task RESUME;<br></pre><p>You can monitor tasks as ACCOUNTADMIN like this:</p><pre><br>show tasks;<br>show alerts;<br><br>SELECT * from LOGIN_EVENTS;<br><br>-- monitor alerts &amp; tasks<br><br>SELECT *<br>FROM<br>  TABLE(INFORMATION_SCHEMA.ALERT_HISTORY(<br>    SCHEDULED_TIME_RANGE_START<br>      =&gt;dateadd(&#39;hour&#39;,-1,current_timestamp())))<br>ORDER BY SCHEDULED_TIME DESC;<br><br>SELECT *<br>FROM<br>  TABLE(INFORMATION_SCHEMA.TASK_HISTORY(<br>    SCHEDULED_TIME_RANGE_START<br>      =&gt;dateadd(&#39;hour&#39;,-1,current_timestamp())))<br>ORDER BY SCHEDULED_TIME DESC;</pre><p>Now that the task is running you sould see a new schema in the database called “ECS_SCHEMA”:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/264/1*TYIcqnOWiVXbqtlUBhgNrQ.png" /></figure><p>Where you can now find the logs, metrics and traces table schemas modfied to comply to ECS:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q8XqooJuiYBIP_6ukjQEcw.png" /></figure><p>These target tables could be modified to be Iceberg tables as promised so that you can get started with having all the data in an open security and observability data lake.</p><p>Just create the table as type Iceberg with Snowflake providing the catalog and setup the external stage (s3, blob or gcp).</p><p>Here is how you can do it quickly, if you have an external stage ready:</p><p>Stop the task for a bit:</p><pre>use role oteltest;<br>ALTER TASK ecs_transform_task suspend;</pre><p>Then navigate to the ECS_SCHEMA and lets drop the tables we have there for now:</p><pre>drop otel.ecs_schema.logs;<br>drop otel.ecs_schema.metrics;<br>drop otel.ecs_schema.traces;</pre><p>Now in the schema, lets re-create those tables as Iceberg tables (for my_ext_vol and ‘my/relative/path/from/extvol’ use the values appropiate for your setup)</p><pre>create or replace ICEBERG TABLE OTEL.ECS_SCHEMA.LOGS (<br> &quot;@timestamp&quot; TIMESTAMP_NTZ(9),<br> &quot;message&quot; VARCHAR(16777216),<br> &quot;log.level&quot; VARCHAR(16777216),<br> &quot;attributes&quot; VARIANT<br>)<br>CATALOG = &#39;SNOWFLAKE&#39;<br>  EXTERNAL_VOLUME = &#39;my_ext_vol&#39;<br>  BASE_LOCATION = &#39;my/relative/path/from/extvol&#39;;<br><br>create or replace ICEBERG TABLE OTEL.ECS_SCHEMA.METRICS (<br> &quot;@timestamp&quot; TIMESTAMP_NTZ(9),<br> &quot;metricset.name&quot; VARCHAR(16777216),<br> &quot;metric.value&quot; FLOAT,<br> &quot;attributes&quot; VARIANT<br>)<br>CATALOG = &#39;SNOWFLAKE&#39;<br>  EXTERNAL_VOLUME = &#39;my_ext_vol&#39;<br>  BASE_LOCATION = &#39;my/relative/path/from/extvol&#39;;<br><br>create or replace ICEBERG TABLE OTEL.ECS_SCHEMA.TRACES (<br> &quot;trace.id&quot; VARCHAR(16777216),<br> &quot;span.id&quot; VARCHAR(16777216),<br> &quot;span.name&quot; VARCHAR(16777216),<br> &quot;span.start&quot; TIMESTAMP_NTZ(9),<br> &quot;span.end&quot; TIMESTAMP_NTZ(9),<br> &quot;span.duration&quot; FLOAT,<br> &quot;attributes&quot; VARIANT<br>)<br>CATALOG = &#39;SNOWFLAKE&#39;<br>  EXTERNAL_VOLUME = &#39;my_ext_vol&#39;<br>  BASE_LOCATION = &#39;my/relative/path/from/extvol&#39;;<br><br>ALTER TASK ecs_transform_task resume;</pre><p><strong>Next steps:</strong></p><p>In the next posts we will move all container components of this demo setup into Snowflake and also how we can use Snowflake notebooks for data crunching and threat hunting.</p><p>We will also introduce batching on the Opentelemetry Collector in order to send data more efficiently to Snowflake.</p><p>We will also add the ingestion of <a href="https://vector.dev/">VECTOR</a> and <a href="https://www.fluentd.org/">Fluentd</a> data in a similiar manner.</p><p>Next up →</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kE65RHS1NzNy4yAGiiUOJw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/0*aZPWxdo9xZzcPFvF.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b562680bd1d6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to let Snowflake raise security incidents in your SIEM or XDR automatically]]></title>
            <link>https://medium.com/@kellerkev/how-to-let-snowflake-raise-security-incidents-in-your-siem-or-xdr-automatically-fb2ee4bea27b?source=rss-51541d7e9f0------2</link>
            <guid isPermaLink="false">https://medium.com/p/fb2ee4bea27b</guid>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[incident-management]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[siem]]></category>
            <category><![CDATA[incident-response]]></category>
            <dc:creator><![CDATA[Kevin Keller]]></dc:creator>
            <pubDate>Thu, 25 Jul 2024 13:44:00 GMT</pubDate>
            <atom:updated>2024-08-21T08:50:13.604Z</atom:updated>
            <content:encoded><![CDATA[<p>Snowflake can alert your SIEM or XDR or any downstream systems automatically of any suspicious events in a timely manner.</p><p>The benefit is that you can react to security related incidents in an automated and fast way without needing to export any logs from Snowflake or run, host and maintain any additional code outside of Snowflake.</p><p>Here is just a quick guide that shall serve as a sample how you can setup Snowflake to send an API request to your SIEM system’s API such as Splunk, Sentinel etc. in order to raise an incident in a timely manner in an automated way.</p><p>While it is possible to have Snowflake raise incidents on any available data apart from Snowflakes audit logs (meaning you can also raise alerts in audit logs ingested into Snowflake from 3rd party applications), this example focuses on raising an incident when a user has tried to login into Snowflake using the wrong user password.</p><p>This use case is particularily interesting for safeguarding service accounts that are forced to use a password and where OAuth or Keypair authentication is not possible.</p><p>A single failed login attempt for these users should already raise an alarm.</p><p>This is especially interesting for those service users that connect from an ever changing public IP address space to Snowflake such as PowerBI Cloud Service, where setting Network Policies in Snowflake can become challenging if you are not willing to use the PowerBI Gateway for those fully automated report creation use cases.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mBhbAHFrcWFszh2V6lsCrg.png" /></figure><p>The process works by running a Snowflake Alert Task every minute to check for any failed login attempts using password authenticaton.</p><p>If there are any new failed password authentication attempts then the Snowflake Alert Task will create a record of that in the LOGIN_EVENTS table.</p><p>The Alert Task will check within a time range of the last 5 minutes in the Snowflake authentication logs from the point in time when the task is running.</p><p>So there is a chance that the same event will be detected multiple times if you run the Alert Task every minute, but that doesn’t matter.</p><p>The reason it does not matter is that we will only record any events that we have not logged yet previously into the LOGIN_EVENTS table given that each failed login attempt in Snowflake has a unique event ID.</p><p>Once the event is recorded another Snowflake Task, which also runs every minute will check, if there are any new failed login attempt records in the LOGIN_EVENTS table.</p><p>If so we take the information of the record(s) and send it/them via External Access function UDF (just an API call) to your SIEM or XDR system in order to raise an incident.</p><p>Finally the Task will also update the column as being “SENT=True” with a timestamp and also create a record of the SIEM’s API response.</p><p>Next time the Task will check the records, it will see that all records have already been sent and that no further action is required.</p><p>Meanwhile if an incident was raised in the SIEM by Snowflake, the SIEM system can trigger the incident response process to react to the incident as you see fit.</p><p>You can have your SIEM system connect to Snowflake and disable the user and load additional log records into the SIEM relevant for this incident.</p><p>So how can we implement this?</p><p>Now is the time to login to your Snowflake account and create a new SQL worksheet.</p><p>You will need the ACCOUNTADMIN role temporarily.</p><p>Let’s create a Snowflake Alert Task to run every minute and check if there were any failed password login attempts in the last 5 minutes and if so, record that event into the LOGIN_EVENTS table.</p><p>In order for our user to be able to do so, you will need the MONITOR privilege on every user (or just the users you want to monitor).</p><p>You can follow this guide along with the highly privileged role ACCOUNTADMIN to get through it quickly all the way, but I would highly recommend to create a bespoke role right now INSTEAD of using a highly privileged user permanently.</p><ol><li>So best you switch to ACCOUNTADMIN temporarily and do the following (replace XXX with YOUR username):</li></ol><pre><br><br>use role accountadmin;<br>CREATE OR REPLACE ROLE SIEM_MONITOR;<br>grant role siem_monitor to user XXXX;<br>GRANT CREATE WAREHOUSE ON ACCOUNT TO ROLE siem_monitor;<br>GRANT MANAGE WAREHOUSES ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE DATABASE ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE ROLE ON ACCOUNT TO siem_monitor;<br>GRANT EXECUTE TASK ON ACCOUNT TO ROLE siem_monitor;<br>GRANT EXECUTE ALERT ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE INTEGRATION ON ACCOUNT TO ROLE  siem_monitor;<br><br><br>use role siem_monitor;<br><br>CREATE OR REPLACE WAREHOUSE siem_alert_demo WITH WAREHOUSE_SIZE=&#39;X-SMALL&#39;;<br>grant usage on warehouse siem_alert_demo to role SIEM_MONITOR;<br><br>CREATE OR REPLACE DATABASE SIEM_ALERT_DEMO_DB;<br>CREATE OR REPLACE SCHEMA SIEM_ALERT_DEMO_SCHEMA;<br><br>grant usage on database SIEM_ALERT_DEMO_DB to role accountadmin;<br>grant usage on schema SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA to role accountadmin;<br>grant usage on warehouse siem_alert_demo to role accountadmin;<br><br>CREATE OR REPLACE ROLE MONITOR_USERS;<br>grant role monitor_users to role siem_monitor;<br><br>CREATE OR REPLACE PROCEDURE set_monitor_privileges_on_all_users(users varchar)<br>RETURNS varchar<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = &#39;3.8&#39;<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;)<br>HANDLER = &#39;run&#39;<br>EXECUTE AS CALLER<br>AS<br>$$<br>from snowflake.snowpark import Session<br><br>def run(session, users):<br>      users=users.replace(&#39;[&#39;,&#39;&#39;)<br>      users=users.replace(&#39;]&#39;,&#39;&#39;)<br>      users=users.split(&#39;,&#39;)<br>      for user in users:<br>              sql = &quot;&quot;&quot;GRANT MONITOR ON USER &quot;&quot;&quot;+user+&quot;&quot;&quot; TO ROLE monitor_users&quot;&quot;&quot;             <br>              result=session.sql(sql).collect()[0][0]<br><br>      return &quot;done&quot;<br>  <br>  <br>$$;<br><br>grant usage on procedure set_monitor_privileges_on_all_users(varchar) to role accountadmin;<br>use role accountadmin;<br>GRANT CREATE NETWORK RULE ON SCHEMA SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA TO ROLE siem_monitor;<br>call set_monitor_privileges_on_all_users(select all_user_names());<br><br>use role siem_monitor;<br>revoke usage on procedure set_monitor_privileges_on_all_users(varchar) from role accountadmin;<br>revoke usage on warehouse siem_alert_demo from role accountadmin;<br>revoke usage on database SIEM_ALERT_DEMO_DB from role accountadmin;<br>revoke usage on schema SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA from role accountadmin;</pre><p>We assign the role MONITOR_USERS which now has the privileges to monitor all users to the regular user role SIEM_MONITOR that you are currently using to create this demo with.</p><p>2. Now we continue so let’s setup our login event table , a Snowflake Alert Task and a View that makes it easier to query login events:</p><pre><br>use role siem_monitor;<br><br>Lets the create the table that will hold the information of all the suspicious / falied logins of users that have entered the wrong password.<br>create or replace TABLE LOGIN_EVENTS (<br> EVENT_TIMESTAMP TIMESTAMP_LTZ(3),<br> EVENT_ID NUMBER(38,0),<br> EVENT_TYPE VARCHAR(16777216),<br> USER_NAME VARCHAR(16777216),<br> CLIENT_IP VARCHAR(16777216),<br> REPORTED_CLIENT_TYPE VARCHAR(16777216),<br> REPORTED_CLIENT_VERSION VARCHAR(16777216),<br> FIRST_AUTHENTICATION_FACTOR VARCHAR(16777216),<br> SECOND_AUTHENTICATION_FACTOR VARCHAR(16777216),<br> IS_SUCCESS VARCHAR(3),<br> ERROR_CODE NUMBER(38,0),<br> ERROR_MESSAGE VARCHAR(16777216),<br> RELATED_EVENT_ID NUMBER(38,0),<br> CONNECTION VARCHAR(16777216),<br> SENT BOOLEAN DEFAULT FALSE,<br> SENT_TIME TIMESTAMP_NTZ(9),<br> SIEM_RESPONSE VARCHAR(16777216)<br>);<br><br>create or replace view logins as select * from table(information_schema.login_history(TIME_RANGE_START =&gt; dateadd(&#39;minutes&#39;,-5,current_timestamp()),current_timestamp())) where IS_SUCCESS=&#39;NO&#39; and FIRST_AUTHENTICATION_FACTOR=&#39;PASSWORD&#39; order by event_timestamp;<br><br>CREATE OR REPLACE ALERT strange_login<br>  WAREHOUSE = siem_alert_demo<br>  SCHEDULE = &#39;1 minute&#39;<br>  IF( EXISTS(<br>      select event_id from logins<br>))<br>  THEN<br><br>INSERT INTO login_events  <br>SELECT *,false, NULL,NULL<br>FROM logins AS src<br>WHERE NOT EXISTS (SELECT event_id<br>                  FROM  login_events AS tgt<br>                  WHERE tgt.event_id = src.event_id);<br><br>    <br>ALTER ALERT STRANGE_LOGIN RESUME;</pre><p>This will record any failed login attempts into the table LOGIN_EVENTS, but only if the EVENT_ID is not already in the table, so we dont’t have any duplicates.</p><p>3. We have to make sure that any event that has not been sent over to the SIEM system can now be sent.</p><p>3a)</p><p>In order to get that done we need to create the external access function to make the API call to a SIEM.</p><p>In this demo I am using a public MOCK API service that just returns an echo of whatever we are sending.</p><p>In real life you will have to setup an external access function to your SIEM system.</p><p>This will not only mean that you need to change the API URL and add things like a BEARER TOKEN, but it will also mean that you will have to adapt and change the JSON payload that needs to be sent.</p><p>Currently this payload is sent over a public IP adress space via the internet or Cloud Service Provider backbone.</p><p>This tutorial can also be adapted to use classic Snowflake external functions (instead of external access functions) which can also connect via Privatelink (on AWS today and soon on Azure).</p><p>External access function will be able to sent payload over Privatelink soon as well.</p><p>Another way is of course to create a proxy API in a DMZ until private connectivity is available, if your SIEM is not hosted in the public Cloud.</p><p>For now in this demo we keep it simple and just send the whole LOGIN_EVENT table info to the MOCK API.</p><pre><br>CREATE OR REPLACE NETWORK RULE siem_alert_network_rule<br>  MODE = EGRESS<br>  TYPE = HOST_PORT<br>  VALUE_LIST = (&#39;echo.free.beeceptor.com:443&#39;);<br><br><br>CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION siem_apis_access_integration<br>  ALLOWED_NETWORK_RULES = (siem_alert_network_rule)<br>  ENABLED = TRUE;<br><br><br><br>CREATE OR REPLACE FUNCTION alert_siem_login(incident variant, url string)<br>RETURNS variant<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = 3.8<br>HANDLER = &#39;incident_alert&#39;<br>EXTERNAL_ACCESS_INTEGRATIONS = (siem_apis_access_integration)<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;,&#39;requests&#39;)<br><br>AS<br>$$<br>import _snowflake<br>import requests<br>import json<br>session = requests.Session()<br><br>def incident_alert(incident, url):<br><br>    if incident != &#39;&#39;:<br>        payload = incident<br>    else:<br>        payload={}<br>    headers = {}<br><br>    response = requests.request(&quot;POST&quot;, url, headers=headers, data=payload)<br><br>    return response.text<br>$$;</pre><p>3b) Now that we are able to send JSON payload to an API we can create the Stored Procedure that goes through the LOGIN_EVENTS table and sents any new events to the SIEM:</p><pre><br>CREATE OR REPLACE PROCEDURE sent_suspicious_logins()<br>RETURNS STRING<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = &#39;3.8&#39;<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;)<br>HANDLER = &#39;run&#39;<br>EXECUTE AS CALLER<br>AS<br>$$<br>from snowflake.snowpark import Session<br>import json<br><br>to_sent={}<br><br>def run(session):<br>  sql1 = &quot;&quot;&quot;select to_variant(<br>SELECT<br>  ARRAY_AGG(json) AS aggregated_results<br>FROM<br>  (<br>    SELECT<br>      OBJECT_CONSTRUCT(*) AS json<br>    FROM<br>      (select * from login_events where sent=false)<br>  )<br>    )&quot;&quot;&quot;       <br><br>  to_sent=json.loads(session.sql(sql1).collect()[0][0])<br><br>  for siem_payload in to_sent:<br>    sql_siem = &quot;&quot;&quot;UPDATE<br>                    login_events<br>                    SET<br>                        sent = TRUE,<br>                        sent_time = CURRENT_TIMESTAMP(),<br>                        siem_response= (select alert_siem_login(&#39;&quot;&quot;&quot; +json.dumps(siem_payload).replace(&#39;\&quot;&#39;,&#39;&quot;&#39;)+                                                       &quot;&quot;&quot;&#39;,&#39;https://echo.free.beeceptor.com:443/siem_api/incident&#39;))<br>                    WHERE<br>                        sent = FALSE&quot;&quot;&quot;<br>    sql_siem_result=session.sql(sql_siem).collect()[0][0]<br><br>  <br><br>  return &quot;payload sent&quot;<br>  <br>  <br>$$;</pre><p>3c) With that all that is left to do is call the Stored Procedure every minute to make sure it regulary checks and sends new events to the SIEM automatically.</p><pre>CREATE OR REPLACE task sent_strange_logins_to_siem<br>  WAREHOUSE = siem_alert_demo<br>  SCHEDULE = &#39;1 minute&#39;<br>  AS <br>call sent_suspicious_logins();<br><br><br><br>alter task sent_strange_logins_to_siem resume;</pre><p>4) In order to see if everything is working it’s good to test with a user to try to login to Snowflake using the wrong password once or twice.</p><p>After that keep running a SELECT query on the LOGIN_EVENTS table and you will see how it’s being populated with the login events and also with the SIEM_RESPONSE payload so you can see that the event was successfully sent.</p><pre>SELECT * from LOGIN_EVENTS;</pre><p>You should see this at the beginning of the table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ijhUAGN1ovzRr3oxpYqt1g.png" /></figure><p>And and the end of the table you can see if the column SENT is still set to FALSE and the if other columns like SENT_TIMESTAMP and SIEM_RESPONSE are still NULL.</p><p>In that case the event was recorded in the table, but not sent to the SIEM yet.</p><p>Once the column SENT is set to TRUE and the SENT_TIMESTAMP and SIEM_RESPONSE columns have values, you can be sure that your SIEM was notified and an incident raised:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6h2bm3OaqU93AJaXytapIA.png" /></figure><p>Here are some sample APIs from MS Sentinel that can be used:</p><p><a href="https://learn.microsoft.com/en-us/rest/api/securityinsights/incidents?view=rest-securityinsights-2024-03-01">Incidents - REST API (Azure Sentinel)</a></p><p>or SPLUNK:</p><p><a href="https://dev.splunk.com/observability/reference/api/incidents/latest">https://dev.splunk.com/observability/reference/api/incidents/latest</a></p><p>Where can you go from here?</p><p>Many options are possible.</p><p>I will create some Snowflake dashboards and/or Streamlit dashboards so incidents can be easily visualized next. You can do that too.</p><p>Or have Snowflake itself deactivte the user first? Just add another stored procedure to the mix that does that.</p><p>Other events are also interesting to monitor e.g. maybe you want to be notified by some queries that some people should not execute?</p><p>Such as changes of NETWORK POLICIES?</p><p>Maybe you want to monitor logs from Snowflake Container services or running containers from an event table?</p><p>Or you think <strong>beyond Snowflake </strong>and ingest logs from <strong>other applications</strong> into Snowflake and build your dashboards and incident response API calls right within Snowflake.</p><p>With Snowflake Notebooks threat hunting in Python or regular SQL has never been more approachable.</p><p>Having all logs in Snowflake and the power to create automated API calls and doing threat hunting right in Snowflake means you can ingest and have as many logs available from as many systems as you like in a hot storage ready to query via Python and SQL at anytime.</p><p>Finally here is the whole SQL workbook for easy copy and pasting (make sure to replace XXX with your username before you run this!) :</p><pre><br><br>use role accountadmin;<br>CREATE OR REPLACE ROLE SIEM_MONITOR;<br>grant role siem_monitor to user XXXX;<br>GRANT CREATE WAREHOUSE ON ACCOUNT TO ROLE siem_monitor;<br>GRANT MANAGE WAREHOUSES ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE DATABASE ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE ROLE ON ACCOUNT TO siem_monitor;<br>GRANT EXECUTE TASK ON ACCOUNT TO ROLE siem_monitor;<br>GRANT EXECUTE ALERT ON ACCOUNT TO ROLE siem_monitor;<br>GRANT CREATE INTEGRATION ON ACCOUNT TO ROLE  siem_monitor;<br><br><br><br>use role siem_monitor;<br><br>CREATE OR REPLACE WAREHOUSE siem_alert_demo WITH WAREHOUSE_SIZE=&#39;X-SMALL&#39;;<br>grant usage on warehouse siem_alert_demo to role SIEM_MONITOR;<br><br>CREATE OR REPLACE DATABASE SIEM_ALERT_DEMO_DB;<br>CREATE OR REPLACE SCHEMA SIEM_ALERT_DEMO_SCHEMA;<br><br>grant usage on database SIEM_ALERT_DEMO_DB to role accountadmin;<br>grant usage on schema SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA to role accountadmin;<br>grant usage on warehouse siem_alert_demo to role accountadmin;<br><br><br><br>CREATE OR REPLACE ROLE MONITOR_USERS;<br>grant role monitor_users to role siem_monitor;<br><br><br><br>CREATE OR REPLACE PROCEDURE set_monitor_privileges_on_all_users(users varchar)<br>RETURNS varchar<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = &#39;3.8&#39;<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;)<br>HANDLER = &#39;run&#39;<br>EXECUTE AS CALLER<br>AS<br>$$<br>from snowflake.snowpark import Session<br><br><br>def run(session, users):<br>      users=users.replace(&#39;[&#39;,&#39;&#39;)<br>      users=users.replace(&#39;]&#39;,&#39;&#39;)<br>      users=users.split(&#39;,&#39;)<br>      for user in users:<br>              sql = &quot;&quot;&quot;GRANT MONITOR ON USER &quot;&quot;&quot;+user+&quot;&quot;&quot; TO ROLE monitor_users&quot;&quot;&quot;             <br>              result=session.sql(sql).collect()[0][0]<br><br><br>      return &quot;done&quot;<br>  <br>  <br>$$;<br><br>grant usage on procedure set_monitor_privileges_on_all_users(varchar) to role accountadmin;<br>use role accountadmin;<br>GRANT CREATE NETWORK RULE ON SCHEMA SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA TO ROLE siem_monitor;<br>call set_monitor_privileges_on_all_users(select all_user_names());<br><br><br><br>use role siem_monitor;<br>revoke usage on procedure set_monitor_privileges_on_all_users(varchar) from role accountadmin;<br>revoke usage on warehouse siem_alert_demo from role accountadmin;<br>revoke usage on database SIEM_ALERT_DEMO_DB from role accountadmin;<br>revoke usage on schema SIEM_ALERT_DEMO_DB.SIEM_ALERT_DEMO_SCHEMA from role accountadmin;<br><br><br><br>create or replace TABLE LOGIN_EVENTS (<br> EVENT_TIMESTAMP TIMESTAMP_LTZ(3),<br> EVENT_ID NUMBER(38,0),<br> EVENT_TYPE VARCHAR(16777216),<br> USER_NAME VARCHAR(16777216),<br> CLIENT_IP VARCHAR(16777216),<br> REPORTED_CLIENT_TYPE VARCHAR(16777216),<br> REPORTED_CLIENT_VERSION VARCHAR(16777216),<br> FIRST_AUTHENTICATION_FACTOR VARCHAR(16777216),<br> SECOND_AUTHENTICATION_FACTOR VARCHAR(16777216),<br> IS_SUCCESS VARCHAR(3),<br> ERROR_CODE NUMBER(38,0),<br> ERROR_MESSAGE VARCHAR(16777216),<br> RELATED_EVENT_ID NUMBER(38,0),<br> CONNECTION VARCHAR(16777216),<br> SENT BOOLEAN DEFAULT FALSE,<br> SENT_TIME TIMESTAMP_NTZ(9),<br> SIEM_RESPONSE VARCHAR(16777216)<br>);<br><br><br><br>create or replace view logins as select * from table(information_schema.login_history(TIME_RANGE_START =&gt; dateadd(&#39;minutes&#39;,-5,current_timestamp()),current_timestamp())) where IS_SUCCESS=&#39;NO&#39; and FIRST_AUTHENTICATION_FACTOR=&#39;PASSWORD&#39; order by event_timestamp;<br><br>CREATE OR REPLACE ALERT strange_login<br>  WAREHOUSE = siem_alert_demo<br>  SCHEDULE = &#39;1 minute&#39;<br>  IF( EXISTS(<br>      select event_id from logins<br>))<br>  THEN<br><br>INSERT INTO login_events  <br>SELECT *,false, NULL,NULL<br>FROM logins AS src<br>WHERE NOT EXISTS (SELECT event_id<br>                  FROM  login_events AS tgt<br>                  WHERE tgt.event_id = src.event_id);<br><br>    <br>ALTER ALERT STRANGE_LOGIN RESUME;<br><br><br><br>CREATE OR REPLACE NETWORK RULE siem_alert_network_rule<br>  MODE = EGRESS<br>  TYPE = HOST_PORT<br>  VALUE_LIST = (&#39;echo.free.beeceptor.com:443&#39;);<br><br>CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION siem_apis_access_integration<br>  ALLOWED_NETWORK_RULES = (siem_alert_network_rule)<br>  ENABLED = TRUE;<br><br><br><br>CREATE OR REPLACE FUNCTION alert_siem_login(incident string, url string)<br>RETURNS string<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = 3.8<br>HANDLER = &#39;incident_alert&#39;<br>EXTERNAL_ACCESS_INTEGRATIONS = (siem_apis_access_integration)<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;,&#39;requests&#39;)<br><br>AS<br>$$<br>import _snowflake<br>import requests<br>import json<br>session = requests.Session()<br><br>def incident_alert(incident, url):<br><br>    if incident != &#39;&#39;:<br>        payload = incident<br>    else:<br>        payload={}<br>    headers = {}<br><br>    response = requests.request(&quot;POST&quot;, url, headers=headers, data=payload)<br><br>    return response.text<br>$$;<br><br><br>CREATE OR REPLACE PROCEDURE sent_suspicious_logins()<br>RETURNS STRING<br>LANGUAGE PYTHON<br>RUNTIME_VERSION = &#39;3.8&#39;<br>PACKAGES = (&#39;snowflake-snowpark-python&#39;)<br>HANDLER = &#39;run&#39;<br>EXECUTE AS CALLER<br>AS<br>$$<br>from snowflake.snowpark import Session<br>import json<br><br>to_sent={}<br><br>def run(session):<br>  sql1 = &quot;&quot;&quot;select to_variant(<br>SELECT<br>  ARRAY_AGG(json) AS aggregated_results<br>FROM<br>  (<br>    SELECT<br>      OBJECT_CONSTRUCT(*) AS json<br>    FROM<br>      (select * from login_events where sent=false)<br>  )<br>    )&quot;&quot;&quot;       <br><br>  to_sent=json.loads(session.sql(sql1).collect()[0][0])<br><br>  for siem_payload in to_sent:<br>    sql_siem = &quot;&quot;&quot;UPDATE<br>                    login_events<br>                    SET<br>                        sent = TRUE,<br>                        sent_time = CURRENT_TIMESTAMP(),<br>                        siem_response= (select alert_siem_login(&#39;&quot;&quot;&quot; +json.dumps(siem_payload).replace(&#39;\&quot;&#39;,&#39;&quot;&#39;)+                                                       &quot;&quot;&quot;&#39;,&#39;https://echo.free.beeceptor.com:443/siem_api/incident&#39;))<br>                    WHERE<br>                        sent = FALSE&quot;&quot;&quot;<br>    sql_siem_result=session.sql(sql_siem).collect()[0][0]<br><br>  <br><br>  return &quot;payload sent&quot;<br>  <br>  <br>$$;<br><br><br>CREATE OR REPLACE task sent_strange_logins_to_siem<br>  WAREHOUSE = siem_alert_demo<br>  SCHEDULE = &#39;1 minute&#39;<br>  AS <br>call sent_suspicious_logins();<br><br><br><br>alter task sent_strange_logins_to_siem resume;<br><br><br><br><br><br></pre><pre>-- check <br><br>show tasks;<br>show alerts;<br><br>SELECT * from LOGIN_EVENTS;<br><br>-- monitor alerts &amp; tasks<br><br>SELECT *<br>FROM<br>  TABLE(INFORMATION_SCHEMA.ALERT_HISTORY(<br>    SCHEDULED_TIME_RANGE_START<br>      =&gt;dateadd(&#39;hour&#39;,-1,current_timestamp())))<br>ORDER BY SCHEDULED_TIME DESC;<br><br>SELECT *<br>FROM<br>  TABLE(INFORMATION_SCHEMA.TASK_HISTORY(<br>    SCHEDULED_TIME_RANGE_START<br>      =&gt;dateadd(&#39;hour&#39;,-1,current_timestamp())))<br>ORDER BY SCHEDULED_TIME DESC;<br><br><br><br><br>-- clean up (optional)<br><br>use role siem_monitor;<br>drop role monitor_users;<br>drop warehouse siem_alert_demo;<br>drop database SIEM_ALERT_DEMO_DB;<br>drop EXTERNAL ACCESS INTEGRATION siem_apis_access_integration;</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fb2ee4bea27b" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>