feat: implement async command execution, improve client status tracking, and expand takeleap diagnostic commands.

This commit is contained in:
2026-06-04 15:48:38 +05:30
parent c4a96adfc1
commit 25e9b5e120
3 changed files with 199 additions and 39 deletions

View File

@@ -123,8 +123,25 @@ def append_to_logs(log_type: str, client_id: str, detail: Any):
except Exception as e: except Exception as e:
print(f"[!] Error writing log to MongoDB: {e}") print(f"[!] Error writing log to MongoDB: {e}")
def mark_client_active(client_id: str, clients: Dict[str, Any]):
if client_id in clients:
was_active = clients[client_id].get("active", False)
if not was_active:
if client_id in last_down_notification_times:
send_rocketchat_notification(
text=f"✅ **System UP:** Client `{client_id}` has recovered and is back online.",
color="#2ecc71"
)
last_down_notification_times.pop(client_id, None)
clients[client_id]["active"] = True
clients[client_id]["last_seen"] = datetime.now().isoformat()
def load_clients() -> Dict[str, Any]: def load_clients() -> Dict[str, Any]:
try: try:
# Clean up any ghost clients with empty or whitespace-only keys
clients_collection.delete_many({"_id": {"$in": ["", None]}})
clients_collection.delete_many({"_id": {"$regex": "^\\s*$"}})
# If collection is empty, seed it with default registry # If collection is empty, seed it with default registry
if clients_collection.count_documents({}) == 0: if clients_collection.count_documents({}) == 0:
default_data = { default_data = {
@@ -248,6 +265,9 @@ async def get_command(client_id: str):
Endpoint for clients to poll for pending commands. Endpoint for clients to poll for pending commands.
Client Agent hits this endpoint to ask: "Do I have any work to do?" Client Agent hits this endpoint to ask: "Do I have any work to do?"
""" """
if not client_id or not client_id.strip():
raise HTTPException(status_code=400, detail="client_id cannot be empty")
clients = load_clients() clients = load_clients()
# 1. Auto-register new client if we haven't seen it yet # 1. Auto-register new client if we haven't seen it yet
@@ -264,8 +284,7 @@ async def get_command(client_id: str):
# 3. Reset the command to "none", mark active, and update last_seen timestamp # 3. Reset the command to "none", mark active, and update last_seen timestamp
clients[client_id]["pending_command"] = "none" clients[client_id]["pending_command"] = "none"
clients[client_id]["active"] = True mark_client_active(client_id, clients)
clients[client_id]["last_seen"] = datetime.now().isoformat()
save_clients(clients) save_clients(clients)
return {"command": cmd} return {"command": cmd}
@@ -297,6 +316,9 @@ async def receive_telemetry(client_id: str, payload: TelemetryPayload):
""" """
Endpoint for agents to push their live hardware telemetry. Endpoint for agents to push their live hardware telemetry.
""" """
if not client_id or not client_id.strip():
raise HTTPException(status_code=400, detail="client_id cannot be empty")
clients = load_clients() clients = load_clients()
# Auto-register new client if we see it through telemetry first # Auto-register new client if we see it through telemetry first
@@ -305,8 +327,7 @@ async def receive_telemetry(client_id: str, payload: TelemetryPayload):
clients[client_id] = {"pending_command": "none", "last_seen": None, "active": True} clients[client_id] = {"pending_command": "none", "last_seen": None, "active": True}
# Update last_seen timestamp, active status, and store the current telemetry details # Update last_seen timestamp, active status, and store the current telemetry details
clients[client_id]["last_seen"] = datetime.now().isoformat() mark_client_active(client_id, clients)
clients[client_id]["active"] = True
clients[client_id]["telemetry"] = payload.dict() clients[client_id]["telemetry"] = payload.dict()
save_clients(clients) save_clients(clients)
@@ -397,6 +418,9 @@ async def receive_command_result(client_id: str, payload: CommandResultPayload):
""" """
Endpoint for remote agents to push execution results back to the central server. Endpoint for remote agents to push execution results back to the central server.
""" """
if not client_id or not client_id.strip():
raise HTTPException(status_code=400, detail="client_id cannot be empty")
append_to_logs("command_result", client_id, payload.dict()) append_to_logs("command_result", client_id, payload.dict())
return {"status": "success"} return {"status": "success"}

View File

@@ -10,9 +10,13 @@
"check_space": ["powershell.exe", "-Command", "Get-PSDrive -PSProvider FileSystem"], "check_space": ["powershell.exe", "-Command", "Get-PSDrive -PSProvider FileSystem"],
"check_takeleap": ["python", "client_agent_prototype.py", "--check-takeleap"], "check_takeleap": ["python", "client_agent_prototype.py", "--check-takeleap"],
"list_takeleap": ["python", "client_agent_prototype.py", "--list-takeleap"], "list_takeleap": ["python", "client_agent_prototype.py", "--list-takeleap"],
"list_takeleap_shift": ["python", "client_agent_prototype.py", "--list-takeleap-sub", "SHIFT"], "list_takeleap_size": ["python", "client_agent_prototype.py", "--list-takeleap-size"],
"list_takeleap_errors": ["python", "client_agent_prototype.py", "--list-takeleap-sub", "Error_Videos"], "list_takeleap_shift_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "SHIFT"],
"list_takeleap_upload": ["python", "client_agent_prototype.py", "--list-takeleap-sub", "UPLOAD_FOLDER"] "list_takeleap_shift_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "SHIFT"],
"list_takeleap_errors_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "Error_Videos"],
"list_takeleap_errors_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "Error_Videos"],
"list_takeleap_upload_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "UPLOAD_FOLDER"],
"list_takeleap_upload_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "UPLOAD_FOLDER"]
}, },
"posix": { "posix": {
"whoami": ["whoami"], "whoami": ["whoami"],
@@ -55,8 +59,12 @@
"netstat": ["ss", "-tulnp"], "netstat": ["ss", "-tulnp"],
"check_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--check-takeleap"], "check_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--check-takeleap"],
"list_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap"], "list_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap"],
"list_takeleap_shift": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub", "SHIFT"], "list_takeleap_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-size"],
"list_takeleap_errors": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub", "Error_Videos"], "list_takeleap_shift_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "SHIFT"],
"list_takeleap_upload": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub", "UPLOAD_FOLDER"] "list_takeleap_shift_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "SHIFT"],
"list_takeleap_errors_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "Error_Videos"],
"list_takeleap_errors_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "Error_Videos"],
"list_takeleap_upload_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "UPLOAD_FOLDER"],
"list_takeleap_upload_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "UPLOAD_FOLDER"]
} }
} }

