diff --git a/VERSION b/VERSION index 71172b4..42624f3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.1 \ No newline at end of file +0.10.2 \ No newline at end of file diff --git a/setup.iss b/setup.iss index 31347a8..c60ace1 100644 --- a/setup.iss +++ b/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "Tactical RMM Agent" -#define MyAppVersion "0.10.1" +#define MyAppVersion "0.10.2" #define MyAppPublisher "wh1te909" #define MyAppURL "https://github.com/wh1te909" #define MyAppExeName "tacticalrmm.exe" diff --git a/winagent/agent.py b/winagent/agent.py index af2a666..6e9f463 100644 --- a/winagent/agent.py +++ b/winagent/agent.py @@ -437,6 +437,11 @@ async def event_log_check(self, data): api_fail_when = data["fail_when"] api_search_last_days = int(data["search_last_days"]) + try: + api_event_id_is_wildcard = data["event_id_is_wildcard"] + except KeyError: + api_event_id_is_wildcard = False + if api_search_last_days != 0: start_time = dt.datetime.now() - dt.timedelta(days=api_search_last_days) @@ -502,7 +507,10 @@ async def event_log_check(self, data): "uid": uid, } - if int(evt_id) == api_event_id and evt_type == api_event_type: + if api_event_id_is_wildcard and evt_type == api_event_type: + log.append(event_dict) + + elif int(evt_id) == api_event_id and evt_type == api_event_type: log.append(event_dict) if done: @@ -575,7 +583,26 @@ def get_used_ram(self): return round(psutil.virtual_memory().percent) def get_services(self): - return [svc.as_dict() for svc in psutil.win_service_iter()] + # see https://github.com/wh1te909/tacticalrmm/issues/38 + # for why I am manually implementing the svc.as_dict() method of psutil + ret = [] + for svc in psutil.win_service_iter(): + i = {} + try: + i["display_name"] = svc.display_name() + i["binpath"] = svc.binpath() + i["username"] = svc.username() + i["start_type"] = svc.start_type() + i["status"] = svc.status() + i["pid"] = svc.pid() + i["name"] = svc.name() + i["description"] = svc.description() + except Exception: + continue + else: + ret.append(i) + + return ret def get_total_ram(self): return math.ceil((psutil.virtual_memory().total / 1_073_741_824)) diff --git a/winagent/installer.py b/winagent/installer.py index 99185ab..43f4b47 100644 --- a/winagent/installer.py +++ b/winagent/installer.py @@ -69,7 +69,59 @@ def rand_string(self): chars = string.ascii_letters return "".join(random.choice(chars) for i in range(35)) + def uninstall_salt(self): + print("Stopping salt-minion service", flush=True) + r = subprocess.run( + ["sc", "stop", "salt-minion"], timeout=45, capture_output=True + ) + sleep(15) + + # clean up any hung salt python procs + pids = [] + for proc in psutil.process_iter(): + with proc.oneshot(): + if proc.name() == "python.exe" and "salt" in proc.exe(): + pids.append(proc.pid) + + for pid in pids: + self.logger.debug(f"Killing salt process with pid {pid}") + try: + kill_proc(pid) + except: + continue + + print("Uninstalling existing salt-minion", flush=True) + r = subprocess.run( + ["c:\\salt\\uninst.exe", "/S"], timeout=120, capture_output=True + ) + sleep(20) + + try: + shutil.rmtree("C:\\salt") + sleep(1) + os.system('rmdir /S /Q "{}"'.format("C:\\salt")) + except Exception: + pass + + print("Salt was removed", flush=True) + def install(self): + # check for existing installation and exit if found + try: + tac = psutil.win_service_get("tacticalagent") + except psutil.NoSuchProcess: + pass + else: + print( + "Found tacticalagent service. Please uninstall the existing Tactical Agent first before reinstalling.", + flush=True, + ) + print( + "If you're trying to perform an upgrade, do so from the RMM web interface.", + flush=True, + ) + sys.exit(1) + # generate the agent id try: r = subprocess.run( @@ -82,11 +134,12 @@ def install(self): self.agent_id = f"{wmic_id}|{self.agent_hostname}" self.logger.debug(f"Agent ID: {self.agent_id}") + sys.stdout.flush() # validate the url and get the salt master r = urlparse(self.api_url) if r.scheme != "https" and r.scheme != "http": - print("ERROR: api url must contain https or http") + print("ERROR: api url must contain https or http", flush=True) sys.exit(1) if validators.domain(r.netloc): @@ -98,10 +151,11 @@ def install(self): else: self.salt_master = r.netloc.split(":")[0] else: - print("Error parsing api url, unable to get salt-master") + print("Error parsing api url, unable to get salt-master", flush=True) sys.exit(1) self.logger.debug(f"Salt master is: {self.salt_master}") + sys.stdout.flush() # set the api base url self.api = f"{r.scheme}://{r.netloc}" @@ -115,24 +169,30 @@ def install(self): ) except Exception as e: self.logger.error(e) + sys.stdout.flush() print( - "ERROR: Unable to contact the RMM. Please check your internet connection." + "ERROR: Unable to contact the RMM. Please check your internet connection.", + flush=True, ) sys.exit(1) if r.status_code == 401: - print("ERROR: Token has expired. Please generate a new one from the rmm.") + print( + "ERROR: Token has expired. Please generate a new one from the rmm.", + flush=True, + ) sys.exit(1) elif r.status_code != 200: e = json.loads(r.text)["error"] self.logger.error(e) + sys.stdout.flush() sys.exit(1) else: self.agent_token = json.loads(r.text)["token"] if not self.local_salt: # download salt - print("Downloading salt minion") + print("Downloading salt minion", flush=True) try: r = requests.get( "https://github.com/wh1te909/winagent/raw/master/bin/salt-minion-setup.exe", @@ -141,11 +201,15 @@ def install(self): ) except Exception as e: self.logger.error(e) - print("ERROR: Timed out trying to download the salt-minion") + sys.stdout.flush() + print("ERROR: Timed out trying to download the salt-minion", flush=True) sys.exit(1) if r.status_code != 200: - print("ERROR: Something went wrong while downloading the salt-minion") + print( + "ERROR: Something went wrong while downloading the salt-minion", + flush=True, + ) sys.exit(1) minion = os.path.join(self.programdir, "salt-minion-setup.exe") @@ -162,9 +226,10 @@ def install(self): os.path.join(self.programdir, "salt-minion-setup.exe"), ) except Exception as e: - print(e) + print(e, flush=True) print( - f"\nERROR: unable to copy the file {self.local_salt} to {self.programdir}" + f"\nERROR: unable to copy the file {self.local_salt} to {self.programdir}", + flush=True, ) sys.exit(1) else: @@ -177,11 +242,15 @@ def install(self): r = requests.post(url, headers=self.headers, stream=True, timeout=400) except Exception as e: self.logger.error(e) - print("ERROR: Timed out trying to download the Mesh Agent") + sys.stdout.flush() + print("ERROR: Timed out trying to download the Mesh Agent", flush=True) sys.exit(1) if r.status_code != 200: - print("ERROR: Something went wrong while downloading the Mesh Agent") + print( + "ERROR: Something went wrong while downloading the Mesh Agent", + flush=True, + ) sys.exit(1) mesh = os.path.join(self.programdir, "meshagent.exe") @@ -198,9 +267,10 @@ def install(self): self.local_mesh, os.path.join(self.programdir, "meshagent.exe") ) except Exception as e: - print(e) + print(e, flush=True) print( - f"\nERROR: unable to copy the file {self.local_mesh} to {self.programdir}" + f"\nERROR: unable to copy the file {self.local_mesh} to {self.programdir}", + flush=True, ) sys.exit(1) else: @@ -219,7 +289,7 @@ def install(self): mesh_cleanup_dir = mesh_two_dir if mesh_exists: - print("Found existing Mesh Agent. Removing...") + print("Found existing Mesh Agent. Removing...", flush=True) try: subprocess.run( ["sc", "stop", "mesh agent"], capture_output=True, timeout=30 @@ -248,7 +318,7 @@ def install(self): [mesh, "-fulluninstall"], capture_output=True, timeout=60 ) except: - print("Timed out trying to uninstall existing Mesh Agent") + print("Timed out trying to uninstall existing Mesh Agent", flush=True) if os.path.exists(mesh_cleanup_dir): try: @@ -259,13 +329,13 @@ def install(self): pass # install the mesh agent - print("Installing mesh agent") + print("Installing mesh agent", flush=True) try: ret = subprocess.run( [mesh, "-fullinstall"], capture_output=True, timeout=120 ) except: - print("Timed out trying to install the Mesh Agent") + print("Timed out trying to install the Mesh Agent", flush=True) sleep(15) # meshcentral changed their installation path recently @@ -290,14 +360,16 @@ def install(self): except Exception: mesh_attempts += 1 print( - f"Failed to get mesh node id: attempt {mesh_attempts} of {mesh_retries}" + f"Failed to get mesh node id: attempt {mesh_attempts} of {mesh_retries}", + flush=True, ) sleep(5) else: if "not defined" in mesh_node_id.lower(): mesh_attempts += 1 print( - f"Failed to get mesh node id: attempt {mesh_attempts} of {mesh_retries}" + f"Failed to get mesh node id: attempt {mesh_attempts} of {mesh_retries}", + flush=True, ) sleep(5) else: @@ -312,8 +384,9 @@ def install(self): self.mesh_node_id = mesh_node_id self.logger.debug(f"Mesh node id: {mesh_node_id}") + sys.stdout.flush() - print("Adding agent to dashboard") + print("Adding agent to dashboard", flush=True) url = f"{self.api}/api/v1/add/" payload = { @@ -326,6 +399,7 @@ def install(self): "monitoring_type": self.agent_type, } self.logger.debug(payload) + sys.stdout.flush() try: r = requests.post( @@ -333,10 +407,11 @@ def install(self): ) except Exception as e: self.logger.error(e) + sys.stdout.flush() sys.exit(1) if r.status_code != 200: - print("Error adding agent to dashboard") + print("Error adding agent to dashboard", flush=True) sys.exit(1) self.agent_pk = r.json()["pk"] @@ -355,11 +430,19 @@ def install(self): salt_id=self.salt_id, ).save() except Exception as e: - print(f"Error creating database: {e}") + print(f"Error creating database: {e}", flush=True) sys.exit(1) - # install salt - print("Installing the salt-minion, this might take a while...") + # install salt, remove any existing installations first + try: + oldsalt = psutil.win_service_get("salt-minion") + except psutil.NoSuchProcess: + pass + else: + print("Found existing salt-minion. Removing", flush=True) + self.uninstall_salt() + + print("Installing the salt-minion, this might take a while...", flush=True) salt_cmd = [ "salt-minion-setup.exe", @@ -386,30 +469,32 @@ def install(self): ) except Exception as e: logger.debug(e) + sys.stdout.flush() accept_attempts += 1 sleep(5) else: if r.status_code != 200: accept_attempts += 1 print( - f"Salt-key was not accepted: attempt {accept_attempts} of {salt_retries}" + f"Salt-key was not accepted: attempt {accept_attempts} of {salt_retries}", + flush=True, ) sleep(5) else: accept_attempts = 0 if accept_attempts == 0: - print("Salt-key was accepted!") + print("Salt-key was accepted!", flush=True) break elif accept_attempts > salt_retries: self.accept_success = False break - print("Waiting for salt to sync with the master") + print("Waiting for salt to sync with the master", flush=True) sleep(10) # wait for salt to sync # sync our custom salt modules - print("Syncing custom modules") + print("Syncing custom modules", flush=True) url = f"{self.api}/api/v1/firstinstall/" payload = {"pk": self.agent_pk} sync_attempts = 0 @@ -422,20 +507,22 @@ def install(self): ) except Exception as e: self.logger.debug(e) + sys.stdout.flush() sync_attempts += 1 sleep(5) else: if r.status_code != 200: sync_attempts += 1 print( - f"Syncing modules failed: attempt {sync_attempts} of {sync_retries}" + f"Syncing modules failed: attempt {sync_attempts} of {sync_retries}", + flush=True, ) sleep(5) else: sync_attempts = 0 if sync_attempts == 0: - print("Modules were synced!") + print("Modules were synced!", flush=True) break elif sync_attempts > sync_retries: self.sync_success = False @@ -452,6 +539,7 @@ def install(self): agent.create_fix_mesh_task() except Exception as e: self.logger.debug(e) + sys.stdout.flush() # remove services if they exists try: @@ -459,7 +547,7 @@ def install(self): except psutil.NoSuchProcess: pass else: - print("Found tacticalagent service. Removing...") + print("Found tacticalagent service. Removing...", flush=True) subprocess.run([self.nssm, "stop", "tacticalagent"], capture_output=True) subprocess.run( [self.nssm, "remove", "tacticalagent", "confirm"], capture_output=True @@ -470,14 +558,14 @@ def install(self): except psutil.NoSuchProcess: pass else: - print("Found checkrunner service. Removing...") + print("Found checkrunner service. Removing...", flush=True) subprocess.run([self.nssm, "stop", "checkrunner"], capture_output=True) subprocess.run( [self.nssm, "remove", "checkrunner", "confirm"], capture_output=True ) # install the windows services - print("Installing services...") + print("Installing services...", flush=True) svc_commands = [ [ self.nssm, @@ -519,21 +607,21 @@ def install(self): subprocess.run(cmd, capture_output=True) if self.disable_power: - print("Disabling sleep/hibernate...") + print("Disabling sleep/hibernate...", flush=True) try: disable_sleep_hibernate() except: pass if self.enable_rdp: - print("Enabling RDP...") + print("Enabling RDP...", flush=True) try: enable_rdp() except: pass if self.enable_ping: - print("Enabling ping...") + print("Enabling ping...", flush=True) try: enable_ping() except: @@ -541,27 +629,30 @@ def install(self): # finish up if not self.accept_success: - print("-" * 75) - print("ERROR: The RMM was unable to accept the salt minion.") - print("Salt may not have been properly installed.") - print("Try running the following command on the rmm:") - print(f"sudo salt-key -y -a '{self.salt_id}'") - print("-" * 75) + print("-" * 75, flush=True) + print("ERROR: The RMM was unable to accept the salt minion.", flush=True) + print("Salt may not have been properly installed.", flush=True) + print("Try running the following command on the rmm:", flush=True) + print(f"sudo salt-key -y -a '{self.salt_id}'", flush=True) + print("-" * 75, flush=True) if not self.sync_success: - print("-" * 75) - print("Unable to sync salt modules.") - print("Salt may not have been properly installed.") - print("-" * 75) + print("-" * 75, flush=True) + print("Unable to sync salt modules.", flush=True) + print("Salt may not have been properly installed.", flush=True) + print("-" * 75, flush=True) if not self.mesh_success: - print("-" * 75) - print("The Mesh Agent was not installed properly.") - print("Some features will not work.") - print("-" * 75) + print("-" * 75, flush=True) + print("The Mesh Agent was not installed properly.", flush=True) + print("Some features will not work.", flush=True) + print("-" * 75, flush=True) if self.accept_success and self.sync_success and self.mesh_success: - print("Installation was successfull!") - print("Allow a few minutes for the agent to properly display in the RMM") + print("Installation was successfull!", flush=True) + print( + "Allow a few minutes for the agent to properly display in the RMM", + flush=True, + ) else: - print("*****Installation finished with errors.*****") + print("*****Installation finished with errors.*****", flush=True) diff --git a/winagent/tacticalrmm.py b/winagent/tacticalrmm.py index 2366715..8362386 100644 --- a/winagent/tacticalrmm.py +++ b/winagent/tacticalrmm.py @@ -94,29 +94,41 @@ def main(): if args.local_salt: if not os.path.exists(args.local_salt): parser.print_help() - print(f"\nError: {args.local_salt} does not exist\n") + sys.stdout.flush() + print(f"\nError: {args.local_salt} does not exist\n", flush=True) sys.exit(1) if not os.path.isfile(args.local_salt): parser.print_help() - print(f"\nError: {args.local_salt} must be a file, not a folder.") + sys.stdout.flush() print( - r'Make sure to use double backslashes for file paths, and double quotes e.g. "C:\\temp\\salt-minion-setup.exe"' + f"\nError: {args.local_salt} must be a file, not a folder.", + flush=True, ) - print("") + print( + r'Make sure to use double backslashes for file paths, and double quotes e.g. "C:\\temp\\salt-minion-setup.exe"', + flush=True, + ) + print("", flush=True) sys.exit(1) if args.local_mesh: if not os.path.exists(args.local_mesh): parser.print_help() - print(f"\nError: {args.local_mesh} does not exist\n") + sys.stdout.flush() + print(f"\nError: {args.local_mesh} does not exist\n", flush=True) sys.exit(1) if not os.path.isfile(args.local_mesh): parser.print_help() - print(f"\nError: {args.local_mesh} must be a file, not a folder.") + sys.stdout.flush() + print( + f"\nError: {args.local_mesh} must be a file, not a folder.", + flush=True, + ) print( - r'Make sure to use double backslashes for file paths, and double quotes e.g. "C:\\temp\\meshagent.exe"' + r'Make sure to use double backslashes for file paths, and double quotes e.g. "C:\\temp\\meshagent.exe"', + flush=True, ) - print("") + print("", flush=True) sys.exit(1) from installer import Installer