diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a6e014..544174e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,10 @@ repos: - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v1.16 + rev: v1.17 hooks: - id: org-hook - id: package-app-dependencies + args: ["-d", "./Dockerfile.wheels"] - repo: https://github.com/Yelp/detect-secrets rev: v1.4.0 hooks: diff --git a/Dockerfile.wheels b/Dockerfile.wheels new file mode 100644 index 0000000..7649a6c --- /dev/null +++ b/Dockerfile.wheels @@ -0,0 +1,2 @@ +FROM quay.io/pypa/manylinux2014_x86_64 +RUN yum install krb5-devel krb5-workstation -y diff --git a/LICENSE b/LICENSE index f377280..3c1cbb5 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ 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. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index fe00c35..96ac528 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,33 @@ or some other command which you want to start but don't care for the output, the **async** parameter. After the command starts, it will return a **command_id** and **shell_id** , which you can optionally use to retrieve the output of that command at a later time. +### Certificate Authentication + +To authenticate using SSL certificates, select `certificate` authentication in asset configuration method and pass following configuration parameters. + +* cert_pem_path - A path to signed certificate file that is trusted by the Windows instance, in PEM format + +* cert_key_pem_path - A filepath to key used to generate cert_pem file + +* ca_trust_path - The certificate of the certificate authority that signed cert_file. It's needed only when you set up your own certificate authority. + +It is recommended that these files be placed under the /etc/ssl/ directory. These files must be readable by the phantom-worker user. + +### Kerberos Authentication + +To authenticate using Kerberos, select `kerberos` authentication in asset configuration and provide hostname and username used for authorization. +You'll also need to setup your instance to support Kerberos: + +- Kerberos packages have to be installed: + - for Debian/Ubuntu/etc: `sudo apt-get install krb5-user` + - for RHEL/CentOS/etc: `sudo yum install krb5-workstation krb5-libs krb5-auth-dialog` + +- `/etc/krb5.conf` needs to be properly configured for your realm and kdc +- If there is no DNS configuration, `hosts` file will need to have mappings for server with mssccm under same domain as on Windows server +- `kinit` must be run for principal that will be used to connect to msccm +- It should be noted that Kerberos tickets will expire, so it is recommended to use a script to + run `kinit` periodically to refresh the ticket for the user, alternatively `keytab` file can be created on server and used on client for connectivity. + ### Configuration Variables The below configuration variables are required for this Connector to operate. These variables are specified when configuring a Windows Remote Management asset in SOAR. @@ -144,6 +171,9 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION **username** | required | string | Username **password** | required | password | Password **transport** | required | string | Type of transport to use +**cert_pem_path** | optional | string | Path to SSL certificate PEM file +**cert_key_pem_path** | optional | string | Path to SSL key file +**ca_trust_path** | optional | string | Path to trusted CRT file ### Supported Actions [test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity using supplied configuration diff --git a/manual_readme_content.md b/manual_readme_content.md index 33117a0..e815137 100644 --- a/manual_readme_content.md +++ b/manual_readme_content.md @@ -117,3 +117,30 @@ default, the app will wait for these actions to finish. In the case of starting or some other command which you want to start but don't care for the output, then you can check the **async** parameter. After the command starts, it will return a **command_id** and **shell_id** , which you can optionally use to retrieve the output of that command at a later time. + +### Certificate Authentication + +To authenticate using SSL certificates, select `certificate` authentication in asset configuration method and pass following configuration parameters. + +* cert_pem_path - A path to signed certificate file that is trusted by the Windows instance, in PEM format + +* cert_key_pem_path - A filepath to key used to generate cert_pem file + +* ca_trust_path - The certificate of the certificate authority that signed cert_file. It's needed only when you set up your own certificate authority. + +It is recommended that these files be placed under the /etc/ssl/ directory. These files must be readable by the phantom-worker user. + +### Kerberos Authentication + +To authenticate using Kerberos, select `kerberos` authentication in asset configuration and provide hostname and username used for authorization. +You'll also need to setup your instance to support Kerberos: + +- Kerberos packages have to be installed: + - for Debian/Ubuntu/etc: `sudo apt-get install krb5-user` + - for RHEL/CentOS/etc: `sudo yum install krb5-workstation krb5-libs krb5-auth-dialog` + +- `/etc/krb5.conf` needs to be properly configured for your realm and kdc +- If there is no DNS configuration, `hosts` file will need to have mappings for server with mssccm under same domain as on Windows server +- `kinit` must be run for principal that will be used to connect to msccm +- It should be noted that Kerberos tickets will expire, so it is recommended to use a script to + run `kinit` periodically to refresh the ticket for the user, alternatively `keytab` file can be created on server and used on client for connectivity. diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index fbcb2fd..cb58c68 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1 +1,4 @@ **Unreleased** + +* [PAPP-32933] Kerberos and Certificate authentication support. + * User can now select Certificate and Kerberos as transport methods diff --git a/requirements.txt b/requirements.txt index f05e3a8..ebba285 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ beautifulsoup4==4.9.1 +pykerberos==1.2.4 pywinrm==0.4.3 # Pinned to the current platform version requests==2.31.0 diff --git a/wheels/py3/charset_normalizer-2.0.12-py3-none-any.whl b/wheels/py3/charset_normalizer-2.0.12-py3-none-any.whl new file mode 100644 index 0000000..17a2dfb Binary files /dev/null and b/wheels/py3/charset_normalizer-2.0.12-py3-none-any.whl differ diff --git a/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl b/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl new file mode 100644 index 0000000..b363a9b Binary files /dev/null and b/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl differ diff --git a/wheels/py3/urllib3-2.2.1-py3-none-any.whl b/wheels/py3/urllib3-2.2.1-py3-none-any.whl new file mode 100644 index 0000000..d7cca6a Binary files /dev/null and b/wheels/py3/urllib3-2.2.1-py3-none-any.whl differ diff --git a/wheels/py36/cffi-1.15.1-cp36-cp36m-manylinux1_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py36/cffi-1.15.1-cp36-cp36m-manylinux1_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..cd555e4 Binary files /dev/null and b/wheels/py36/cffi-1.15.1-cp36-cp36m-manylinux1_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/wheels/py36/cryptography-40.0.2-cp36-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py36/cryptography-40.0.2-cp36-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..a0d4277 Binary files /dev/null and b/wheels/py36/cryptography-40.0.2-cp36-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/py36/pykerberos-1.2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py36/pykerberos-1.2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..9072496 Binary files /dev/null and b/wheels/py36/pykerberos-1.2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/wheels/py39/charset_normalizer-3.3.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py39/charset_normalizer-3.3.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl index b11229e..3559f21 100644 Binary files a/wheels/py39/charset_normalizer-3.3.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl and b/wheels/py39/charset_normalizer-3.3.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/py39/cryptography-42.0.4-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py39/cryptography-42.0.4-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..4f3952b Binary files /dev/null and b/wheels/py39/cryptography-42.0.4-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/py39/pykerberos-1.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/py39/pykerberos-1.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..3799d1d Binary files /dev/null and b/wheels/py39/pykerberos-1.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/wheels/shared/ntlm_auth-1.5.0-py2.py3-none-any.whl b/wheels/shared/ntlm_auth-1.5.0-py2.py3-none-any.whl new file mode 100644 index 0000000..6083118 Binary files /dev/null and b/wheels/shared/ntlm_auth-1.5.0-py2.py3-none-any.whl differ diff --git a/wheels/shared/requests-2.27.1-py2.py3-none-any.whl b/wheels/shared/requests-2.27.1-py2.py3-none-any.whl new file mode 100644 index 0000000..807fc61 Binary files /dev/null and b/wheels/shared/requests-2.27.1-py2.py3-none-any.whl differ diff --git a/wheels/shared/requests_ntlm-1.1.0-py2.py3-none-any.whl b/wheels/shared/requests_ntlm-1.1.0-py2.py3-none-any.whl new file mode 100644 index 0000000..5f97789 Binary files /dev/null and b/wheels/shared/requests_ntlm-1.1.0-py2.py3-none-any.whl differ diff --git a/winrm.json b/winrm.json index 589c061..07b92ba 100644 --- a/winrm.json +++ b/winrm.json @@ -72,10 +72,30 @@ "required": true, "value_list": [ "basic", - "ntlm" + "ntlm", + "certificate", + "kerberos" ], "default": "basic", "order": 7 + }, + "cert_pem_path": { + "description": "Path to SSL certificate PEM file", + "data_type": "string", + "required": false, + "order": 8 + }, + "cert_key_pem_path": { + "description": "Path to SSL key file", + "data_type": "string", + "required": false, + "order": 9 + }, + "ca_trust_path": { + "description": "Path to trusted CRT file", + "data_type": "string", + "required": false, + "order": 10 } }, "actions": [ @@ -3321,7 +3341,7 @@ }, { "module": "cryptography", - "input_file": "wheels/py39/cryptography-42.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + "input_file": "wheels/py39/cryptography-42.0.4-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" }, { "module": "idna", @@ -3331,6 +3351,10 @@ "module": "pycparser", "input_file": "wheels/shared/pycparser-2.21-py2.py3-none-any.whl" }, + { + "module": "pykerberos", + "input_file": "wheels/py39/pykerberos-1.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, { "module": "pyspnego", "input_file": "wheels/py3/pyspnego-0.10.2-py3-none-any.whl" @@ -3355,6 +3379,74 @@ "module": "soupsieve", "input_file": "wheels/py3/soupsieve-2.5-py3-none-any.whl" }, + { + "module": "urllib3", + "input_file": "wheels/py3/urllib3-2.2.1-py3-none-any.whl" + }, + { + "module": "xmltodict", + "input_file": "wheels/shared/xmltodict-0.13.0-py2.py3-none-any.whl" + } + ] + }, + "pip_dependencies": { + "wheel": [ + { + "module": "beautifulsoup4", + "input_file": "wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl" + }, + { + "module": "certifi", + "input_file": "wheels/py3/certifi-2024.2.2-py3-none-any.whl" + }, + { + "module": "cffi", + "input_file": "wheels/py36/cffi-1.15.1-cp36-cp36m-manylinux1_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "module": "charset_normalizer", + "input_file": "wheels/py3/charset_normalizer-2.0.12-py3-none-any.whl" + }, + { + "module": "cryptography", + "input_file": "wheels/py36/cryptography-40.0.2-cp36-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + { + "module": "idna", + "input_file": "wheels/py3/idna-3.6-py3-none-any.whl" + }, + { + "module": "ntlm_auth", + "input_file": "wheels/shared/ntlm_auth-1.5.0-py2.py3-none-any.whl" + }, + { + "module": "pycparser", + "input_file": "wheels/shared/pycparser-2.21-py2.py3-none-any.whl" + }, + { + "module": "pykerberos", + "input_file": "wheels/py36/pykerberos-1.2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "module": "pywinrm", + "input_file": "wheels/shared/pywinrm-0.4.3-py2.py3-none-any.whl" + }, + { + "module": "requests", + "input_file": "wheels/shared/requests-2.27.1-py2.py3-none-any.whl" + }, + { + "module": "requests_ntlm", + "input_file": "wheels/shared/requests_ntlm-1.1.0-py2.py3-none-any.whl" + }, + { + "module": "six", + "input_file": "wheels/shared/six-1.16.0-py2.py3-none-any.whl" + }, + { + "module": "soupsieve", + "input_file": "wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl" + }, { "module": "urllib3", "input_file": "wheels/shared/urllib3-1.26.18-py2.py3-none-any.whl" @@ -3365,4 +3457,4 @@ } ] } -} \ No newline at end of file +} diff --git a/winrm_connector.py b/winrm_connector.py index e1b99a5..c592963 100644 --- a/winrm_connector.py +++ b/winrm_connector.py @@ -37,6 +37,7 @@ from bs4 import UnicodeDammit from phantom.action_result import ActionResult from phantom.base_connector import BaseConnector +from phantom_common.install_info import is_fips_enabled # Local imports import parse_callbacks as pc @@ -187,6 +188,11 @@ def _sanitize_string(self, string): # break any double quotes which are found, then we break any $, which is used to declare variables return string.replace('`', '``').replace('"', '`"').replace('$', '`$').replace('&', '`&').replace(')', '`)').replace('(', '`(') + def _get_fips_enabled(self): + fips_enabled = is_fips_enabled() + self.debug_print(f'FIPS is enabled: {fips_enabled}') + return fips_enabled + def _create_ps_script(self, action_result, args, whitelist_args=set(), cmd_prefix="", cmd_suffix=""): # Here, you can pass it something like {"val1": "value"} which will generate a string for "-val1 value" # "For your convenience" you can also pass it a list of strings and dicts, something like [val1, {"val2": "asdf"}, foo], @@ -224,19 +230,19 @@ def _create_ps_script(self, action_result, args, whitelist_args=set(), cmd_prefi def _init_session(self, action_result, param=None): config = self.get_config() - default_protocol = config.get('default_protocol', 'http') + default_protocol = config.get(consts.WINRM_CONFIG_PROTOCOL, 'http') ret_val, default_port = self._validate_integer( action_result, - config.get('default_port', 5985 if default_protocol == 'http' else 5986), + config.get(consts.WINRM_CONFIG_PORT, 5985 if default_protocol == 'http' else 5986), "Default port", True) if phantom.is_fail(ret_val): return action_result.get_status() + endpoint = self._handle_py_ver_compat_for_input_str(config.get(consts.WINRM_CONFIG_ENDPOINT)) if param: - endpoint = self._handle_py_ver_compat_for_input_str(param.get('ip_hostname', config.get('endpoint'))) - else: - endpoint = self._handle_py_ver_compat_for_input_str(config.get('endpoint')) + endpoint = self._handle_py_ver_compat_for_input_str(param.get('ip_hostname', endpoint)) + if endpoint is None: return action_result.set_status( phantom.APP_ERROR, "No Endpoint Configured" @@ -245,12 +251,16 @@ def _init_session(self, action_result, param=None): endpoint = '{0}://{1}'.format(default_protocol, endpoint) if re.search(r':\d+$', endpoint, re.UNICODE | re.IGNORECASE) is None: endpoint = '{0}:{1}'.format(endpoint, default_port) - username = config['username'] - password = config['password'] - transport = config.get('transport') - domain = self._handle_py_ver_compat_for_input_str(config.get('domain')) + username = config[consts.WINRM_CONFIG_USERNAME] + password = config[consts.WINRM_CONFIG_PASSWORD] + transport = config.get(consts.WINRM_CONFIG_TRANSPORT) + domain = self._handle_py_ver_compat_for_input_str(config.get(consts.WINRM_CONFIG_DOMAIN)) verify_bool = config.get(phantom.APP_JSON_VERIFY, False) + cert_pem_path = None + cert_key_pem_path = None + cert_ca_trust_path = config.get(consts.WINRM_CONFIG_CA_TRUST, "legacy_requests") + if verify_bool: verify = 'validate' else: @@ -262,12 +272,19 @@ def _init_session(self, action_result, param=None): "Warning: Domain is set but transport type is set to 'basic'" ) elif transport == 'ntlm': + if self._get_fips_enabled(): + return action_result.set_status( + phantom.APP_ERROR, "This transport type is not supported when FIPS is enabled" + ) if domain: username = r'{}\{}'.format(domain, username) elif transport == 'kerberos': - return action_result.set_status( - phantom.APP_ERROR, "This transport type is not yet implemented" - ) + if domain: + username = '{}@{}'.format(username, domain) + elif transport == 'certificate': + username = r'{}\{}'.format(domain, username) + cert_pem_path = config.get(consts.WINRM_CONFIG_CERT_PEM) + cert_key_pem_path = config.get(consts.WINRM_CONFIG_CERT_KEY_PEM) elif transport == 'credssp': return action_result.set_status( phantom.APP_ERROR, "This transport type is not yet implemented" @@ -281,7 +298,10 @@ def _init_session(self, action_result, param=None): endpoint, auth=(username, password), server_cert_validation=verify, - transport=transport + transport=transport, + cert_pem=cert_pem_path, + cert_key_pem=cert_key_pem_path, + ca_trust_path=cert_ca_trust_path ) self._protocol = self._session.protocol diff --git a/winrm_consts.py b/winrm_consts.py index d4a34f5..6987a6b 100644 --- a/winrm_consts.py +++ b/winrm_consts.py @@ -108,3 +108,16 @@ LOCATION_VALUE_LIST = ["local", "domain", "effective"] DENY_ALLOW_VALUE_LIST = ["deny", "allow"] VALUE_LIST_VALIDATION_MESSAGE = "Please provide valid input from {} in '{}' action parameter" + +# Config keys + +WINRM_CONFIG_ENDPOINT = "endpoint" +WINRM_CONFIG_PROTOCOL = "default_protocol" +WINRM_CONFIG_PORT = "default_port" +WINRM_CONFIG_USERNAME = "username" +WINRM_CONFIG_PASSWORD = "password" +WINRM_CONFIG_TRANSPORT = "transport" +WINRM_CONFIG_DOMAIN = "domain" +WINRM_CONFIG_CERT_PEM = "cert_pem_path" +WINRM_CONFIG_CERT_KEY_PEM = "cert_key_pem_path" +WINRM_CONFIG_CA_TRUST = "ca_trust_path"