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%} + + {% endfor %} + + + + {% for row in result.data %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
{{ header }}
{{ cell.value }}
+
+
+ {% 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 NAMETRANSPORT PROTOCOLPORT
httpstcp443
+

+

+ 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 @@ + + logo-blue-svg + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + logo-blue-svg + + + + + + + + + + + + + + + + + + + + + + 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