gcli-control 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {gcli_control-0.2.0 → gcli_control-0.3.0}/PKG-INFO +2 -2
  2. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/__init__.py +1 -1
  3. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/client.py +165 -87
  4. gcli_control-0.3.0/gcli/crypto.py +97 -0
  5. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/host.py +11 -1
  6. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/monitoring.py +2 -1
  7. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/netutils.py +2 -1
  8. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/protocol.py +13 -29
  9. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/security.py +23 -0
  10. gcli_control-0.3.0/gcli/system.py +767 -0
  11. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/PKG-INFO +2 -2
  12. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/SOURCES.txt +1 -0
  13. {gcli_control-0.2.0 → gcli_control-0.3.0}/pyproject.toml +2 -2
  14. gcli_control-0.2.0/gcli/crypto.py +0 -51
  15. {gcli_control-0.2.0 → gcli_control-0.3.0}/README.md +0 -0
  16. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/__main__.py +0 -0
  17. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/aliases.py +0 -0
  18. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/clipboard.py +0 -0
  19. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/colors.py +0 -0
  20. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/fileops.py +0 -0
  21. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/npoint.py +0 -0
  22. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/output.py +0 -0
  23. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/processes.py +0 -0
  24. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/session.py +0 -0
  25. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli/utils.py +0 -0
  26. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/dependency_links.txt +0 -0
  27. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/entry_points.txt +0 -0
  28. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/requires.txt +0 -0
  29. {gcli_control-0.2.0 → gcli_control-0.3.0}/gcli_control.egg-info/top_level.txt +0 -0
  30. {gcli_control-0.2.0 → gcli_control-0.3.0}/setup.cfg +0 -0
  31. {gcli_control-0.2.0 → gcli_control-0.3.0}/tests/test_integration.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gcli-control
3
- Version: 0.2.0
4
- Summary: Remote access tool via npoint.io - encrypted command relay using AES-256-GCM
3
+ Version: 0.3.0
4
+ Summary: Remote access tool via npoint.io - encrypted command relay with compression, caching, and advanced system commands
5
5
  Author: gcli contributors
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/gcli-control/gcli
@@ -2,5 +2,5 @@
2
2
  gcli — remote access tool via npoint.io.
3
3
  Encrypted command relay using AES-256-GCM over npoint.io JSON bins.
