diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7e6c658..70d7508 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/phantomcyber/dev-cicd-tools
- rev: v1.13
+ rev: v1.16
hooks:
- id: org-hook
- id: package-app-dependencies
- repo: https://github.com/Yelp/detect-secrets
- rev: v1.2.0
+ rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--no-verify']
diff --git a/LICENSE b/LICENSE
index 257ffde..d2970d5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2022 Splunk Inc.
+ Copyright (c) 2023 Splunk Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 73d42d1..fcdce6a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,218 @@
-# Splunk> Phantom
+[comment]: # "Auto-generated SOAR connector documentation"
+# Snowflake
-Welcome to the open-source repository for Splunk> Phantom's snowflake App.
+Publisher: Splunk
+Connector Version: 1\.0\.1
+Product Vendor: Snowflake
+Product Name: Snowflake
+Product Version Supported (regex): "\.\*"
+Minimum Product Version: 5\.3\.5
-Please have a look at our [Contributing Guide](https://github.com/Splunk-SOAR-Apps/.github/blob/main/.github/CONTRIBUTING.md) if you are interested in contributing, raising issues, or learning more about open-source Phantom apps.
+This app supports investigative and data manipulation actions on Snowflake
-## Legal and License
-This Phantom App is licensed under the Apache 2.0 license. Please see our [Contributing Guide](https://github.com/Splunk-SOAR-Apps/.github/blob/main/.github/CONTRIBUTING.md#legal-notice) for further details.
+## Port Details
+
+The app uses HTTPS protocol for communicating with Snowflake. Below are the default ports used by
+the Splunk SOAR Connector.
+
+| SERVICE NAME | TRANSPORT PROTOCOL | PORT |
+|--------------|--------------------|------|
+| https | tcp | 443 |
+
+## Roles
+
+Roles are used by Snowflake to **control access to objects** within the organization and allow users
+to perform actions against those objects. Users can have several roles granted to them, and can also
+have a default role assigned. Since a user is allowed to switch roles during a session in order to
+have the appropriate permissions to perform certain actions, the Snowflake app accomodates this by
+having an optional 'role' parameter in each of the actions. If this parameter is left blank, the
+default role assigned to the user will be used.
+
+
+### Configuration Variables
+The below configuration variables are required for this Connector to operate. These variables are specified when configuring a Snowflake asset in SOAR.
+
+VARIABLE | REQUIRED | TYPE | DESCRIPTION
+-------- | -------- | ---- | -----------
+**account** | required | string | Account
+**username** | required | string | Username
+**password** | required | password | Password
+
+### Supported Actions
+[test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity using supplied configuration
+[run query](#action-run-query) - Perform a SQL query
+[disable user](#action-disable-user) - Disable a Snowflake user
+[show network policies](#action-show-network-policies) - List available network policies
+[describe network policy](#action-describe-network-policy) - List the details of a network policy
+[update network policy](#action-update-network-policy) - Update an existing network policy
+[remove grants](#action-remove-grants) - Remove a specified granted role from a Snowflake user
+
+## action: 'test connectivity'
+Validate the asset configuration for connectivity using supplied configuration
+
+Type: **test**
+Read only: **True**
+
+#### Action Parameters
+No parameters are required for this action
+
+#### Action Output
+No Output
+
+## action: 'run query'
+Perform a SQL query
+
+Type: **investigate**
+Read only: **False**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**query** | required | Query string | string | `sql query`
+**role** | optional | Role to use to execute action | string |
+**warehouse** | optional | Warehouse | string |
+**database** | optional | Database | string |
+**schema** | optional | Schema | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.data | string | |
+action\_result\.status | string | | success
+action\_result\.message | string | | Total rows\: 4
+action\_result\.summary\.total\_rows | numeric | | 4
+action\_result\.parameter\.role | string | | accountadmin
+action\_result\.parameter\.query | string | `sql query` | select \* from test\_table;
+action\_result\.parameter\.schema | string | | testschema
+action\_result\.parameter\.database | string | | test1db
+action\_result\.parameter\.warehouse | string | | warehouse1
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
+
+## action: 'disable user'
+Disable a Snowflake user
+
+Type: **investigate**
+Read only: **False**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**username** | required | Snowflake user name | string | `user name`
+**role** | optional | Role to use to execute action | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.parameter\.username | string | `user name` | test1
+action\_result\.data\.\*\.status | string | | Statement executed successfully\.
+action\_result\.status | string | | success
+action\_result\.message | string | | Status\: Statement executed successfully\.
+action\_result\.summary\.status | string | | Statement executed successfully\.
+action\_result\.parameter\.role | string | | accountadmin
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
+
+## action: 'show network policies'
+List available network policies
+
+Type: **investigate**
+Read only: **True**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**role** | optional | Role to use to execute action | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.data\.\*\.name | string | | MYPOLICY1
+action\_result\.parameter\.role | string | | accountadmin
+action\_result\.data\.\*\.comment | string | | testing app
+action\_result\.data\.\*\.created\_on | string | | 2022\-12\-19 14\:10\:12\.084000\-08\:00
+action\_result\.data\.\*\.entries\_in\_allowed\_ip\_list | numeric | | 2
+action\_result\.data\.\*\.entries\_in\_blocked\_ip\_list | numeric | | 1
+action\_result\.status | string | | success
+action\_result\.message | string | | Total policies\: 1
+action\_result\.summary\.total\_policies | numeric | | 1
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
+
+## action: 'describe network policy'
+List the details of a network policy
+
+Type: **investigate**
+Read only: **True**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**policy\_name** | required | Name of policy to describe | string | `snowflake policy name`
+**role** | optional | Role to use to execute action | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.data\.\*\.name | string | | ALLOWED\_IP\_LIST
+action\_result\.data\.\*\.value | string | `ip` | 192\.168\.1\.0/24,192\.168\.2\.0/24
+action\_result\.status | string | | success
+action\_result\.message | string | |
+action\_result\.parameter\.policy\_name | string | `snowflake policy name` | mypolicy1
+action\_result\.parameter\.role | string | | accountadmin
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
+
+## action: 'update network policy'
+Update an existing network policy
+
+Type: **investigate**
+Read only: **False**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**policy\_name** | required | Name of network policy to update | string | `snowflake policy name`
+**role** | optional | Role to use to execute action | string |
+**allowed\_ip\_list** | optional | Comma\-separated list of IPs to replace current allow list\. Add an empty list to clear all IPs from allow list\. | string |
+**blocked\_ip\_list** | optional | Comma\-separated list of IPs to replace current block list\. Add an empty list to clear all IPs from block list\. | string |
+**comment** | optional | Replace current comment on network policy | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.data\.\*\.status | string | | Statement executed successfully\.
+action\_result\.status | string | | success
+action\_result\.message | string | | Network policy mypolicy1 was updated successfully
+action\_result\.parameter\.comment | string | | updated policy a new update
+action\_result\.parameter\.policy\_name | string | `snowflake policy name` | mypolicy1
+action\_result\.parameter\.role | string | | accountadmin
+action\_result\.parameter\.allowed\_ip\_list | string | | 192\.168\.1\.0/24, 192\.168\.2\.0/24 192\.168\.10\.0/24
+action\_result\.parameter\.blocked\_ip\_list | string | | 192\.168\.1\.1, 192\.168\.2\.1 192\.168\.10\.1, 192\.168\.10\.5, 192\.168\.10\.6
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
+
+## action: 'remove grants'
+Remove a specified granted role from a Snowflake user
+
+Type: **investigate**
+Read only: **False**
+
+#### Action Parameters
+PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS
+--------- | -------- | ----------- | ---- | --------
+**username** | required | Username | string | `user name`
+**role\_to\_remove** | required | Role to remove from user | string |
+**role** | optional | Role to use to execute action | string |
+
+#### Action Output
+DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES
+--------- | ---- | -------- | --------------
+action\_result\.data\.\*\.status | string | | Statement executed successfully\.
+action\_result\.status | string | | success
+action\_result\.message | string | | Role accountadmin was successfully removed from user
+action\_result\.parameter\.username | string | `user name` | test2
+action\_result\.parameter\.role\_to\_remove | string | | accountadmin
+summary\.total\_objects | numeric | | 1
+summary\.total\_objects\_successful | numeric | | 1
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..755982e
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,14 @@
+# File: __init__.py
+#
+# Copyright (c) 2023 Splunk Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the License for the specific language governing permissions
+# and limitations under the License.
diff --git a/exclude_files.txt b/exclude_files.txt
new file mode 100644
index 0000000..c1c9f4d
--- /dev/null
+++ b/exclude_files.txt
@@ -0,0 +1 @@
+.git*
diff --git a/query_results.html b/query_results.html
new file mode 100644
index 0000000..036a58a
--- /dev/null
+++ b/query_results.html
@@ -0,0 +1,109 @@
+{% extends 'widgets/widget_template.html' %}
+{% load custom_template %}
+
+{% block custom_title_prop %}{% if title_logo %}style="background-size: auto 60%; background-position: 50%; background-repeat: no-repeat; background-image: url('/app_resource/{{ title_logo }}');"{% endif %}{% endblock %}
+{% block title1 %}{{ title1 }}{% endblock %}
+{% block title2 %}{{ title2 }}{% endblock %}
+{% block custom_tools %}
+{% endblock %}
+
+{% block widget_content %}
+
+
+
+
+
+ {% for result in results %}
+ {% if not result.data %}
+
No data found
+
+ {% else %}
+
+
+
+
+
+ {% for header in result.headers%}
+ {{ header }} |
+ {% endfor %}
+
+
+
+ {% for row in result.data %}
+
+ {% for cell in row %}
+ {{ cell.value }} |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/readme.html b/readme.html
new file mode 100644
index 0000000..828fea4
--- /dev/null
+++ b/readme.html
@@ -0,0 +1,27 @@
+
+
+ Port Details
+
+ The app uses HTTPS protocol for communicating with Snowflake. Below are the default ports used by the Splunk SOAR Connector.
+
+
+ SERVICE NAME |
+ TRANSPORT PROTOCOL |
+ PORT |
+
+
+ https |
+ tcp |
+ 443 |
+
+
+
+
+ Roles
+
+ Roles are used by Snowflake to control access to objects within the organization and allow users to perform actions against those objects.
+ Users can have several roles granted to them, and can also have a default role assigned. Since a user is allowed to switch roles during a session in
+ order to have the appropriate permissions to perform certain actions, the Snowflake app accomodates this by having an optional 'role' parameter
+ in each of the actions. If this parameter is left blank, the default role assigned to the user will be used.
+
+
diff --git a/release_notes/1.0.1.md b/release_notes/1.0.1.md
new file mode 100644
index 0000000..c9e7489
--- /dev/null
+++ b/release_notes/1.0.1.md
@@ -0,0 +1 @@
+* Initial release [PAPP-27453]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ed2110b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,17 @@
+asn1crypto==1.5.1
+certifi==2022.12.07
+cffi==1.15.1
+charset-normalizer==2.1.1
+cryptography==38.0.4
+filelock==3.8.2
+idna==3.4
+oscrypto==1.3.0
+pycparser==2.21
+pycryptodomex==3.16.0
+PyJWT==2.6.0
+pyOpenSSL==22.1.0
+pytz==2022.6
+requests==2.28.1
+snowflake-connector-python==2.9.0
+typing_extensions==4.4.0
+urllib3==1.26.13
diff --git a/snowflake.json b/snowflake.json
new file mode 100644
index 0000000..1eb8a3c
--- /dev/null
+++ b/snowflake.json
@@ -0,0 +1,763 @@
+{
+ "appid": "9dc1ac34-cac9-41fb-af81-e71ffe256f49",
+ "name": "Snowflake",
+ "description": "This app supports investigative and data manipulation actions on Snowflake",
+ "type": "siem",
+ "product_vendor": "Snowflake",
+ "logo": "snowflake.svg",
+ "logo_dark": "snowflake_dark.svg",
+ "product_name": "Snowflake",
+ "python_version": "3",
+ "product_version_regex": ".*",
+ "publisher": "Splunk",
+ "license": "Copyright (c) 2023 Splunk Inc.",
+ "app_version": "1.0.1",
+ "utctime_updated": "2022-12-13T22:47:52.338441Z",
+ "package_name": "phantom_snowflake",
+ "main_module": "snowflake_connector.py",
+ "min_phantom_version": "5.3.5",
+ "app_wizard_version": "1.0.0",
+ "fips_compliant": false,
+ "latest_tested_versions": [
+ "Snowflake Jan 2023"
+ ],
+ "configuration": {
+ "account": {
+ "description": "Account",
+ "data_type": "string",
+ "required": true,
+ "order": 0
+ },
+ "username": {
+ "description": "Username",
+ "data_type": "string",
+ "required": true,
+ "order": 1
+ },
+ "password": {
+ "description": "Password",
+ "data_type": "password",
+ "required": true,
+ "order": 2
+ }
+ },
+ "actions": [
+ {
+ "action": "test connectivity",
+ "identifier": "test_connectivity",
+ "description": "Validate the asset configuration for connectivity using supplied configuration",
+ "type": "test",
+ "read_only": true,
+ "parameters": {},
+ "output": [],
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "run query",
+ "identifier": "run_query",
+ "description": "Perform a SQL query",
+ "type": "investigate",
+ "read_only": false,
+ "parameters": {
+ "query": {
+ "description": "Query string",
+ "data_type": "string",
+ "required": true,
+ "primary": true,
+ "contains": [
+ "sql query"
+ ],
+ "order": 0
+ },
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 1
+ },
+ "warehouse": {
+ "description": "Warehouse",
+ "data_type": "string",
+ "order": 2
+ },
+ "database": {
+ "description": "Database",
+ "data_type": "string",
+ "order": 3
+ },
+ "schema": {
+ "description": "Schema",
+ "data_type": "string",
+ "order": 4
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.data",
+ "data_type": "string"
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string",
+ "example_values": [
+ "Total rows: 4"
+ ]
+ },
+ {
+ "data_path": "action_result.summary.total_rows",
+ "data_type": "numeric",
+ "example_values": [
+ 4
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.role",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.query",
+ "data_type": "string",
+ "example_values": [
+ "select * from test_table;"
+ ],
+ "contains": [
+ "sql query"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.schema",
+ "data_type": "string",
+ "example_values": [
+ "testschema"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.database",
+ "data_type": "string",
+ "example_values": [
+ "test1db"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.warehouse",
+ "data_type": "string",
+ "example_values": [
+ "warehouse1"
+ ]
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "render": {
+ "type": "custom",
+ "width": 10,
+ "height": 5,
+ "view": "snowflake_view.display_query_results",
+ "title": "Execute Query"
+ },
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "disable user",
+ "identifier": "disable_user",
+ "description": "Disable a Snowflake user",
+ "parameters": {
+ "username": {
+ "description": "Snowflake user name",
+ "data_type": "string",
+ "required": true,
+ "primary": true,
+ "contains": [
+ "user name"
+ ],
+ "order": 0
+ },
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 1
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.parameter.username",
+ "data_type": "string",
+ "example_values": [
+ "test1"
+ ],
+ "contains": [
+ "user name"
+ ],
+ "column_name": "Username",
+ "column_order": 0
+ },
+ {
+ "data_path": "action_result.data.*.status",
+ "data_type": "string",
+ "example_values": [
+ "Statement executed successfully."
+ ],
+ "column_name": "Status",
+ "column_order": 1
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string",
+ "example_values": [
+ "Status: Statement executed successfully."
+ ]
+ },
+ {
+ "data_path": "action_result.summary.status",
+ "data_type": "string",
+ "example_values": [
+ "Statement executed successfully."
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.role",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ]
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "read_only": false,
+ "type": "investigate",
+ "render": {
+ "type": "table"
+ },
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "show network policies",
+ "identifier": "show_network_policies",
+ "description": "List available network policies",
+ "parameters": {
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 0
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.data.*.name",
+ "data_type": "string",
+ "example_values": [
+ "MYPOLICY1"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.role",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ]
+ },
+ {
+ "data_path": "action_result.data.*.comment",
+ "data_type": "string",
+ "example_values": [
+ "testing app"
+ ]
+ },
+ {
+ "data_path": "action_result.data.*.created_on",
+ "data_type": "string",
+ "example_values": [
+ "2022-12-19 14:10:12.084000-08:00"
+ ]
+ },
+ {
+ "data_path": "action_result.data.*.entries_in_allowed_ip_list",
+ "data_type": "numeric",
+ "example_values": [
+ 2
+ ]
+ },
+ {
+ "data_path": "action_result.data.*.entries_in_blocked_ip_list",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string",
+ "example_values": [
+ "Total policies: 1"
+ ]
+ },
+ {
+ "data_path": "action_result.summary.total_policies",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "read_only": true,
+ "type": "investigate",
+ "render": {
+ "type": "custom",
+ "width": 10,
+ "height": 5,
+ "view": "snowflake_view.display_query_results",
+ "title": "Show Network Policies"
+ },
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "describe network policy",
+ "identifier": "describe_network_policy",
+ "description": "List the details of a network policy",
+ "parameters": {
+ "policy_name": {
+ "description": "Name of policy to describe",
+ "data_type": "string",
+ "required": true,
+ "primary": true,
+ "contains": [
+ "snowflake policy name"
+ ],
+ "order": 0
+ },
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 1
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.data.*.name",
+ "data_type": "string",
+ "example_values": [
+ "ALLOWED_IP_LIST"
+ ]
+ },
+ {
+ "data_path": "action_result.data.*.value",
+ "data_type": "string",
+ "example_values": [
+ "192.168.1.0/24,192.168.2.0/24"
+ ],
+ "contains": [
+ "ip"
+ ]
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string"
+ },
+ {
+ "data_path": "action_result.parameter.policy_name",
+ "data_type": "string",
+ "example_values": [
+ "mypolicy1"
+ ],
+ "contains": [
+ "snowflake policy name"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.role",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ]
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "read_only": true,
+ "type": "investigate",
+ "render": {
+ "type": "custom",
+ "width": 10,
+ "height": 5,
+ "view": "snowflake_view.display_query_results",
+ "title": "Describe Network Policy"
+ },
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "update network policy",
+ "identifier": "update_network_policy",
+ "description": "Update an existing network policy",
+ "parameters": {
+ "policy_name": {
+ "description": "Name of network policy to update",
+ "data_type": "string",
+ "required": true,
+ "primary": true,
+ "contains": [
+ "snowflake policy name"
+ ],
+ "order": 0
+ },
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 1
+ },
+ "allowed_ip_list": {
+ "description": "Comma-separated list of IPs to replace current allow list. Add an empty list to clear all IPs from allow list.",
+ "data_type": "string",
+ "order": 2
+ },
+ "blocked_ip_list": {
+ "description": "Comma-separated list of IPs to replace current block list. Add an empty list to clear all IPs from block list.",
+ "data_type": "string",
+ "order": 3
+ },
+ "comment": {
+ "description": "Replace current comment on network policy",
+ "data_type": "string",
+ "order": 4
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.data.*.status",
+ "data_type": "string",
+ "example_values": [
+ "Statement executed successfully."
+ ],
+ "column_name": "Status",
+ "column_order": 1
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string",
+ "example_values": [
+ "Network policy mypolicy1 was updated successfully"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.comment",
+ "data_type": "string",
+ "example_values": [
+ "updated policy",
+ "a new update"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.policy_name",
+ "data_type": "string",
+ "example_values": [
+ "mypolicy1"
+ ],
+ "contains": [
+ "snowflake policy name"
+ ],
+ "column_name": "Policy Name",
+ "column_order": 0
+ },
+ {
+ "data_path": "action_result.parameter.role",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.allowed_ip_list",
+ "data_type": "string",
+ "example_values": [
+ "192.168.1.0/24, 192.168.2.0/24",
+ "192.168.10.0/24"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.blocked_ip_list",
+ "data_type": "string",
+ "example_values": [
+ "192.168.1.1, 192.168.2.1",
+ "192.168.10.1, 192.168.10.5, 192.168.10.6"
+ ]
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "read_only": false,
+ "type": "investigate",
+ "render": {
+ "type": "table"
+ },
+ "versions": "EQ(*)"
+ },
+ {
+ "action": "remove grants",
+ "identifier": "remove_grants",
+ "description": "Remove a specified granted role from a Snowflake user",
+ "parameters": {
+ "username": {
+ "description": "Username",
+ "data_type": "string",
+ "required": true,
+ "contains": [
+ "user name"
+ ],
+ "order": 0
+ },
+ "role_to_remove": {
+ "description": "Role to remove from user",
+ "data_type": "string",
+ "required": true,
+ "order": 1
+ },
+ "role": {
+ "description": "Role to use to execute action",
+ "data_type": "string",
+ "order": 2
+ }
+ },
+ "output": [
+ {
+ "data_path": "action_result.data.*.status",
+ "data_type": "string",
+ "example_values": [
+ "Statement executed successfully."
+ ],
+ "column_name": "Status",
+ "column_order": 2
+ },
+ {
+ "data_path": "action_result.status",
+ "data_type": "string",
+ "example_values": [
+ "success"
+ ]
+ },
+ {
+ "data_path": "action_result.message",
+ "data_type": "string",
+ "example_values": [
+ "Role accountadmin was successfully removed from user"
+ ]
+ },
+ {
+ "data_path": "action_result.parameter.username",
+ "data_type": "string",
+ "example_values": [
+ "test2"
+ ],
+ "contains": [
+ "user name"
+ ],
+ "column_name": "Username",
+ "column_order": 0
+ },
+ {
+ "data_path": "action_result.parameter.role_to_remove",
+ "data_type": "string",
+ "example_values": [
+ "accountadmin"
+ ],
+ "column_name": "Role to Remove",
+ "column_order": 1
+ },
+ {
+ "data_path": "summary.total_objects",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ },
+ {
+ "data_path": "summary.total_objects_successful",
+ "data_type": "numeric",
+ "example_values": [
+ 1
+ ]
+ }
+ ],
+ "read_only": false,
+ "type": "investigate",
+ "render": {
+ "type": "table"
+ },
+ "versions": "EQ(*)"
+ }
+ ],
+ "pip39_dependencies": {
+ "wheel": [
+ {
+ "module": "PyJWT",
+ "input_file": "wheels/py3/PyJWT-2.6.0-py3-none-any.whl"
+ },
+ {
+ "module": "asn1crypto",
+ "input_file": "wheels/shared/asn1crypto-1.5.1-py2.py3-none-any.whl"
+ },
+ {
+ "module": "certifi",
+ "input_file": "wheels/py3/certifi-2022.12.7-py3-none-any.whl"
+ },
+ {
+ "module": "cffi",
+ "input_file": "wheels/py39/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ },
+ {
+ "module": "charset_normalizer",
+ "input_file": "wheels/py3/charset_normalizer-2.1.1-py3-none-any.whl"
+ },
+ {
+ "module": "cryptography",
+ "input_file": "wheels/py36/cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ },
+ {
+ "module": "filelock",
+ "input_file": "wheels/py3/filelock-3.8.2-py3-none-any.whl"
+ },
+ {
+ "module": "idna",
+ "input_file": "wheels/py3/idna-3.4-py3-none-any.whl"
+ },
+ {
+ "module": "oscrypto",
+ "input_file": "wheels/shared/oscrypto-1.3.0-py2.py3-none-any.whl"
+ },
+ {
+ "module": "pyOpenSSL",
+ "input_file": "wheels/py3/pyOpenSSL-22.1.0-py3-none-any.whl"
+ },
+ {
+ "module": "pycparser",
+ "input_file": "wheels/shared/pycparser-2.21-py2.py3-none-any.whl"
+ },
+ {
+ "module": "pycryptodomex",
+ "input_file": "wheels/py3/pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ },
+ {
+ "module": "pytz",
+ "input_file": "wheels/shared/pytz-2022.6-py2.py3-none-any.whl"
+ },
+ {
+ "module": "requests",
+ "input_file": "wheels/py3/requests-2.28.1-py3-none-any.whl"
+ },
+ {
+ "module": "setuptools",
+ "input_file": "wheels/py3/setuptools-67.1.0-py3-none-any.whl"
+ },
+ {
+ "module": "snowflake_connector_python",
+ "input_file": "wheels/py39/snowflake_connector_python-2.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ },
+ {
+ "module": "typing_extensions",
+ "input_file": "wheels/py3/typing_extensions-4.4.0-py3-none-any.whl"
+ },
+ {
+ "module": "urllib3",
+ "input_file": "wheels/shared/urllib3-1.26.13-py2.py3-none-any.whl"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/snowflake.svg b/snowflake.svg
new file mode 100644
index 0000000..5b79263
--- /dev/null
+++ b/snowflake.svg
@@ -0,0 +1,27 @@
+
diff --git a/snowflake_connector.py b/snowflake_connector.py
new file mode 100644
index 0000000..2529a73
--- /dev/null
+++ b/snowflake_connector.py
@@ -0,0 +1,448 @@
+# File: snowflake_connector.py
+#
+# Copyright (c) 2023 Splunk Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the License for the specific language governing permissions
+# and limitations under the License.
+#
+
+# Import order matters here - if isort is allowed to put the Snowflake connector
+# import later in the file, the connector crashes at runtime.
+import snowflake.connector # isort: skip
+from snowflake_consts import * # isort: skip
+
+import datetime
+import json
+import traceback
+
+# Phantom App imports
+import phantom.app as phantom
+import requests
+from phantom.action_result import ActionResult
+from phantom.base_connector import BaseConnector
+
+
+class RetVal(tuple):
+
+ def __new__(cls, val1, val2=None):
+ return tuple.__new__(RetVal, (val1, val2))
+
+
+class SnowflakeConnector(BaseConnector):
+
+ def __init__(self):
+
+ # Call the BaseConnectors init first
+ super(SnowflakeConnector, self).__init__()
+
+ self._state = None
+
+ self._account = None
+ self._username = None
+ self._password = None
+
+ def _get_error_msg_from_exception(self, e):
+ error_code = SNOWFLAKE_ERROR_CODE_UNAVAILABLE
+ error_msg = SNOWFLAKE_ERROR_MSG_UNAVAILABLE
+
+ self.error_print(traceback.format_exc())
+
+ try:
+ if e.args:
+ if len(e.args) > 1:
+ error_code = e.args[0]
+ error_msg = e.args[1]
+ return "Error Code: {0}. Error Message: {1}".format(error_code, error_msg)
+ elif len(e.args) == 1:
+ error_msg = e.args[0]
+ except Exception:
+ pass
+
+ return "Error Message: {0}".format(error_msg)
+
+ def convert_value(self, value):
+ if isinstance(value, (bytearray, bytes)):
+ return value.decode('utf-8')
+ elif isinstance(value, (datetime.datetime, datetime.timedelta, datetime.date)):
+ return str(value)
+ else:
+ return value
+
+ def _cleanup_row_values(self, row):
+ return {k: self.convert_value(v) for k, v in row.items()}
+
+ def _handle_test_connectivity(self, param):
+ self.save_progress(TEST_CONNECTIVITY_PROGRESS_MSG)
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ try:
+ self._connection = self._handle_create_connection()
+ cursor = self._connection.cursor()
+
+ cursor.execute(SNOWFLAKE_VERSION_QUERY)
+ if cursor:
+ self.save_progress(TEST_CONNECTIVITY_SUCCESS_MSG)
+ return action_result.set_status(phantom.APP_SUCCESS)
+ except Exception as e:
+ self.save_progress(self._get_error_msg_from_exception(e))
+ return action_result.set_status(phantom.APP_ERROR, TEST_CONNECTIVITY_ERROR_MSG)
+
+ def _handle_run_query(self, param):
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ query = param['query']
+ role = param.get('role')
+ warehouse = param.get('warehouse')
+ database = param.get('database')
+ schema = param.get('schema')
+
+ try:
+ self._connection = self._handle_create_connection(role, warehouse, database, schema)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(query)
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+
+ while len(returned_rows) > 0:
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, '{0}: {1}'.format(SQL_QUERY_ERROR_MSG, error_msg))
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+
+ summary = action_result.update_summary({})
+
+ if cursor.rowcount > 0:
+ summary[SNOWFLAKE_TOTAL_ROWS_JSON] = cursor.rowcount
+ else:
+ summary[SNOWFLAKE_TOTAL_ROWS_JSON] = 0
+
+ return action_result.set_status(phantom.APP_SUCCESS)
+
+ def _handle_disable_user(self, param):
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ database = SNOWFLAKE_DATABASE
+ username = param['username']
+ role = param.get('role')
+
+ try:
+ self._connection = self._handle_create_connection(database=database, role=role)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(DISABLE_SNOWFLAKE_USER_SQL.format(username=username))
+ row = cursor.fetchone()
+ action_result.add_data(row)
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, '{0}: {1}'.format(DISABLE_USER_ERROR_MSG, error_msg))
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+
+ summary = action_result.update_summary({})
+ summary['user_status'] = 'disabled'
+
+ return action_result.set_status(phantom.APP_SUCCESS)
+
+ def _handle_show_network_policies(self, param):
+
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ database = SNOWFLAKE_DATABASE
+ role = param.get('role')
+
+ try:
+ self._connection = self._handle_create_connection(database=database, role=role)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(SHOW_NETWORK_POLICIES_SQL)
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+ self.debug_print("returned_rows: {}".format(returned_rows))
+
+ while len(returned_rows) > 0:
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, error_msg)
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+
+ summary = action_result.update_summary({})
+ summary['total_policies'] = len(action_result.get_data())
+
+ return action_result.set_status(phantom.APP_SUCCESS)
+
+ def _handle_describe_network_policy(self, param):
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ database = SNOWFLAKE_DATABASE
+ role = param.get('role')
+
+ policy_name = param['policy_name']
+
+ try:
+ self._connection = self._handle_create_connection(database=database, role=role)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(DESCRIBE_NETWORK_POLICY_SQL.format(policy_name=policy_name))
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+ self.debug_print("returned_rows: {}".format(returned_rows))
+
+ while len(returned_rows) > 0:
+ returned_rows = cursor.fetchmany(DEFAULT_NUM_ROWS_TO_FETCH)
+ for row in returned_rows:
+ action_result.add_data(self._cleanup_row_values(row))
+
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, error_msg)
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+ return action_result.set_status(phantom.APP_SUCCESS)
+
+ def _handle_update_network_policy(self, param):
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ database = SNOWFLAKE_DATABASE
+ policy_name = param['policy_name']
+ role = param.get('role')
+
+ # Putting single quotes around each IP address in the list to satisfy SQL formatting. Empty string to clear.
+ try:
+ allowed_ip_list = param.get('allowed_ip_list')
+ if allowed_ip_list:
+ allowed_ip_list = ','.join(f"'{ip.strip()}'" for ip in allowed_ip_list.split(','))
+ else:
+ allowed_ip_list = ''
+
+ blocked_ip_list = param.get('blocked_ip_list')
+ if blocked_ip_list:
+ blocked_ip_list = ','.join(f"'{ip.strip()}'" for ip in blocked_ip_list.split(','))
+ else:
+ blocked_ip_list = ''
+
+ comment = param.get('comment')
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, error_msg)
+
+ try:
+ self._connection = self._handle_create_connection(database=database, role=role)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(UPDATE_NETWORK_POLICY_SQL.format(policy_name=policy_name,
+ allowed_ip_list=allowed_ip_list, blocked_ip_list=blocked_ip_list, comment=comment))
+ row = cursor.fetchone()
+ action_result.add_data(row)
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, error_msg)
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+
+ return action_result.set_status(phantom.APP_SUCCESS, UPDATE_NETWORK_POLICY_SUCCESS_MSG.format(policy_name=policy_name))
+
+ def _handle_remove_grants(self, param):
+ self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
+
+ action_result = self.add_action_result(ActionResult(dict(param)))
+
+ database = SNOWFLAKE_DATABASE
+ username = param['username']
+ role_to_remove = param['role_to_remove']
+ role = param.get('role')
+
+ try:
+ self._connection = self._handle_create_connection(role=role, database=database)
+ cursor = self._connection.cursor(snowflake.connector.DictCursor)
+ cursor.execute(REMOVE_GRANTS_SQL.format(username=username, role_to_remove=role_to_remove))
+ row = cursor.fetchone()
+ action_result.add_data(row)
+
+ except Exception as e:
+ error_msg = self._get_error_msg_from_exception(e)
+ self.save_progress("Error: {}".format(error_msg))
+ return action_result.set_status(phantom.APP_ERROR, error_msg)
+
+ finally:
+ if self._connection:
+ cursor.close()
+ self._connection.close()
+
+ return action_result.set_status(phantom.APP_SUCCESS, REMOVE_GRANTS_SUCCESS_MSG.format(role=role_to_remove))
+
+ def _handle_create_connection(self, role=None, warehouse=None, database=None, schema=None):
+ ctx = snowflake.connector.connect(
+ user=self._username,
+ password=self._password,
+ account=self._account,
+ role=role,
+ warehouse=warehouse,
+ database=database,
+ schema=schema
+ )
+ return ctx
+
+ def handle_action(self, param):
+ ret_val = phantom.APP_SUCCESS
+
+ # Get the action that we are supposed to execute for this App Run
+ action_id = self.get_action_identifier()
+
+ self.debug_print("action_id", self.get_action_identifier())
+
+ if action_id == 'test_connectivity':
+ ret_val = self._handle_test_connectivity(param)
+
+ if action_id == 'run_query':
+ ret_val = self._handle_run_query(param)
+
+ if action_id == 'disable_user':
+ ret_val = self._handle_disable_user(param)
+
+ if action_id == 'remove_grants':
+ ret_val = self._handle_remove_grants(param)
+
+ if action_id == 'show_network_policies':
+ ret_val = self._handle_show_network_policies(param)
+
+ if action_id == 'describe_network_policy':
+ ret_val = self._handle_describe_network_policy(param)
+
+ if action_id == 'update_network_policy':
+ ret_val = self._handle_update_network_policy(param)
+
+ return ret_val
+
+ def initialize(self):
+ # Load the state in initialize, use it to store data
+ # that needs to be accessed across actions
+ self._state = self.load_state()
+
+ # get the asset config
+ config = self.get_config()
+
+ self._account = config['account']
+ self._username = config['username']
+ self._password = config['password']
+ self._connection = None
+
+ return phantom.APP_SUCCESS
+
+ def finalize(self):
+ # Save the state, this data is saved across actions and app upgrades
+ self.save_state(self._state)
+ return phantom.APP_SUCCESS
+
+
+def main():
+ import argparse
+
+ import pudb
+ pudb.set_trace()
+
+ argparser = argparse.ArgumentParser()
+
+ argparser.add_argument('input_test_json', help='Input Test JSON file')
+ argparser.add_argument('-u', '--username', help='username', required=False)
+ argparser.add_argument('-p', '--password', help='password', required=False)
+
+ args = argparser.parse_args()
+ session_id = None
+
+ username = args.username
+ password = args.password
+
+ if username is not None and password is None:
+
+ # User specified a username but not a password, so ask
+ import getpass
+ password = getpass.getpass("Password: ")
+
+ if username and password:
+ try:
+ login_url = SnowflakeConnector._get_phantom_base_url() + '/login'
+
+ print("Accessing the Login page")
+ r = requests.get(login_url, verify=False)
+ csrftoken = r.cookies['csrftoken']
+
+ data = dict()
+ data['username'] = username
+ data['password'] = password
+ data['csrfmiddlewaretoken'] = csrftoken
+
+ headers = dict()
+ headers['Cookie'] = 'csrftoken=' + csrftoken
+ headers['Referer'] = login_url
+
+ print("Logging into Platform to get the session id")
+ r2 = requests.post(login_url, verify=False, data=data, headers=headers)
+ session_id = r2.cookies['sessionid']
+ except Exception as e:
+ print("Unable to get session id from the platform. Error: " + str(e))
+ exit(1)
+
+ with open(args.input_test_json) as f:
+ in_json = f.read()
+ in_json = json.loads(in_json)
+ print(json.dumps(in_json, indent=4))
+
+ connector = SnowflakeConnector()
+ connector.print_progress_message = True
+
+ if session_id is not None:
+ in_json['user_session_token'] = session_id
+ connector._set_csrf_info(csrftoken, headers['Referer'])
+
+ ret_val = connector._handle_action(json.dumps(in_json), None)
+ print(json.dumps(json.loads(ret_val), indent=4))
+
+ exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/snowflake_consts.py b/snowflake_consts.py
new file mode 100644
index 0000000..f5dd7df
--- /dev/null
+++ b/snowflake_consts.py
@@ -0,0 +1,51 @@
+# File: snowflake_consts.py
+#
+# Copyright (c) 2023 Splunk Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the License for the specific language governing permissions
+# and limitations under the License.
+#
+
+# Database admin config parameters
+SNOWFLAKE_DATABASE = "SNOWFLAKE"
+SNOWFLAKE_ACCOUNT_ADMIN_ROLE = "ACCOUNTADMIN"
+DEFAULT_NUM_ROWS_TO_FETCH = 100
+
+# Test connectivity constants
+SNOWFLAKE_VERSION_QUERY = "select current_version();"
+TEST_CONNECTIVITY_PROGRESS_MSG = "Connecting to Snowflake endpoint"
+
+# Action SQL statements
+DESCRIBE_SNOWFLAKE_USER_SQL = "desc user {username};"
+DISABLE_SNOWFLAKE_USER_SQL = "alter user {username} set disabled=true;"
+SHOW_NETWORK_POLICIES_SQL = "show network policies;"
+DESCRIBE_NETWORK_POLICY_SQL = "describe network policy {policy_name};"
+UPDATE_NETWORK_POLICY_SQL = "alter network policy {policy_name} " \
+ "set allowed_ip_list=({allowed_ip_list}) blocked_ip_list=({blocked_ip_list}) comment='{comment}';"
+REMOVE_GRANTS_SQL = 'revoke role {role_to_remove} from user {username};'
+
+# Action error messages
+TEST_CONNECTIVITY_ERROR_MSG = 'Test connectivity failed'
+SQL_QUERY_ERROR_MSG = 'SQL query failed'
+DISABLE_USER_ERROR_MSG = 'Disable user failed'
+SHOW_NETWORK_POLICIES_ERROR_MSG = 'Show network policies failed'
+DESCRIBE_NETWORK_POLICY_ERROR_MSG = 'Describe network policy failed'
+
+# Action success messages
+TEST_CONNECTIVITY_SUCCESS_MSG = 'Test connectivity passed'
+REMOVE_GRANTS_SUCCESS_MSG = 'Role {role} was successfully removed from user'
+UPDATE_NETWORK_POLICY_SUCCESS_MSG = 'Network policy {policy_name} was updated successfully'
+
+# Default error messages
+SNOWFLAKE_ERROR_CODE_UNAVAILABLE = 'Unavailable'
+SNOWFLAKE_ERROR_MSG_UNAVAILABLE = 'Unavailable. Please check the asset configuration and|or the action parameters.'
+
+SNOWFLAKE_TOTAL_ROWS_JSON = 'total_rows'
diff --git a/snowflake_dark.svg b/snowflake_dark.svg
new file mode 100644
index 0000000..49d5305
--- /dev/null
+++ b/snowflake_dark.svg
@@ -0,0 +1,27 @@
+
diff --git a/snowflake_view.py b/snowflake_view.py
new file mode 100644
index 0000000..c7fe5ac
--- /dev/null
+++ b/snowflake_view.py
@@ -0,0 +1,40 @@
+# File: snowflake_view.py
+#
+#
+# Copyright (c) 2023 Splunk Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the License for the specific language governing permissions
+# and limitations under the License.
+def display_query_results(provides, all_results, context):
+
+ context['results'] = results = []
+
+ adjusted_names = dict()
+
+ for summary, action_results in all_results:
+ for result in action_results:
+ headers_set = set()
+ table = dict()
+ table['data'] = rows = []
+ data = result.get_data()
+ if data:
+ headers_set.update(data[0].keys())
+ headers = sorted(headers_set)
+ table['headers'] = headers
+
+ for item in data:
+ row = []
+ for header in headers:
+ row.append({ 'value': item.get(adjusted_names.get(header, header)) })
+ rows.append(row)
+ results.append(table)
+
+ return 'query_results.html'
diff --git a/wheels/py3/PyJWT-2.6.0-py3-none-any.whl b/wheels/py3/PyJWT-2.6.0-py3-none-any.whl
new file mode 100755
index 0000000..1c979e6
Binary files /dev/null and b/wheels/py3/PyJWT-2.6.0-py3-none-any.whl differ
diff --git a/wheels/py3/certifi-2022.12.7-py3-none-any.whl b/wheels/py3/certifi-2022.12.7-py3-none-any.whl
new file mode 100644
index 0000000..a083056
Binary files /dev/null and b/wheels/py3/certifi-2022.12.7-py3-none-any.whl differ
diff --git a/wheels/py3/charset_normalizer-2.1.1-py3-none-any.whl b/wheels/py3/charset_normalizer-2.1.1-py3-none-any.whl
new file mode 100755
index 0000000..cf36f15
Binary files /dev/null and b/wheels/py3/charset_normalizer-2.1.1-py3-none-any.whl differ
diff --git a/wheels/py3/filelock-3.8.2-py3-none-any.whl b/wheels/py3/filelock-3.8.2-py3-none-any.whl
new file mode 100755
index 0000000..b994908
Binary files /dev/null and b/wheels/py3/filelock-3.8.2-py3-none-any.whl differ
diff --git a/wheels/py3/idna-3.4-py3-none-any.whl b/wheels/py3/idna-3.4-py3-none-any.whl
new file mode 100755
index 0000000..7343c68
Binary files /dev/null and b/wheels/py3/idna-3.4-py3-none-any.whl differ
diff --git a/wheels/py3/pyOpenSSL-22.1.0-py3-none-any.whl b/wheels/py3/pyOpenSSL-22.1.0-py3-none-any.whl
new file mode 100755
index 0000000..c212483
Binary files /dev/null and b/wheels/py3/pyOpenSSL-22.1.0-py3-none-any.whl differ
diff --git a/wheels/py3/pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py3/pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl
new file mode 100755
index 0000000..f31bf81
Binary files /dev/null and b/wheels/py3/pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ
diff --git a/wheels/py3/requests-2.28.1-py3-none-any.whl b/wheels/py3/requests-2.28.1-py3-none-any.whl
new file mode 100755
index 0000000..08649f5
Binary files /dev/null and b/wheels/py3/requests-2.28.1-py3-none-any.whl differ
diff --git a/wheels/py3/setuptools-67.1.0-py3-none-any.whl b/wheels/py3/setuptools-67.1.0-py3-none-any.whl
new file mode 100644
index 0000000..949c26b
Binary files /dev/null and b/wheels/py3/setuptools-67.1.0-py3-none-any.whl differ
diff --git a/wheels/py3/typing_extensions-4.4.0-py3-none-any.whl b/wheels/py3/typing_extensions-4.4.0-py3-none-any.whl
new file mode 100755
index 0000000..2fa6abb
Binary files /dev/null and b/wheels/py3/typing_extensions-4.4.0-py3-none-any.whl differ
diff --git a/wheels/py36/cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py36/cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
new file mode 100755
index 0000000..0731e8f
Binary files /dev/null and b/wheels/py36/cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ
diff --git a/wheels/py39/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py39/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
new file mode 100755
index 0000000..2f3e234
Binary files /dev/null and b/wheels/py39/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ
diff --git a/wheels/py39/snowflake_connector_python-2.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py39/snowflake_connector_python-2.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
new file mode 100755
index 0000000..b288b29
Binary files /dev/null and b/wheels/py39/snowflake_connector_python-2.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ
diff --git a/wheels/shared/asn1crypto-1.5.1-py2.py3-none-any.whl b/wheels/shared/asn1crypto-1.5.1-py2.py3-none-any.whl
new file mode 100755
index 0000000..e3eb77b
Binary files /dev/null and b/wheels/shared/asn1crypto-1.5.1-py2.py3-none-any.whl differ
diff --git a/wheels/shared/oscrypto-1.3.0-py2.py3-none-any.whl b/wheels/shared/oscrypto-1.3.0-py2.py3-none-any.whl
new file mode 100755
index 0000000..84a8799
Binary files /dev/null and b/wheels/shared/oscrypto-1.3.0-py2.py3-none-any.whl differ
diff --git a/wheels/shared/pycparser-2.21-py2.py3-none-any.whl b/wheels/shared/pycparser-2.21-py2.py3-none-any.whl
new file mode 100755
index 0000000..fef6735
Binary files /dev/null and b/wheels/shared/pycparser-2.21-py2.py3-none-any.whl differ
diff --git a/wheels/shared/pytz-2022.6-py2.py3-none-any.whl b/wheels/shared/pytz-2022.6-py2.py3-none-any.whl
new file mode 100755
index 0000000..c7a9994
Binary files /dev/null and b/wheels/shared/pytz-2022.6-py2.py3-none-any.whl differ
diff --git a/wheels/shared/urllib3-1.26.13-py2.py3-none-any.whl b/wheels/shared/urllib3-1.26.13-py2.py3-none-any.whl
new file mode 100755
index 0000000..887f782
Binary files /dev/null and b/wheels/shared/urllib3-1.26.13-py2.py3-none-any.whl differ