View File

@@ -48,6 +48,7 @@ import psutil
import os import os
import sys import sys
import string import string
import threading
CLIENT_ID = "TEMPLATE_CLIENT_ID" CLIENT_ID = "TEMPLATE_CLIENT_ID"
CENTRAL_BASE = "TEMPLATE_CENTRAL_URL" CENTRAL_BASE = "TEMPLATE_CENTRAL_URL"
@@ -59,6 +60,32 @@ platform_name = "nt" if os.name == "nt" else "posix"
CENTRAL_COMMANDS_URL = f"{CENTRAL_BASE}/api/get-commands?platform={platform_name}" CENTRAL_COMMANDS_URL = f"{CENTRAL_BASE}/api/get-commands?platform={platform_name}"
CACHED_COMMANDS = {} CACHED_COMMANDS = {}
command_in_progress = False
def run_command_async(command_key, safe_cmd_list, is_shutdown_or_restart):
global command_in_progress
try:
result = subprocess.run(safe_cmd_list, capture_output=True, text=True)
output = result.stdout.strip()
error_output = result.stderr.strip() if result.stderr else ""
# Post execution results to Central Server
send_command_result_to_server(command_key, result.returncode, output, error_output)
if not is_shutdown_or_restart:
if result.returncode == 0:
print(f" -> Output: {output}")
send_rocket_chat_notification(f"✅ Success: {command_key}", output, "#00FF00")
else:
print(f" -> Error: {error_output}")
send_rocket_chat_notification(f"❌ Failed: {command_key}", error_output, "#FF0000")
except Exception as ex:
print(f" -> [!] Execution Error: {ex}")
send_command_result_to_server(command_key, -1, "", str(ex))
if not is_shutdown_or_restart:
send_rocket_chat_notification(f"❌ Execution Error: {command_key}", str(ex), "#FF0000")
finally:
command_in_progress = False
def load_allowed_commands(): def load_allowed_commands():
global CACHED_COMMANDS global CACHED_COMMANDS
@@ -185,6 +212,9 @@ def send_command_result_to_server(command: str, returncode: int, stdout: str, st
print(f" -> [!] Failed to send command result to server: {e}") print(f" -> [!] Failed to send command result to server: {e}")
def poll_server(): def poll_server():
global command_in_progress
if command_in_progress:
return
try: try:
print(f"[*] Checking for commands...") print(f"[*] Checking for commands...")
req = urllib.request.Request(CENTRAL_API_URL) req = urllib.request.Request(CENTRAL_API_URL)
@@ -211,26 +241,10 @@ def poll_server():
) )
time.sleep(3) time.sleep(3)
try: command_in_progress = True
result = subprocess.run(safe_cmd_list, capture_output=True, text=True) t = threading.Thread(target=run_command_async, args=(command_key, safe_cmd_list, is_shutdown_or_restart))
output = result.stdout.strip() t.daemon = True
error_output = result.stderr.strip() if result.stderr else "" t.start()
# Post execution results to Central Server
send_command_result_to_server(command_key, result.returncode, output, error_output)
if not is_shutdown_or_restart:
if result.returncode == 0:
print(f" -> Output: {output}")
send_rocket_chat_notification(f"✅ Success: {command_key}", output, "#00FF00")
else:
print(f" -> Error: {error_output}")
send_rocket_chat_notification(f"❌ Failed: {command_key}", error_output, "#FF0000")
except Exception as ex:
print(f" -> [!] Execution Error: {ex}")
send_command_result_to_server(command_key, -1, "", str(ex))
if not is_shutdown_or_restart:
send_rocket_chat_notification(f"❌ Execution Error: {command_key}", str(ex), "#FF0000")
else: else:
warning_msg = f"Server requested unknown/malicious command: '{command_key}'. Ignored." warning_msg = f"Server requested unknown/malicious command: '{command_key}'. Ignored."
print(f" -> [!] SECURITY WARNING: {warning_msg}") print(f" -> [!] SECURITY WARNING: {warning_msg}")
@@ -239,6 +253,13 @@ def poll_server():
print(f" -> [!] Failed to connect to central server: {e}") print(f" -> [!] Failed to connect to central server: {e}")
def get_dir_size(path): def get_dir_size(path):
if os.name != "nt":
try:
result = subprocess.run(["du", "-sb", path], capture_output=True, text=True, timeout=30)
if result.returncode == 0:
return int(result.stdout.split()[0])
except Exception:
pass
total_size = 0 total_size = 0
try: try:
for dirpath, dirnames, filenames in os.walk(path): for dirpath, dirnames, filenames in os.walk(path):
@@ -252,6 +273,30 @@ def get_dir_size(path):
pass pass
return total_size return total_size
def get_takeleap_sizes(root_path, subdirs):
sizes = {d: 0 for d in subdirs}
total_size = 0
subdir_abs_paths = {}
for d in subdirs:
subdir_abs_paths[os.path.join(root_path, d)] = d
try:
for dirpath, dirnames, filenames in os.walk(root_path):
dir_size = 0
for f in filenames:
fp = os.path.join(dirpath, f)
try:
dir_size += os.path.getsize(fp)
except Exception:
pass
total_size += dir_size
for sub_abs, sub_name in subdir_abs_paths.items():
if dirpath == sub_abs or dirpath.startswith(sub_abs + os.sep):
sizes[sub_name] += dir_size
break
except Exception:
pass
return total_size, sizes
def format_size(size_in_bytes): def format_size(size_in_bytes):
for unit in ['B', 'KB', 'MB', 'GB', 'TB']: for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_in_bytes < 1024.0: if size_in_bytes < 1024.0:
@@ -305,7 +350,7 @@ def check_takeleap_folders():
else: else:
print("true" if files_found else "false") print("true" if files_found else "false")
def list_takeleap_parent(): def list_takeleap_parent_folders():
found_any = False found_any = False
if os.name == "nt": if os.name == "nt":
@@ -327,13 +372,51 @@ def list_takeleap_parent():
if os.path.basename(root).upper() == "TAKELEAP": if os.path.basename(root).upper() == "TAKELEAP":
found_any = True found_any = True
total_size = get_dir_size(root) print(f"\n[+] Found TAKELEAP at: {root}")
try:
items = os.listdir(root)
if items:
print(" Contents:")
for idx, item in enumerate(items, 1):
is_dir = "Folder" if os.path.isdir(os.path.join(root, item)) else "File"
print(f" {idx}. {item} ({is_dir})")
else:
print(" (Empty)")
except Exception as e:
print(f" (Error listing directory: {e})")
if not found_any:
print("No TAKELEAP folders found on this system.")
def list_takeleap_parent_size():
found_any = False
if os.name == "nt":
drives = [f"{letter}:\\" for letter in string.ascii_uppercase if os.path.exists(f"{letter}:\\")]
else:
drives = ["/mnt", "/media", "/home"]
if os.path.exists("/TAKELEAP"):
drives.append("/TAKELEAP")
for drive in drives:
for root, dirs, files in os.walk(drive, topdown=True):
if os.name == "nt":
dirs[:] = [d for d in dirs if d.lower() not in {"windows", "program files", "program files (x86)", "appdata", "programdata", "$recycle.bin", "system volume information", "node_modules", ".git"}]
else:
if root == "/":
dirs[:] = [d for d in dirs if d.lower() not in {"proc", "sys", "dev", "var", "lib", "run", "boot", "snap", "node_modules", ".git"}]
elif root == "/mnt":
dirs[:] = [d for d in dirs if not (len(d) == 1 and d.isalpha()) and d.lower() not in {"wslg", "wsl"}]
if os.path.basename(root).upper() == "TAKELEAP":
found_any = True
subdirs = list(dirs)
total_size, subdir_sizes = get_takeleap_sizes(root, subdirs)
print(f"\n[+] Found TAKELEAP at: {root} (Total Size: {format_size(total_size)})") print(f"\n[+] Found TAKELEAP at: {root} (Total Size: {format_size(total_size)})")
if dirs: if subdirs:
print(" Subfolders present:") print(" Subfolders present:")
for d in dirs: for d in subdirs:
subpath = os.path.join(root, d) sub_size = subdir_sizes.get(d, 0)
sub_size = get_dir_size(subpath)
print(f" - {d} (Size: {format_size(sub_size)})") print(f" - {d} (Size: {format_size(sub_size)})")
else: else:
print(" (No subfolders found inside)") print(" (No subfolders found inside)")
@@ -341,7 +424,7 @@ def list_takeleap_parent():
if not found_any: if not found_any:
print("No TAKELEAP folders found on this system.") print("No TAKELEAP folders found on this system.")
def list_takeleap_subfolder(subfolder_name): def list_takeleap_subfolder_size(subfolder_name):
found_any = False found_any = False
if os.name == "nt": if os.name == "nt":
@@ -368,8 +451,41 @@ def list_takeleap_subfolder(subfolder_name):
try: try:
found_any = True found_any = True
sub_size = get_dir_size(subpath) sub_size = get_dir_size(subpath)
items = [item for item in os.listdir(subpath) if os.path.isfile(os.path.join(subpath, item))]
print(f"\n[+] Folder: {subpath} (Total Size: {format_size(sub_size)})") print(f"\n[+] Folder: {subpath} (Total Size: {format_size(sub_size)})")
except Exception as e:
print(f" [!] Error accessing folder {subpath}: {e}")
if not found_any:
print(f"No TAKELEAP/{subfolder_name} folders found on this system.")
def list_takeleap_subfolder_files(subfolder_name):
found_any = False
if os.name == "nt":
drives = [f"{letter}:\\" for letter in string.ascii_uppercase if os.path.exists(f"{letter}:\\")]
else:
drives = ["/mnt", "/media", "/home"]
if os.path.exists("/TAKELEAP"):
drives.append("/TAKELEAP")
for drive in drives:
for root, dirs, files in os.walk(drive, topdown=True):
if os.name == "nt":
dirs[:] = [d for d in dirs if d.lower() not in {"windows", "program files", "program files (x86)", "appdata", "programdata", "$recycle.bin", "system volume information", "node_modules", ".git"}]
else:
if root == "/":
dirs[:] = [d for d in dirs if d.lower() not in {"proc", "sys", "dev", "var", "lib", "run", "boot", "snap", "node_modules", ".git"}]
elif root == "/mnt":
dirs[:] = [d for d in dirs if not (len(d) == 1 and d.isalpha()) and d.lower() not in {"wslg", "wsl"}]
if os.path.basename(root).upper() == "TAKELEAP":
for sub in dirs:
if sub.lower() == subfolder_name.lower():
subpath = os.path.join(root, sub)
try:
found_any = True
items = [item for item in os.listdir(subpath) if os.path.isfile(os.path.join(subpath, item))]
print(f"\n[+] Folder Contents: {subpath}")
if items: if items:
for idx, item in enumerate(items, 1): for idx, item in enumerate(items, 1):
print(f" {idx}. {item}") print(f" {idx}. {item}")
@@ -381,17 +497,29 @@ def list_takeleap_subfolder(subfolder_name):
if not found_any: if not found_any:
print(f"No TAKELEAP/{subfolder_name} folders found on this system.") print(f"No TAKELEAP/{subfolder_name} folders found on this system.")
def list_takeleap_subfolder(subfolder_name):
list_takeleap_subfolder_files(subfolder_name)
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) > 1: if len(sys.argv) > 1:
if sys.argv[1] == "--check-takeleap": if sys.argv[1] == "--check-takeleap":
check_takeleap_folders() check_takeleap_folders()
sys.exit(0) sys.exit(0)
elif sys.argv[1] == "--list-takeleap": elif sys.argv[1] == "--list-takeleap":
list_takeleap_parent() list_takeleap_parent_folders()
sys.exit(0)
elif sys.argv[1] == "--list-takeleap-size":
list_takeleap_parent_size()
sys.exit(0) sys.exit(0)
elif sys.argv[1] == "--list-takeleap-sub" and len(sys.argv) > 2: elif sys.argv[1] == "--list-takeleap-sub" and len(sys.argv) > 2:
list_takeleap_subfolder(sys.argv[2]) list_takeleap_subfolder(sys.argv[2])
sys.exit(0) sys.exit(0)
elif sys.argv[1] == "--list-takeleap-sub-size" and len(sys.argv) > 2:
list_takeleap_subfolder_size(sys.argv[2])
sys.exit(0)
elif sys.argv[1] == "--list-takeleap-sub-files" and len(sys.argv) > 2:
list_takeleap_subfolder_files(sys.argv[2])
sys.exit(0)
else: else:
print(f"Unknown argument or missing parameters: {sys.argv[1]}") print(f"Unknown argument or missing parameters: {sys.argv[1]}")
sys.exit(1) sys.exit(1)