4
4
  """
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.0"
6
6
  __author__ = "gcli"
@@ -156,99 +156,168 @@ class SSHSession:
156
156
  if not line:
157
157
  continue
158
158
 
159
- # Built-in commands
160
- if line == ":exit" or line == ":quit":
161
- self._send_disconnect()
162
- break
163
- elif line == ":help":
164
- self._do_help()
165
- elif line == ":info":
166
- self._do_info()
167
- elif line == ":ping":
168
- self._do_ping()
169
- elif line == ":history":
170
- self._do_history()
171
- elif line.startswith(":timeout"):
172
- self._do_timeout(line)
173
- elif line.startswith(":upload"):
174
- self._do_upload(line)
175
- elif line.startswith(":download"):
176
- self._do_download(line)
177
- # File operations
178
- elif line == ":ls" or line.startswith(":ls "):
179
- self._do_feature(line, "ls", {"type": "ls"})
180
- elif line == ":tree" or line.startswith(":tree "):
181
- self._do_feature_tree(line)
182
- elif line.startswith(":mkdir "):
183
- self._do_feature(line, "mkdir", {"type": "mkdir", "path": line.split(None, 1)[1]})
184
- elif line.startswith(":rm "):
185
- self._do_feature(line, "rm", {"type": "rm", "path": line.split(None, 1)[1]})
186
- elif line.startswith(":find "):
187
- self._do_feature(line, "find", {"type": "find", "path": ".", "pattern": line.split(None, 1)[1]})
188
- elif line.startswith(":stat "):
189
- self._do_feature(line, "stat", {"type": "stat", "path": line.split(None, 1)[1]})
190
- # Process operations
191
- elif line == ":ps" or line.startswith(":ps "):
192
- limit = line.split()[1] if len(line.split()) > 1 else "15"
193
- self._do_feature(line, "ps", {"type": "ps", "limit": int(limit)})
194
- elif line.startswith(":kill "):
195
- self._do_feature(line, "kill", {"type": "kill", "pid": int(line.split()[1])})
196
- elif line == ":uptime":
197
- self._do_feature(line, "uptime", {"type": "uptime"})
198
- # Monitoring
199
- elif line == ":cpu":
200
- self._do_feature(line, "cpu", {"type": "cpu_usage"})
201
- elif line == ":mem":
202
- self._do_feature(line, "mem", {"type": "memory_usage"})
203
- elif line == ":disk" or line.startswith(":disk "):
204
- path = line.split()[1] if len(line.split()) > 1 else "/"
205
- self._do_feature(line, "disk", {"type": "disk_usage", "path": path})
206
- elif line == ":top" or line.startswith(":top "):
207
- limit = line.split()[1] if len(line.split()) > 1 else "5"
208
- self._do_feature(line, "top", {"type": "top_processes", "metric": "cpu", "limit": int(limit)})
209
- # Clipboard
210
- elif line == ":clipget":
211
- self._do_feature(line, "clipget", {"type": "clip_get"})
212
- elif line.startswith(":clipset "):
213
- self._do_feature(line, "clipset", {"type": "clip_set", "content": line.split(None, 1)[1]})
214
- elif line == ":clipclear":
215
- self._do_feature(line, "clipclear", {"type": "clip_clear"})
216
- # Network
217
- elif line.startswith(":dns "):
218
- self._do_feature(line, "dns", {"type": "dns_lookup", "hostname": line.split()[1]})
219
- elif line.startswith(":portcheck "):
220
- parts = line.split()
221
- self._do_feature(line, "portcheck", {"type": "port_check", "host": parts[1], "port": int(parts[2])})
222
- # Aliases
223
- elif line.startswith(":alias "):
224
- parts = line.split(None, 2)
225
- if len(parts) >= 3:
226
- self._do_feature(line, "alias", {"type": "alias", "name": parts[1], "command": parts[2]})
227
- else:
228
- print(warn("Usage: :alias <name> <command>"))
229
- elif line == ":aliases":
230
- self._do_feature(line, "aliases", {"type": "list_aliases"})
231
- elif line.startswith(":script "):
232
- cmds = line.split(None, 1)[1].split(";")
233
- self._do_feature(line, "script", {"type": "run_script", "commands": [c.strip() for c in cmds]})
234
- # Security
235
- elif line == ":audit":
236
- self._do_feature(line, "audit", {"type": "get_audit_log", "limit": 20})
237
- elif line.startswith(":threat "):
238
- self._do_feature(line, "threat", {"type": "threat_check", "command": line.split(None, 1)[1]})
239
- elif line == ":lock":
240
- self._do_feature(line, "lock", {"type": "lock_session", "reason": "manual"})
241
- elif line == ":unlock":
242
- self._do_feature(line, "unlock", {"type": "unlock_session"})
243
- else:
244
- # Remote exec
245
- self._do_exec(line)
159
+ try:
160
+ self._dispatch_line(line)
161
+ except Exception as e:
162
+ print(error(f"[gcli] Error: {e}"))
163
+ continue
246
164
  except Exception as e:
247
165
  logger.error("REPL error: %s", e)
248
166
  finally:
249
167
  self.connected = False
250
168
  print(info("[gcli] Session closed."))
251
169
 
170
+ def _dispatch_line(self, line: str) -> None:
171
+ """Dispatch a single REPL input line to the appropriate handler."""
172
+ # Built-in commands
173
+ if line == ":exit" or line == ":quit":
174
+ self._send_disconnect()
175
+ self.connected = False
176
+ return
177
+ elif line == ":help":
178
+ self._do_help()
179
+ elif line == ":info":
180
+ self._do_info()
181
+ elif line == ":ping":
182
+ self._do_ping()
183
+ elif line == ":history":
184
+ self._do_history()
185
+ elif line.startswith(":timeout"):
186
+ self._do_timeout(line)
187
+ elif line.startswith(":upload"):
188
+ self._do_upload(line)
189
+ elif line.startswith(":download"):
190
+ self._do_download(line)
191
+ # File operations
192
+ elif line == ":ls" or line.startswith(":ls "):
193
+ ls_path = line.split(None, 1)[1] if len(line.split()) > 1 else "."
194
+ self._do_feature(line, "ls", {"type": "ls", "path": ls_path})
195
+ elif line == ":tree" or line.startswith(":tree "):
196
+ self._do_feature_tree(line)
197
+ elif line.startswith(":mkdir "):
198
+ self._do_feature(line, "mkdir", {"type": "mkdir", "path": line.split(None, 1)[1]})
199
+ elif line.startswith(":rm "):
200
+ self._do_feature(line, "rm", {"type": "rm", "path": line.split(None, 1)[1]})
201
+ elif line.startswith(":find "):
202
+ self._do_feature(line, "find", {"type": "find", "path": ".", "pattern": line.split(None, 1)[1]})
203
+ elif line.startswith(":stat "):
204
+ self._do_feature(line, "stat", {"type": "stat", "path": line.split(None, 1)[1]})
205
+ # Process operations
206
+ elif line == ":ps" or line.startswith(":ps "):
207
+ try:
208
+ limit = int(line.split()[1]) if len(line.split()) > 1 else 15
209
+ except (ValueError, IndexError):
210
+ limit = 15
211
+ self._do_feature(line, "ps", {"type": "ps", "limit": limit})
212
+ elif line.startswith(":kill "):
213
+ try:
214
+ pid = int(line.split()[1])
215
+ except (ValueError, IndexError):
216
+ print(warn("Usage: :kill <pid>"))
217
+ return
218
+ self._do_feature(line, "kill", {"type": "kill", "pid": pid})
219
+ elif line == ":uptime":
220
+ self._do_feature(line, "uptime", {"type": "uptime"})
221
+ # Monitoring
222
+ elif line == ":cpu":
223
+ self._do_feature(line, "cpu", {"type": "cpu_usage"})
224
+ elif line == ":mem":
225
+ self._do_feature(line, "mem", {"type": "memory_usage"})
226
+ elif line == ":disk" or line.startswith(":disk "):
227
+ path = line.split()[1] if len(line.split()) > 1 else "/"
228
+ self._do_feature(line, "disk", {"type": "disk_usage", "path": path})
229
+ elif line == ":top" or line.startswith(":top "):
230
+ try:
231
+ limit = int(line.split()[1]) if len(line.split()) > 1 else 5
232
+ except (ValueError, IndexError):
233
+ limit = 5
234
+ self._do_feature(line, "top", {"type": "top_processes", "metric": "cpu", "limit": limit})
235
+ # Clipboard
236
+ elif line == ":clipget":
237
+ self._do_feature(line, "clipget", {"type": "clip_get"})
238
+ elif line.startswith(":clipset "):
239
+ self._do_feature(line, "clipset", {"type": "clip_set", "content": line.split(None, 1)[1]})
240
+ elif line == ":clipclear":
241
+ self._do_feature(line, "clipclear", {"type": "clip_clear"})
242
+ # Network
243
+ elif line.startswith(":dns "):
244
+ self._do_feature(line, "dns", {"type": "dns_lookup", "hostname": line.split()[1]})
245
+ elif line.startswith(":portcheck "):
246
+ parts = line.split()
247
+ if len(parts) < 3:
248
+ print(warn("Usage: :portcheck <host> <port>"))
249
+ return
250
+ try:
251
+ port = int(parts[2])
252
+ except ValueError:
253
+ print(warn("Port must be a number"))
254
+ return
255
+ self._do_feature(line, "portcheck", {"type": "port_check", "host": parts[1], "port": port})
256
+ # Aliases
257
+ elif line.startswith(":alias "):
258
+ parts = line.split(None, 2)
259
+ if len(parts) >= 3:
260
+ self._do_feature(line, "alias", {"type": "alias", "name": parts[1], "command": parts[2]})
261
+ else:
262
+ print(warn("Usage: :alias <name> <command>"))
263
+ elif line == ":aliases":
264
+ self._do_feature(line, "aliases", {"type": "list_aliases"})
265
+ elif line.startswith(":script "):
266
+ cmds = line.split(None, 1)[1].split(";")
267
+ self._do_feature(line, "script", {"type": "run_script", "commands": [c.strip() for c in cmds]})
268
+ # Security
269
+ elif line == ":audit":
270
+ self._do_feature(line, "audit", {"type": "get_audit_log", "limit": 20})
271
+ elif line.startswith(":threat "):
272
+ self._do_feature(line, "threat", {"type": "threat_check", "command": line.split(None, 1)[1]})
273
+ elif line == ":lock":
274
+ self._do_feature(line, "lock", {"type": "lock_session", "reason": "manual"})
275
+ elif line == ":unlock":
276
+ self._do_feature(line, "unlock", {"type": "unlock_session"})
277
+ # System commands
278
+ elif line == ":netstat":
279
+ self._do_feature(line, "netstat", {"type": "netstat"})
280
+ elif line.startswith(":services"):
281
+ parts = line.split()
282
+ if len(parts) >= 3 and parts[1] in ("start", "stop"):
283
+ self._do_feature(line, "services", {"type": "services", "action": parts[1], "name": parts[2]})
284
+ elif len(parts) == 2 and parts[1] == "list":
285
+ self._do_feature(line, "services", {"type": "services", "action": "list"})
286
+ else:
287
+ self._do_feature(line, "services", {"type": "services", "action": "list"})
288
+ elif line == ":screenshot":
289
+ self._do_feature(line, "screenshot", {"type": "screenshot"})
290
+ elif line.startswith(":history "):
291
+ try:
292
+ limit = int(line.split()[1])
293
+ except (ValueError, IndexError):
294
+ limit = 50
295
+ self._do_feature(line, "history", {"type": "shell_history", "limit": limit})
296
+ elif line == ":history":
297
+ self._do_feature(line, "history", {"type": "shell_history", "limit": 50})
298
+ elif line.startswith(":envset "):
299
+ parts = line.split(None, 2)
300
+ if len(parts) >= 3:
301
+ self._do_feature(line, "envset", {"type": "envset", "key": parts[1], "value": parts[2]})
302
+ else:
303
+ print(warn("Usage: :envset <KEY> <value>"))
304
+ elif line.startswith(":grep "):
305
+ parts = line.split()
306
+ if len(parts) >= 2:
307
+ pattern = parts[1]
308
+ path = parts[2] if len(parts) >= 3 else "."
309
+ include = parts[3] if len(parts) >= 4 else "*"
310
+ self._do_feature(line, "grep", {"type": "grep", "pattern": pattern, "path": path, "include": include})
311
+ else:
312
+ print(warn("Usage: :grep <pattern> [path] [include]"))
313
+ elif line == ":partitions":
314
+ self._do_feature(line, "partitions", {"type": "disk_partitions"})
315
+ elif line == ":whoami":
316
+ self._do_feature(line, "whoami", {"type": "whoami"})
317
+ else:
318
+ # Remote exec
319
+ self._do_exec(line)
320
+
252
321
  # -------------------------------------------------------------------
253
322
  # Built-in command handlers
254
323
  # -------------------------------------------------------------------
@@ -294,6 +363,15 @@ class SSHSession:
294
363
  print(f" {Style.GREEN}:threat <cmd>{Style.RESET} Check threat level")
295
364
  print(f" {Style.GREEN}:lock{Style.RESET} Lock session")
296
365
  print(f" {Style.GREEN}:unlock{Style.RESET} Unlock session")
366
+ print(f"\n{Style.CYAN}{Style.BOLD}--- Advanced System ---{Style.RESET}")
367
+ print(f" {Style.GREEN}:netstat{Style.RESET} Active network connections")
368
+ print(f" {Style.GREEN}:services [list|start|stop] <n>{Style.RESET} Manage services")
369
+ print(f" {Style.GREEN}:screenshot{Style.RESET} Capture remote screen")
370
+ print(f" {Style.GREEN}:history [n]{Style.RESET} Remote shell history (n=limit)")
371
+ print(f" {Style.GREEN}:envset <K> <V>{Style.RESET} Set env variable on host")
372
+ print(f" {Style.GREEN}:grep <pattern> [path] [ext]{Style.RESET} Search files")
373
+ print(f" {Style.GREEN}:partitions{Style.RESET} Disk partitions")
374
+ print(f" {Style.GREEN}:whoami{Style.RESET} Remote user info")
297
375
  print(f"\n {Style.GREEN}:exit{Style.RESET} Disconnect")
298
376
  print(f"{Style.CYAN}{Style.BOLD}{'=' * 40}{Style.RESET}\n")
299
377
 
@@ -0,0 +1,97 @@
1
+ """
2
+ AES-256-GCM encryption/decryption for gcli.
3
+ Password -> PBKDF2 -> 256-bit key -> AES-256-GCM.
4
+ Includes optional zlib compression before encryption.
5
+ """
6
+ import os
7
+ import json
8
+ import zlib
9
+ import base64
10
+ import hashlib
11
+ from typing import Dict
12
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
13
+
14
+ SALT = b"gcli-v1-salt-2024" # fixed salt (password provides secrecy)
15
+ ITERATIONS = 600_000 # OWASP 2023 recommendation for PBKDF2-SHA256
16
+
17
+ # ── Key cache ─────────────────────────────────────────────────────────
18
+ # PBKDF2 with 600K iterations takes ~150ms. Cache derived keys per password.
19
+ _key_cache: Dict[str, bytes] = {}
20
+
21
+
22
+ def derive_key(password: str) -> bytes:
23
+ """Derive a 256-bit key from a password using PBKDF2 (cached)."""
24
+ cached = _key_cache.get(password)
25
+ if cached is not None:
26
+ return cached
27
+ key = hashlib.pbkdf2_hmac(
28
+ "sha256",
29
+ password.encode("utf-8"),
30
+ SALT,
31
+ ITERATIONS,
32
+ dklen=32,
33
+ )
34
+ _key_cache[password] = key
35
+ return key
36
+
37
+
38
+ def clear_key_cache() -> None:
39
+ """Clear the derived-key cache (e.g. on password change)."""
40
+ _key_cache.clear()
41
+
42
+
43
+ # ── Compression helpers ──────────────────────────────────────────────
44
+
45
+ def _compress(data: bytes) -> bytes:
46
+ """Compress with zlib (level 6 = balanced speed/ratio)."""
47
+ return zlib.compress(data, 6)
48
+
49
+
50
+ def _decompress(data: bytes) -> bytes:
51
+ """Decompress zlib data."""
52
+ return zlib.decompress(data)
53
+
54
+
55
+ # ── Encrypt / Decrypt ────────────────────────────────────────────────
56
+
57
+ def encrypt(plaintext: str, password: str, compress: bool = True) -> str:
58
+ """Encrypt plaintext string -> base64 string.
59
+
60
+ Format: flag(1) + nonce(12) + ciphertext + tag(16).
61
+ Flag byte: 0x00 = raw, 0x01 = zlib compressed before encryption.
62
+ """
63
+ key = derive_key(password)
64
+ raw = plaintext.encode("utf-8")
65
+ flag = b"\x00"
66
+ if compress and len(raw) > 128:
67
+ compressed = _compress(raw)
68
+ if len(compressed) < len(raw):
69
+ raw = compressed
70
+ flag = b"\x01"
71
+ nonce = os.urandom(12)
72
+ aesgcm = AESGCM(key)
73
+ ct = aesgcm.encrypt(nonce, raw, None)
74
+ return base64.b64encode(flag + nonce + ct).decode("ascii")
75
+
76
+
77
+ def decrypt(blob: str, password: str) -> str:
78
+ """Decrypt base64 string -> plaintext string."""
79
+ key = derive_key(password)
80
+ raw = base64.b64decode(blob)
81
+ flag = raw[0:1]
82
+ nonce, ct = raw[1:13], raw[13:]
83
+ aesgcm = AESGCM(key)
84
+ plaintext = aesgcm.decrypt(nonce, ct, None)
85
+ if flag == b"\x01":
86
+ plaintext = _decompress(plaintext)
87
+ return plaintext.decode("utf-8")
88
+
89
+
90
+ def encrypt_dict(data: dict, password: str, compress: bool = True) -> str:
91
+ """Encrypt a dict -> base64 string (JSON-serialised then compressed+encrypted)."""
92
+ return encrypt(json.dumps(data, separators=(",", ":")), password, compress=compress)
93
+
94
+
95
+ def decrypt_dict(blob: str, password: str) -> dict:
96
+ """Decrypt a base64 string -> dict."""
97
+ return json.loads(decrypt(blob, password))
@@ -27,6 +27,7 @@ _netutils = None
27
27
  _aliases = None
28
28
  _security = None
29
29
  _monitoring = None
30
+ _system = None
30
31
 
31
32
  logger = logging.getLogger("gcli.host")
32
33
 
@@ -43,11 +44,13 @@ _SECURITY_TYPES = {"rate_check", "audit_log", "get_audit_log", "clear_audit_log"
43
44
  "rotate_key", "threat_check", "lock_session", "unlock_session"}
44
45
  _MONITOR_TYPES = {"cpu_usage", "memory_usage", "disk_usage", "disk_list",
45
46
  "top_processes", "io_stats", "temperature", "battery", "snapshot"}
47
+ _SYSTEM_TYPES = {"netstat", "services", "screenshot", "shell_history",
48
+ "envset", "grep", "disk_partitions", "whoami"}
46
49
 
47
50
 
48
51
  def _get_module(name):
49
52
  """Lazy-import a feature module."""
50
- global _fileops, _processes, _session, _clipboard, _netutils, _aliases, _security, _monitoring
53
+ global _fileops, _processes, _session, _clipboard, _netutils, _aliases, _security, _monitoring, _system
51
54
  if name == "fileops":
52
55
  if _fileops is None:
53
56
  from . import fileops as _fileops_mod
@@ -83,6 +86,11 @@ def _get_module(name):
83
86
  from . import monitoring as _mon_mod
84
87
  _monitoring = _mon_mod
85
88
  return _monitoring
89
+ elif name == "system":
90
+ if _system is None:
91
+ from . import system as _sys_mod
92
+ _system = _sys_mod
93
+ return _system
86
94
  return None
87
95
 
88
96
 
@@ -194,6 +202,8 @@ class Daemon:
194
202
  self._dispatch_module(cmd_id, "security", payload)
195
203
  elif cmd_type in _MONITOR_TYPES:
196
204
  self._dispatch_module(cmd_id, "monitoring", payload)
205
+ elif cmd_type in _SYSTEM_TYPES:
206
+ self._dispatch_module(cmd_id, "system", payload)
197
207
  else:
198
208
  self._respond(cmd_id, {"ok": False, "error": f"Unknown command type: {cmd_type}"})
199
209
 
@@ -788,7 +788,8 @@ def dispatch(payload: dict) -> dict:
788
788
  elif cmd_type == "memory_usage":
789
789
  return mon.memory_usage()
790
790
  elif cmd_type == "disk_usage":
791
- return mon.disk_usage(path=payload.get("path", "/"))
791
+ default_path = "C:\\" if sys.platform == "win32" else "/"
792
+ return mon.disk_usage(path=payload.get("path") or default_path)
792
793
  elif cmd_type == "disk_list":
793
794
  return mon.disk_list()
794
795
  elif cmd_type == "top_processes":
@@ -374,7 +374,8 @@ def dispatch(payload: dict) -> dict:
374
374
  max_hops=payload.get("max_hops", 15),
375
375
  )
376
376
  elif cmd_type == "speed_test":
377
- return nu.speed_test(url=payload.get("url", ""))
377
+ url = payload.get("url") or "http://speedtest.tele2.net/1MB.zip"
378
+ return nu.speed_test(url=url)
378
379
  elif cmd_type == "ports_scan":
379
380
  host = payload.get("host", "")
380
381
  ports = payload.get("ports", [])
@@ -118,12 +118,8 @@ def _safe_append(bin_id: str, field: str, new_entry: dict) -> None:
118
118
  """
119
119
  Read-modify-write with merge. Retries up to 3 times on conflict.
120
120
 
121
- Strategy:
122
- 1. Read doc, note all existing IDs in the target field.
123
- 2. Append new entry.
124
- 3. Re-read to pick up any concurrent writes that happened during our write.
125
- 4. Merge any new entries from the re-read that we missed.
126
- 5. Write back.
121
+ OPTIMIZATION: On the common-case success path, we skip the re-read
122
+ (saves 1 HTTP round-trip). Only re-read if we suspect a conflict.
127
123
  """
128
124
  MAX_ATTEMPTS = 3
129
125
  # Step 1: read
@@ -140,26 +136,7 @@ def _safe_append(bin_id: str, field: str, new_entry: dict) -> None:
140
136
  for attempt in range(MAX_ATTEMPTS):
141
137
  try:
142
138
  write_doc(bin_id, doc)
143
- # Step 4: re-read to check for concurrent writes that may have been lost
144
- doc2 = read_doc(bin_id)
145
- doc2_ids = {e["id"] for e in doc2.get(field, [])}
146
- missing = [e for e in doc.get(field, []) if e["id"] not in doc2_ids]
147
- if missing:
148
- # Our write was overwritten by a concurrent write - re-merge
149
- for entry in missing:
150
- doc2.setdefault(field, []).append(entry)
151
- # Also merge their new entries we didn't have
152
- existing = {e["id"] for e in doc2.get(field, [])}
153
- for entry in doc.get(field, []):
154
- if entry["id"] not in existing:
155
- doc2.setdefault(field, []).append(entry)
156
- # Carry over other fields
157
- for key in ("session", "host_alive"):
158
- if key in doc:
159
- doc2[key] = doc[key]
160
- doc2 = trim(doc)
161
- write_doc(bin_id, doc2)
162
- return
139
+ return # success skip the re-read on the common path
163
140
  except RuntimeError as e:
164
141
  logger.debug("Write failed on attempt %d: %s", attempt + 1, e)
165
142
  if attempt < MAX_ATTEMPTS - 1:
@@ -194,19 +171,26 @@ def claim_results(doc: dict, password: str, target_cmd_id: str, seen: Set[str])
194
171
  """
195
172
  Look for a result matching target_cmd_id that we haven't seen yet.
196
173
  Returns the decrypted payload or None.
174
+
175
+ OPTIMIZATION: We store cmd_id in the entry metadata (unencrypted),
176
+ so we can check it before expensive decryption (~150ms saved per non-match).
197
177
  """
198
178
  for entry in doc.get("results", []):
199
179
  res_id = entry.get("id", "")
200
180
  if res_id in seen:
201
181
  continue
182
+ # FAST PATH: check unencrypted cmd_id in entry metadata first
183
+ if entry.get("cmd_id") != target_cmd_id:
184
+ seen.add(res_id) # mark as seen so we skip it next time
185
+ continue
186
+ # Slow path: only decrypt entries that match our target
202
187
  try:
203
188
  payload = decrypt_dict(entry["payload"], password)
189
+ seen.add(res_id)
190
+ return payload
204
191
  except Exception:
205
192
  seen.add(res_id)
206
193
  continue
207
- if payload.get("cmd_id") == target_cmd_id:
208
- seen.add(res_id)
209
- return payload
210
194
  return None
211
195
 
212
196
 
@@ -243,3 +243,26 @@ if __name__ == "__main__":
243
243
  sm = SecurityManager()
244
244
  sm.test_security()
245
245
  print("All security tests passed.")
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Module-level dispatch (called by host._dispatch_module)
250
+ # ---------------------------------------------------------------------------
251
+
252
+ _manager: Optional["SecurityManager"] = None
253
+
254
+
255
+ def _get_manager() -> "SecurityManager":
256
+ global _manager
257
+ if _manager is None:
258
+ _manager = SecurityManager()
259
+ return _manager
260
+
261
+
262
+ def dispatch(payload: dict) -> dict:
263
+ """Route a security command payload to SecurityManager.dispatch().
264
+
265
+ >>> dispatch({"type": "threat_check", "command": "ls"})["ok"]
266
+ True
267
+ """
268
+ return _get_manager().dispatch(payload)