diff --git a/deployability/deps/requirements.txt b/deployability/deps/requirements.txt index 1e4478b9ec..5213aad50d 100755 --- a/deployability/deps/requirements.txt +++ b/deployability/deps/requirements.txt @@ -12,3 +12,4 @@ pytest==7.4.4 paramiko==3.4.0 requests==2.31.0 chardet==5.2.0 +pywinrm==0.3.0 diff --git a/deployability/modules/allocation/allocation.py b/deployability/modules/allocation/allocation.py index 46158d6896..802da474b6 100755 --- a/deployability/modules/allocation/allocation.py +++ b/deployability/modules/allocation/allocation.py @@ -3,6 +3,8 @@ # This program is a free software; you can redistribute it and/or modify it under the terms of GPLv2 import yaml +import paramiko, logging, time +import winrm from pathlib import Path @@ -58,8 +60,18 @@ def __create(cls, payload: models.CreationPayload): instance.start() logger.info(f"Instance {instance.identifier} started.") # Generate the inventory and track files. - cls.__generate_inventory(instance, payload.inventory_output) - cls.__generate_track_file(instance, payload.provider, payload.track_output) + inventory = cls.__generate_inventory(instance, payload.inventory_output) + # Validate connection + check_connection = cls.__check_connection(inventory) + track_file = cls.__generate_track_file(instance, payload.provider, payload.track_output) + if check_connection is False: + if payload.rollback: + logger.warning(f"Rolling back instance creation.") + track_payload = {'track_output': track_file} + cls.__delete(track_payload) + logger.info(f"Instance {instance.identifier} deleted.") + else: + logger.warning(f'The VM will not be automatically removed. Please remove it executing Allocation module with --action delete.') @classmethod def __delete(cls, payload: models.InstancePayload) -> None: @@ -138,6 +150,7 @@ def __generate_inventory(instance: Instance, inventory_path: Path) -> None: with open(inventory_path, 'w') as f: yaml.dump(inventory.model_dump(exclude_none=True), f) logger.info(f"Inventory file generated at {inventory_path}") + return inventory @staticmethod def __generate_track_file(instance: Instance, provider_name: str, track_path: Path) -> None: @@ -172,3 +185,68 @@ def __generate_track_file(instance: Instance, provider_name: str, track_path: P if Path(str(instance.path) + "/ppc-key").exists(): Path(str(instance.path) + "/ppc-key").unlink() logger.info(f"Track file generated at {track_path}") + return track_path + + @staticmethod + def __check_connection(inventory: models.InventoryOutput, attempts=30, sleep=30) -> None: + """ + Checks if the ssh connection is successful. + + Args: + inventory (InventoryOutput): The inventory object. + attempts (int): The number of attempts to try the connection. + sleep (int): The time to wait between attempts. + + """ + + for attempt in range(1, attempts + 1): + if inventory.ansible_connection == 'winrm': + if inventory.ansible_port == 5986: + protocol = 'https' + else: + protocol = 'http' + endpoint_url = f'{protocol}://{inventory.ansible_host}:{inventory.ansible_port}' + try: + session = winrm.Session(endpoint_url, auth=(inventory.ansible_user, inventory.ansible_password),transport='ntlm', server_cert_validation='ignore') + cmd = session.run_cmd('ipconfig') + if cmd.status_code == 0: + logger.info("WinRM connection successful.") + return True + else: + logger.error(f'WinRM connection failed. Check the credentials in the inventory file.') + return False + except Exception as e: + logger.warning(f'Error on attempt {attempt} of {attempts}: {e}') + time.sleep(sleep) + else: + ssh = paramiko.SSHClient() + paramiko.util.get_logger("paramiko").setLevel(logging.WARNING) + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if inventory.ansible_ssh_private_key_file: + ssh_parameters = { + 'hostname': inventory.ansible_host, + 'port': inventory.ansible_port, + 'username': inventory.ansible_user, + 'key_filename': inventory.ansible_ssh_private_key_file + } + else: + ssh_parameters = { + 'hostname': inventory.ansible_host, + 'port': inventory.ansible_port, + 'username': inventory.ansible_user, + 'password': inventory.ansible_password + } + try: + ssh.connect(**ssh_parameters) + logger.info("SSH connection successful.") + ssh.close() + return True + except paramiko.AuthenticationException: + logger.error(f'Authentication error. Check the credentials in the inventory file.') + return False + except Exception as e: + logger.warning(f'Error on attempt {attempt} of {attempts}: {e}') + time.sleep(sleep) + + logger.error(f'Connection attempts failed after {attempts} tries. Connection timeout.') + return False diff --git a/deployability/modules/allocation/aws/helpers/windowsUserData.ps1 b/deployability/modules/allocation/aws/helpers/windowsUserData.ps1 index 18d18a20a0..f0fc48086b 100644 --- a/deployability/modules/allocation/aws/helpers/windowsUserData.ps1 +++ b/deployability/modules/allocation/aws/helpers/windowsUserData.ps1 @@ -4,20 +4,59 @@ try { + Write-Output "Executing winrm quickconfig" + if (-not ([Net.ServicePointManager]::SecurityProtocol -band [Net.SecurityProtocolType]::Tls12)) { + Write-Output "Enabling TLS 1.2" + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + } + # Check if WinRM HTTPS listener is configured + $httpsListener = Get-Item -Path WSMan:\LocalHost\Listener\* | Where-Object { $_.Keys -contains 'Transport' -and $_.Transport -eq 'HTTPS' } + + if ($httpsListener) { + # Remove existing HTTPS listener + Write-Output "Removing existing HTTPS listener." + Remove-Item -Path WSMan:\LocalHost\Listener\$($httpsListener.Name) -Force + } + # Create a self-signed certificate + $cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME + + # Enable PSRemoting and set up HTTPS listener + Enable-PSRemoting -SkipNetworkProfileCheck -Force + New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint -Force + + # Add firewall rule for WinRM HTTPS + New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP $url = "https://raw.githubusercontent.com/ansible/ansible/6e325d9e4dbdc020eb520a81148866d988a5dbc5/examples/scripts/ConfigureRemotingForAnsible.ps1" $file = "$env:temp\ConfigureRemotingForAnsible.ps1" (New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file) powershell.exe -ExecutionPolicy ByPass -File $file + Write-Output "WinRM enabled on HTTPS." } catch { $_.Exception.Message "Error enabling WinRM on HTTPS." + Write-Output "Error enabling WinRM on HTTPS." +} +# Check if wazuh-user user exists +if (-not (Get-LocalUser -Name "wazuh-user" -ErrorAction SilentlyContinue)) { + # Create wazuh-user user + Write-Output "Creating wazuh-user user" + $password = ConvertTo-SecureString "ChangeMe" -AsPlainText -Force + New-LocalUser "wazuh-user" -Password $password -FullName "wazuh-user" -Description "wazuh-user user for remote desktop" + + Write-Output "Adding wazuh-user user to RDP group." + # Add wazuh-user to Remote Desktop Users group + Add-LocalGroupMember -Group "Remote Desktop Users" -Member "wazuh-user" + + Write-Output "Adding wazuh-user user to Administrators group." + # Add wazuh-user to wazuh-users group + Add-LocalGroupMember -Group "Administrators" -Member "wazuh-user" +} else { + Write-Output "wazuh-user user already exists." + # Set the password for the wazuh-user account + $admin = [ADSI]"WinNT://./wazuh-user, user" + $password = "ChangeMe" + $admin.SetPassword($password) + $admin.SetInfo() + Write-Output "wazuh-user password changed successfully." } -New-LocalUser "Administrator" -Password ChangeMe -FullName "Administrator" -Description "Administrator user for remote desktop" -Add-LocalGroupMember -Group "Remote Desktop Users" -Member "Administrator" -# Set Administrator user to administrator group -Add-LocalGroupMember -Group "Administrators" -Member "Administrator" -# Set the password for the Administrator account -$admin = [ADSI]"WinNT://./Administrator, user" -$admin.SetPassword("ChangeMe") -$admin.SetInfo() diff --git a/deployability/modules/allocation/aws/instance.py b/deployability/modules/allocation/aws/instance.py index e4978d1048..f02cfbd710 100644 --- a/deployability/modules/allocation/aws/instance.py +++ b/deployability/modules/allocation/aws/instance.py @@ -77,7 +77,7 @@ def ssh_connection_info(self) -> ConnectionInfo: if self.platform == 'windows': return ConnectionInfo(hostname=self._instance.public_dns_name, user=self._user, - port=3389, + port=5986, password=str(self.credentials.name)) else: return ConnectionInfo(hostname=self._instance.public_dns_name, diff --git a/deployability/modules/allocation/aws/provider.py b/deployability/modules/allocation/aws/provider.py index 50d49bcc59..9aaf277565 100644 --- a/deployability/modules/allocation/aws/provider.py +++ b/deployability/modules/allocation/aws/provider.py @@ -119,10 +119,11 @@ def _create_instance(cls, base_dir: Path, params: CreationPayload, config: AWSCo instance_dir = Path(base_dir, instance_id) logger.debug(f"Renaming temp {temp_dir} directory to {instance_dir}") os.rename(temp_dir, instance_dir) - if not ssh_key: - credentials.key_path = (instance_dir / credentials.name) - else: - credentials.key_path = (os.path.splitext(ssh_key)[0]) + if platform != "windows": + if not ssh_key: + credentials.key_path = (instance_dir / credentials.name) + else: + credentials.key_path = (os.path.splitext(ssh_key)[0]) instance_params = {} instance_params['instance_dir'] = instance_dir @@ -159,8 +160,9 @@ def _destroy_instance(cls, destroy_parameters: InstancePayload) -> None: destroy_parameters (InstancePayload): The parameters for destroying the instance. """ credentials = AWSCredentials() - key_id = os.path.basename(destroy_parameters.key_path) - credentials.load(key_id) + if destroy_parameters.platform != 'windows': + key_id = os.path.basename(destroy_parameters.key_path) + credentials.load(key_id) instance_params = {} instance_params['instance_dir'] = destroy_parameters.instance_dir instance_params['identifier'] = destroy_parameters.identifier @@ -168,7 +170,7 @@ def _destroy_instance(cls, destroy_parameters: InstancePayload) -> None: instance_params['host_identifier'] = destroy_parameters.host_identifier instance = AWSInstance(InstancePayload(**instance_params), credentials) - if os.path.dirname(destroy_parameters.key_path) == str(destroy_parameters.instance_dir): + if os.path.dirname(destroy_parameters.key_path) == str(destroy_parameters.instance_dir) and destroy_parameters.platform != 'windows': logger.debug(f"Deleting credentials: {instance.credentials.name}") instance.credentials.delete() instance.delete() diff --git a/deployability/modules/allocation/generic/models.py b/deployability/modules/allocation/generic/models.py index 807ef6e576..fc5274a558 100644 --- a/deployability/modules/allocation/generic/models.py +++ b/deployability/modules/allocation/generic/models.py @@ -59,6 +59,7 @@ class InputPayload(BaseModel): label_team: str | None = None label_termination_date: str | None = None instance_name: str | None = None + rollback: bool class CreationPayload(InputPayload): provider: str diff --git a/deployability/modules/allocation/main.py b/deployability/modules/allocation/main.py index ddb4dfb8e8..1f92ff9377 100755 --- a/deployability/modules/allocation/main.py +++ b/deployability/modules/allocation/main.py @@ -27,6 +27,7 @@ def parse_arguments(): parser.add_argument("--label-team", required=False, default=None) parser.add_argument("--label-termination-date", required=False, default=None) parser.add_argument("--instance-name", required=False, default=None) + parser.add_argument("--rollback", choices=['True', 'False'], required=False, default=True) return parser.parse_args() diff --git a/deployability/modules/allocation/static/specs/os.yml b/deployability/modules/allocation/static/specs/os.yml index b0cd4e2063..aadd5fc5dc 100644 --- a/deployability/modules/allocation/static/specs/os.yml +++ b/deployability/modules/allocation/static/specs/os.yml @@ -367,24 +367,20 @@ aws: windows-desktop-10-amd64: ami: ami-0a747df120215911a zone: us-east-1 - user: Administrator - windows-desktop-11-amd64: - ami: ami-09d8cef159442d5b0 - zone: us-east-1 - user: Jenkins + user: wazuh-user windows-server-2012r2-amd64: - ami: ami-09a3ef2bc0c6a252f + ami: ami-05710c71113d5a40e zone: us-east-1 - user: Administrator + user: wazuh-user windows-server-2016-amd64: - ami: ami-04d7825822fe66af3 + ami: ami-0f279f6c0764bef8f zone: us-east-1 - user: Administrator + user: wazuh-user windows-server-2019-amd64: ami: ami-06cc514f1012a7431 zone: us-east-1 - user: Administrator + user: wazuh-user windows-server-2022-amd64: ami: ami-0f9c44e98edf38a2b zone: us-east-1 - user: Administrator + user: wazuh-user diff --git a/deployability/modules/allocation/vagrant/instance.py b/deployability/modules/allocation/vagrant/instance.py index d0a9deb4ad..f5bb5bfedc 100755 --- a/deployability/modules/allocation/vagrant/instance.py +++ b/deployability/modules/allocation/vagrant/instance.py @@ -182,7 +182,7 @@ def ssh_connection_info(self) -> ConnectionInfo: if match and key == 'hostname': ip = match.group(1).strip() ssh_config['hostname'] = ip - ssh_config['port'] = 3389 + ssh_config['port'] = 5985 ssh_config['user'] = 'vagrant' ssh_config['password'] = 'vagrant' else: