zscams 2.0.1__py2.py3-none-any.whl

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 (37) hide show
  1. zscams/__init__.py +0 -0
  2. zscams/__main__.py +101 -0
  3. zscams/agent/__init__.py +0 -0
  4. zscams/agent/certificates/.gitkeep +0 -0
  5. zscams/agent/config.yaml +121 -0
  6. zscams/agent/keys/autoport.key +27 -0
  7. zscams/agent/src/__init__.py +0 -0
  8. zscams/agent/src/core/__init__.py +1 -0
  9. zscams/agent/src/core/api/backend/bootstrap.py +21 -0
  10. zscams/agent/src/core/api/backend/client.py +242 -0
  11. zscams/agent/src/core/api/backend/exceptions.py +10 -0
  12. zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
  13. zscams/agent/src/core/prerequisites.py +36 -0
  14. zscams/agent/src/core/service_health_check.py +49 -0
  15. zscams/agent/src/core/services.py +86 -0
  16. zscams/agent/src/core/tunnel/__init__.py +144 -0
  17. zscams/agent/src/core/tunnel/tls.py +56 -0
  18. zscams/agent/src/core/tunnels.py +55 -0
  19. zscams/agent/src/services/__init__.py +0 -0
  20. zscams/agent/src/services/reverse_ssh.py +73 -0
  21. zscams/agent/src/services/ssh_forwarder.py +75 -0
  22. zscams/agent/src/services/system_monitor.py +264 -0
  23. zscams/agent/src/support/__init__.py +0 -0
  24. zscams/agent/src/support/configuration.py +53 -0
  25. zscams/agent/src/support/filesystem.py +41 -0
  26. zscams/agent/src/support/logger.py +40 -0
  27. zscams/agent/src/support/mac.py +18 -0
  28. zscams/agent/src/support/network.py +49 -0
  29. zscams/agent/src/support/openssl.py +100 -0
  30. zscams/libs/getmac/__init__.py +3 -0
  31. zscams/libs/getmac/__main__.py +123 -0
  32. zscams/libs/getmac/getmac.py +1900 -0
  33. zscams/libs/getmac/shutilwhich.py +67 -0
  34. zscams-2.0.1.dist-info/METADATA +118 -0
  35. zscams-2.0.1.dist-info/RECORD +37 -0
  36. zscams-2.0.1.dist-info/WHEEL +4 -0
  37. zscams-2.0.1.dist-info/entry_points.txt +3 -0
zscams/__init__.py ADDED
File without changes
zscams/__main__.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ Main entry point for ZSCAMs Agent
3
+ """
4
+
5
+ import os
6
+ import asyncio
7
+ import sys
8
+ import argparse
9
+ from zscams.agent.src.core.api.backend.bootstrap import bootstrap
10
+ from zscams.agent.src.core.api.backend.update_machine_info import update_machine_info
11
+ from zscams.agent.src.support.configuration import get_config, CONFIG_PATH
12
+ from zscams.agent.src.support.logger import get_logger
13
+ from zscams.agent.src.core.tunnel.tls import create_ssl_context
14
+ from zscams.agent.src.core.tunnels import start_all_tunnels
15
+ from zscams.agent.src.core.services import start_all_services
16
+ from zscams.agent.src.core.service_health_check import monitor_services
17
+ from zscams.agent.src.core.api.backend.client import backend_client
18
+
19
+ logger = get_logger("tls_tunnel_main")
20
+
21
+
22
+ def ensure_bootstrapped():
23
+ """Ensure the agent is bootstrapped with the backend."""
24
+
25
+ bootstrapped = backend_client.is_bootstrapped()
26
+ logger.debug("Agent bootstrapped: %s", bootstrapped)
27
+ if not bootstrapped:
28
+ logger.error(
29
+ "Agent is not bootstrapped. Please run `zscams --bootstrap` to start the bootstrap process.",
30
+ )
31
+ sys.exit(1)
32
+
33
+
34
+ async def async_main():
35
+ """Asynchronous main function to start tunnels and services."""
36
+
37
+ config = get_config()
38
+ config_dir = os.path.dirname(CONFIG_PATH)
39
+ ssl_context = create_ssl_context(config["remote"], config_dir=config_dir)
40
+ remote_host = config["remote"]["host"]
41
+ remote_port = config["remote"]["port"]
42
+
43
+ # Start tunnels and wait for readiness
44
+ tunnel_tasks = await start_all_tunnels(
45
+ config.get("forwards", []), remote_host, remote_port, ssl_context
46
+ )
47
+
48
+ # Start services that depend on tunnels
49
+ service_tasks = asyncio.create_task(
50
+ start_all_services(config.get("services", []), config_dir=config_dir)
51
+ )
52
+
53
+ # Start health checks
54
+ monitor_task = asyncio.create_task(
55
+ monitor_services(
56
+ config.get("services", []),
57
+ config_dir=config_dir,
58
+ interval=config.get("general", {}).get("health_check_interval", 300),
59
+ )
60
+ )
61
+
62
+ logger.info("[*] All tunnels and services started. Press Ctrl+C to stop.")
63
+ await asyncio.gather(*tunnel_tasks, service_tasks, monitor_task)
64
+
65
+
66
+ def main():
67
+ """Main function to run the asynchronous main."""
68
+ parser = argparse.ArgumentParser(description="ZSCAMs Agent")
69
+ parser.add_argument(
70
+ "--bootstrap",
71
+ action="store_true",
72
+ help="Run bootstrap process and exit",
73
+ )
74
+ parser.add_argument(
75
+ "--update-machine-info",
76
+ action="store_true",
77
+ help="Run bootstrap process and exit",
78
+ )
79
+ args = parser.parse_args()
80
+
81
+ if args.bootstrap:
82
+ try:
83
+ bootstrap()
84
+ sys.exit(0)
85
+ except Exception as exception:
86
+ logger.error(exception)
87
+ sys.exit(1)
88
+
89
+ if args.update_machine_info:
90
+ update_machine_info()
91
+ sys.exit(0)
92
+
93
+ try:
94
+ ensure_bootstrapped()
95
+ asyncio.run(async_main())
96
+ except KeyboardInterrupt:
97
+ logger.info("Exiting TLS Tunnel Client")
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
File without changes
File without changes
@@ -0,0 +1,121 @@
1
+ ---
2
+ logging:
3
+ level: 10
4
+
5
+ general:
6
+ health_check_interval: 30 # seconds between health checks for services
7
+
8
+ remote:
9
+ host: 81.255.240.214 # stunnel server IP or hostname
10
+ port: 443 # Single TLS port on stunnel server
11
+ verify_cert: false # set false for self-signed certs
12
+ ca_cert: certificates/ca_chain.pem
13
+ client_cert: certificates/client.crt
14
+ client_key: certificates/client.key
15
+ ca_chain: certificates/ca_chain.pem
16
+
17
+ forwards:
18
+ - local_port: 4422
19
+ sni_hostname: ssh-tunnel
20
+ description: "Forward to SSH backend"
21
+ - local_port: 4423
22
+ sni_hostname: monitor-tunnel
23
+ description: "Forward to Seculogs"
24
+
25
+ backend:
26
+ base_url: "http://22.0.122.40:5000/api"
27
+ cache_dir: ".cache"
28
+
29
+ bootstrap_info:
30
+ csr:
31
+ country: "FR"
32
+ state: "Ile-de-France"
33
+ locality: "Paris"
34
+ common_name: "zscams-agent.example.com"
35
+ organization: "zscams-agent.orangecyberdefense.com"
36
+ cert_issuer: "ZSCAMs"
37
+ blacklist_ports: []
38
+
39
+ services:
40
+ - name: "Reverse SSH"
41
+ script: "src/services/reverse_ssh.py"
42
+ # Its just for reference in logs but not used within the service as the
43
+ # actual port is passed via params (Remote Port). Im setting it to 22 as
44
+ # its the eventual port that will be connected to
45
+ port: 22
46
+ args: []
47
+ health:
48
+ host: localhost
49
+ # port that should be open if service is healthy
50
+ port: 22
51
+
52
+ # -R [remote_listen_address:]remote_listen_port:local_host:local_port
53
+ params:
54
+ remote_host: "localhost"
55
+ # The forwarding port on ZSCAMs server to connect to in order to create
56
+ # the reverse tunnel, this should match the local_port of one of the
57
+ # forwards above
58
+ local_port: 4422
59
+ server_ssh_user: "ssh_user"
60
+ # Port on ZSCAMS server to forward to this agent, this port should be
61
+ # dynamically assigned by the server and passed to the agent via some
62
+ # secure means
63
+ reverse_port: 53612
64
+ # Path to private key for authentication
65
+ private_key: 'zscams/agent/keys/autoport.key'
66
+ ssh_options:
67
+ - "-o LogLevel=error"
68
+ - "-o UserKnownHostsFile=/dev/null"
69
+ - "-o StrictHostKeyChecking=no"
70
+ - "-o ServerAliveInterval=30"
71
+ - "-o ServerAliveCountMax=2"
72
+ prerequisites:
73
+ files:
74
+ - 'zscams/agent/keys/autoport.key'
75
+ services: []
76
+ ports:
77
+ - 22
78
+ - name: "Monitor Forwarder"
79
+ script: "src/services/ssh_forwarder.py"
80
+ port: 530
81
+ args: []
82
+ health:
83
+ host: localhost
84
+ # port that should be open if service is healthy
85
+ port: 44514
86
+ params:
87
+ remote_host: "194.51.14.159"
88
+ remote_port: 530
89
+ local_port: 4423 #Stunnel port
90
+ forwarder_port: 44514 #Port the forwarder will listen on and forward to remote host and port
91
+ server_ssh_user: "ssh_user"
92
+ private_key: 'zscams/agent/keys/autoport.key'
93
+ ssh_options:
94
+ - "-o LogLevel=error"
95
+ - "-o UserKnownHostsFile=/dev/null"
96
+ - "-o StrictHostKeyChecking=no"
97
+ - "-o ServerAliveInterval=30"
98
+ - "-o ServerAliveCountMax=2"
99
+ prerequisites:
100
+ files:
101
+ - 'zscams/agent/keys/autoport.key'
102
+ services: []
103
+ ports: []
104
+ - name: "Monitor Service"
105
+ script: "src/services/system_monitor.py"
106
+ port: 44514
107
+ args: []
108
+ health:
109
+ host: localhost
110
+ port: 44514
111
+ params:
112
+ remote_host: "localhost"
113
+ remote_port: 44514
114
+ equipment_name: "zpa-connector"
115
+ equipment_type: "zpa"
116
+ service_name: "Zscaler-AppConnector"
117
+ prerequisites:
118
+ files: []
119
+ services: []
120
+ ports:
121
+ - 44514
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpQIBAAKCAQEA4kpci9i3k9OhuYpC1+0OSQCpXVRThIjtmwl9Nl4oRSsse9zj
3
+ vkzPShMvlZwvEzzZ/Y3hVDHLD5W3hhy63pyi75uxC9RDzb/7ckc1SdunTzj0mtAt
4
+ FhV+cXFZfv79NfaluN/WZwrbpMO+7+uATuEFIGe/t05PgYX9CzhaagCsC355jFHt
5
+ t2z/9Ul40mJkpMZwr+pk6BOSo/DeW7LNikFljqU8BjoZmGuwGuIHzF6ivhDA8hRz
6
+ aKVfw3e+BuxSyOH3xWxzpHSYiZGU5x9hHk8VEbkP7lISxqYdncCw1Is0O6awfXs0
7
+ azx7yzfUumjDdNSmrwpb5/ard30x/aE96upnRwIDAQABAoIBAQC2ghUcMWD2WDTS
8
+ iSGaNzZTSLZQcKefeDRy23didxRxnP2WtLP2EssIymqRdtM859JFPr4igrpsymqn
9
+ Ptq0mo6LQ/3KNZuuAQ4SwD3JYOAS9DPL/OSwMAu4ARyWYZ/lexV0AwxQNSCrRbjk
10
+ lgL5G2FgHm0wsXdMVr2c5Al//yTDauzgOJtQfi/LzuHusnz05iHzEp0ByBfiRL8z
11
+ xKQ9LFW7pIYUsgn4kXry5tQokH6NwDZ7vGPcjAQLD0lLDfAHF9HOiwpkQYXw8q91
12
+ lNvhaFHxa9jyfvuG9e66Q72YCevsCGBEl8Hvmbpz5/9oBZ6Pt82BZKjSouYh7Cya
13
+ a+JVxQcxAoGBAPSKdJdMXH6hCi5DBYhWnGSpDKvRwvxb2PgntCwajkaEZmuITj4y
14
+ dk9IH89ryl0yAmF6q2pKLyWHOir0fVX1HsnC9m2SErhMuSrhSVpcOLiI09wFwcAM
15
+ PRbS/6g1h/Q+MoaVuD0h9ElO8GkONBIBrmiVYqOifotXuPXjMMGT8oNpAoGBAOzk
16
+ +OSilwjwtwv/MkLOXqGFymSnG7l5zmt/KGxMCThjxY9b1DzyvXcUvojCRjFl4L0W
17
+ OyDbKRiEJZHSH6jV1gj2N8/53b3C1ziCRvHWvix7MJ7DeDfZLp0QEYZBm6Xm7mG+
18
+ TZ27kya0Hprev/t960UfOpwhb8+MNd1dxdgspC8vAoGBALcGpcrTyWqxZ2hGm252
19
+ vKkOacBzyAePSu448T4NRi17TRjwtPcSV8BxD/X0DEsCcgu5f3CXQ4BIHP4nbWOX
20
+ icqi1EQgD0jHi9OPOJKb8YwURNUpreDqiBJ8LAMexbnFj5Vxm6qNrkPsBD3s9oX/
21
+ oiT+ogwtQ59RMcs/lq9b5yf5AoGAbPmYFXVGDXLOgdJPiLPujFdDl7HX6ybBcmn4
22
+ ank/9JTRGPWhWLhBuDnuvHLCX48CJ3nGkYLAEOsZbU9ACSb1YwIBAsdq3hR3dSNZ
23
+ B39F1KiG4UICV46tBsuRhDVCKLtnBcfJZLoZI0DQo2W84zA1voJzL8eh69QQI1kz
24
+ 3hILJTkCgYEAwT+XKnxX8jExNvpe71O2pm3mit6UyOHXB1KyT4Y3UuKjDxwZZ4w1
25
+ vPp6eQ3VcI+4+mpf0qs1cgDz8w35EInt60nKy0+0hwQaM7N0EmsguASHRprESZR9
26
+ dAPDJQ1UJlAMSJxZxFzTvMzDQHY+86GGh5mdoNwJDb0142etE7rSFUI=
27
+ -----END RSA PRIVATE KEY-----
File without changes
@@ -0,0 +1 @@
1
+ running_services = set()
@@ -0,0 +1,21 @@
1
+ from zscams.agent.src.core.api.backend.client import backend_client
2
+ from zscams.agent.src.support.logger import get_logger
3
+
4
+ logger = get_logger("agent_bootstrap")
5
+
6
+
7
+ def bootstrap():
8
+ """Ensure the agent is bootstrapped with the backend."""
9
+
10
+ if backend_client.is_bootstrapped():
11
+ logger.info("Agent ID %s is already bootstrapped.", backend_client.agent_id)
12
+ return
13
+
14
+ username = input("LDAP Username: ")
15
+ totp = input("TOTP: ")
16
+
17
+ backend_client.login(username, totp)
18
+
19
+ equipment_name = input("Equipment Name: ")
20
+ enforced_id = input("Enforced Agent ID (Optional): ")
21
+ backend_client.bootstrap(equipment_name, enforced_id or None)
@@ -0,0 +1,242 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ import requests
5
+
6
+ from typing import Optional, cast
7
+
8
+ from .exceptions import AgentBootstrapError
9
+
10
+ from zscams.agent.src.support.configuration import CONFIG_PATH, ROOT_PATH, get_config
11
+ from zscams.agent.src.support.mac import get_mac_address
12
+ from zscams.agent.src.support.filesystem import resolve_path, ensure_dir, is_file_exists
13
+ from zscams.agent.src.support.network import get_local_hostname, get_local_ip_address
14
+ from zscams.agent.src.support.openssl import (
15
+ generate_csr_from_private_key,
16
+ generate_private_key,
17
+ )
18
+ from zscams.agent.src.support.logger import get_logger
19
+
20
+
21
+ class BackendClient:
22
+ """Client to interact with the backend service."""
23
+
24
+ MACHINE_INFO_FILE_NAME = "machine_info.json"
25
+
26
+ def __init__(self):
27
+ config = get_config()
28
+ self.services_config = config.get("services", [])
29
+ self.backend_config = config.get("backend", {})
30
+ self.bootstrap_info = config.get("bootstrap_info", {})
31
+ self.remote_config = config.get("remote", {})
32
+ self.url = self.backend_config.get("base_url")
33
+ self.session = requests.session()
34
+ self.agent_id = get_mac_address()
35
+ self.logger = get_logger("backend_client")
36
+
37
+ self.__machine_cache_path = Path(
38
+ os.path.join(
39
+ ROOT_PATH.parent,
40
+ self.backend_config.get("cache_dir"),
41
+ self.MACHINE_INFO_FILE_NAME,
42
+ )
43
+ )
44
+
45
+ def bootstrap(self, equipment_name: str, enforced_id: Optional[str] = None):
46
+ """Bootstrap the agent with the backend."""
47
+
48
+ if not self.is_authenticated():
49
+ self.logger.error("Couldn't authenticate")
50
+ raise AgentBootstrapError(
51
+ "Client is not authenticated to bootstrap. Please login first."
52
+ )
53
+
54
+ self.logger.info("bootstrapping agent ID %s", self.agent_id)
55
+
56
+ csr_info = self.bootstrap_info.get("csr", {})
57
+ remote_configs = get_config().get("remote", {})
58
+
59
+ private_key_path = cast(
60
+ str,
61
+ resolve_path(
62
+ remote_configs.get("client_key"), os.path.dirname(CONFIG_PATH)
63
+ ),
64
+ )
65
+
66
+ self.logger.info("Generating the private key...")
67
+ private_key_content = generate_private_key(private_key_path)
68
+
69
+ self.logger.info("Generated the private key at %s", private_key_path)
70
+
71
+ ip = get_local_ip_address()
72
+ csr = generate_csr_from_private_key(
73
+ private_key_content,
74
+ csr_info.get("country"),
75
+ csr_info.get("state"),
76
+ csr_info.get("locality"),
77
+ csr_info.get("organization"),
78
+ csr_info.get("common_name"),
79
+ )
80
+
81
+ payload = {
82
+ "private_ip": ip,
83
+ "lan_ip": ip,
84
+ "hostname": get_local_hostname(),
85
+ "equipment_name": equipment_name,
86
+ "csr": csr,
87
+ "enforced_id": enforced_id,
88
+ "services": [service.get("name") for service in self.services_config],
89
+ "blacklist_ports": self.bootstrap_info.get("blacklist_ports", []),
90
+ "cert_issuer": self.bootstrap_info.get("cert_issuer", None),
91
+ }
92
+
93
+ self.logger.debug(
94
+ "bootstrapping the agent with payload %s", json.dumps(payload, indent=2)
95
+ )
96
+
97
+ res = self._post(f"agent-bootstrap/{self.agent_id}", payload)
98
+ res_json = res.json()
99
+
100
+ if not res.ok or res_json.get("success") is False:
101
+ message = (
102
+ res_json.get("message", None)
103
+ or res_json.get("error", None)
104
+ or res_json.get("errors", None)
105
+ or "Unknown error"
106
+ )
107
+ raise AgentBootstrapError(
108
+ f"Bootstrapping failed on backend request: {message}"
109
+ )
110
+
111
+ self.logger.info("Signed the cert")
112
+ self.logger.debug("Signed cert response: %s", json.dumps(res_json, indent=2))
113
+
114
+ ca_chain: list[str] = res_json.get("response", {}).get("ca_chain", [])
115
+ signed_cert: str = res_json.get("response", {}).get("signed_certificate", [])
116
+ self._write_certificates(ca_chain, signed_cert)
117
+
118
+ # To cache the machine info
119
+ self.get_machine_info(ignore_cache=True)
120
+
121
+ return res_json
122
+
123
+ def get_machine_info(self, ignore_cache: bool = False):
124
+ """Get machine information from the backend."""
125
+
126
+ if not ignore_cache and is_file_exists(self.__machine_cache_path, self.logger):
127
+ return self.__load_machine_info_from_cache()
128
+
129
+ res = self._get(f"customers-machines/machine-id/{self.agent_id}")
130
+ res.raise_for_status()
131
+
132
+ machine_info = res.json().get("response", {})
133
+ self.logger.debug(
134
+ "Fetched machine info: %s", json.dumps(machine_info, indent=2)
135
+ )
136
+ self.__cache_machine_info(machine_info)
137
+ return machine_info
138
+
139
+ def __load_machine_info_from_cache(self):
140
+ """Cache machine information to a local file."""
141
+
142
+ self.logger.info(
143
+ "Loading machine info from cache at %s", self.__machine_cache_path
144
+ )
145
+ with open(self.__machine_cache_path, "r", encoding="utf-8") as cache_file:
146
+ return json.load(cache_file)
147
+
148
+ def __cache_machine_info(self, machine_info: dict):
149
+ """Cache machine information to a local file."""
150
+
151
+ ensure_dir(str(self.__machine_cache_path.parent.absolute()))
152
+ self.logger.info("Caching machine info to %s", self.__machine_cache_path)
153
+ with open(self.__machine_cache_path, "w", encoding="utf-8") as cache_file:
154
+ json.dump(machine_info, cache_file, indent=2)
155
+
156
+ def _write_certificates(self, ca_chain: list[str], cert: str):
157
+ if cert:
158
+ cert_path = os.path.join(ROOT_PATH, self.remote_config.get("client_cert"))
159
+
160
+ self.logger.info("Writing signed certificate to %s", cert_path)
161
+ with open(cert_path, "w", encoding="utf-8") as cert_file:
162
+ cert_file.write(cert)
163
+
164
+ if ca_chain:
165
+ ca_chain_path = os.path.join(ROOT_PATH, self.remote_config.get("ca_chain"))
166
+
167
+ self.logger.info("Writing CA chain to %s", ca_chain_path)
168
+ with open(ca_chain_path, "w", encoding="utf-8") as ca_chain_file:
169
+ ca_chain_file.write("\n\n".join(ca_chain))
170
+
171
+ def is_bootstrapped(self) -> bool:
172
+ """Check if the agent is bootstrapped."""
173
+
174
+ is_bootstrapped_res = self._get(
175
+ f"agent-bootstrap/{self.agent_id}/is-bootstrapped"
176
+ )
177
+ is_bootstrapped_res.raise_for_status()
178
+
179
+ return (
180
+ is_bootstrapped_res.json().get("response", {}).get("is-bootstrapped", False)
181
+ )
182
+
183
+ def _get(self, path: str):
184
+ """Perform a GET request to the backend."""
185
+
186
+ return self.session.get(self.__prepare_path(path))
187
+
188
+ def _post(self, path: str, body: dict):
189
+ """Perform a POST request to the backend."""
190
+
191
+ return self.session.post(self.__prepare_path(path), json=body)
192
+
193
+ def _put(self, path: str, body: dict):
194
+ """Perform a PUT request to the backend."""
195
+
196
+ return self.session.put(self.__prepare_path(path), json=body)
197
+
198
+ def _delete(self, path: str):
199
+ """Perform a DELETE request to the backend."""
200
+
201
+ return self.session.delete(self.__prepare_path(path))
202
+
203
+ def login(self, username: str, totp: str):
204
+ """Perform login to the backend using username and TOTP."""
205
+
206
+ payload = {
207
+ "username": username,
208
+ "totp": totp,
209
+ "client_id": "orchestrator-client",
210
+ "grant_type": "password",
211
+ }
212
+
213
+ self.logger.info("Logging in to backend as %s", username)
214
+
215
+ response = self.session.post(
216
+ self.__prepare_path("authentication/temp/login"), json=payload
217
+ )
218
+ if not response.ok:
219
+ self.logger.error(
220
+ "Login failed with status code %d: %s",
221
+ response.status_code,
222
+ response.text,
223
+ )
224
+ response.raise_for_status()
225
+
226
+ token = response.json().get("access_token")
227
+ if token:
228
+ self.session.headers.update({"Authorization": f"Bearer {token}"})
229
+
230
+ self.logger.info("Login successful")
231
+
232
+ return response.json()
233
+
234
+ def is_authenticated(self) -> bool:
235
+ """Check if the client is authenticated."""
236
+ return self.session.headers.get("Authorization") is not None
237
+
238
+ def __prepare_path(self, path):
239
+ return f"{self.url}/{path.lstrip('/')}"
240
+
241
+
242
+ backend_client = BackendClient()
@@ -0,0 +1,10 @@
1
+ class AgentBootstrapError(Exception):
2
+ """Base exception for agent bootstrap-related errors."""
3
+
4
+
5
+ class AgentNotBootstrapped(AgentBootstrapError):
6
+ """Raised when an operation requires a bootstrapped agent, but the agent is not bootstrapped."""
7
+
8
+
9
+ class AgentAlreadyBootstrapped(AgentBootstrapError):
10
+ """Raised when attempting to bootstrap an already bootstrapped agent."""
@@ -0,0 +1,16 @@
1
+ from zscams.agent.src.core.api.backend.client import backend_client
2
+ from zscams.agent.src.support.logger import get_logger
3
+
4
+ logger = get_logger("agent_machine_info")
5
+
6
+
7
+ def update_machine_info():
8
+ """Ensure the agent is bootstrapped with the backend."""
9
+
10
+ username = input("LDAP Username: ")
11
+ totp = input("TOTP: ")
12
+
13
+ backend_client.login(username, totp)
14
+
15
+ logger.info("Getting machine info from backend...")
16
+ backend_client.get_machine_info(ignore_cache=True)
@@ -0,0 +1,36 @@
1
+ from zscams.agent.src.support.logger import get_logger
2
+ from zscams.agent.src.support.filesystem import is_file_exists
3
+ from zscams.agent.src.core import running_services
4
+ from zscams.agent.src.support.network import is_port_open, is_service_running
5
+
6
+ logger = get_logger("service_prerequisites")
7
+
8
+
9
+ def check_prerequisites(prereqs: dict) -> bool:
10
+ """
11
+ Perform a one-shot prerequisites check (no waiting or retry).
12
+ Returns True only if all listed conditions are satisfied.
13
+ """
14
+ if not prereqs:
15
+ return True
16
+
17
+ results = []
18
+
19
+ # Check file prerequisites
20
+ for path in prereqs.get("files", []):
21
+ results.append(is_file_exists(path, logger))
22
+
23
+ # Check port prerequisites
24
+ for port in prereqs.get("ports", []):
25
+ results.append(is_port_open("localhost", port, logger))
26
+
27
+ # Check service prerequisites
28
+ for svc in prereqs.get("services", []):
29
+ results.append(is_service_running(svc, running_services, logger))
30
+
31
+ all_ok = all(results)
32
+ if not all_ok:
33
+ logger.warning("Some prerequisites failed for this service.")
34
+ else:
35
+ logger.info("All prerequisites satisfied.")
36
+ return all_ok
@@ -0,0 +1,49 @@
1
+ """
2
+ Health monitor for agent services.
3
+
4
+ This module periodically checks the defined health ports of all services.
5
+ If a service is not responding on its port, it will be restarted.
6
+ """
7
+
8
+ import asyncio
9
+ from zscams.agent.src.support.logger import get_logger
10
+ from zscams.agent.src.core.services import start_service
11
+ from zscams.agent.src.core import running_services
12
+ from zscams.agent.src.support.network import is_port_open
13
+
14
+ logger = get_logger("health_monitor")
15
+
16
+
17
+ async def monitor_services(services: list, config_dir: str, interval: int = 60):
18
+ """
19
+ Periodically check service health by port.
20
+
21
+ If a service's health port is closed, attempt to restart it.
22
+ """
23
+ logger.info(f"Health monitor started (interval={interval}s)")
24
+ await asyncio.sleep(30) # Initial delay to allow services to start
25
+ while True:
26
+ for svc in services:
27
+ name = svc["name"]
28
+ health = svc.get("health", {})
29
+
30
+ host = health.get("host", "localhost")
31
+ port = health.get("port")
32
+ if not port:
33
+ logger.debug(f"Skipping {name}: no health port defined.")
34
+ continue
35
+
36
+ if name in running_services and is_port_open(host, port, logger):
37
+ logger.debug(f"{name} is healthy on {host}:{port}")
38
+ continue
39
+
40
+ logger.warning(f"{name} not healthy, restarting...")
41
+ if name in running_services:
42
+ running_services.remove(name)
43
+ try:
44
+ await start_service(svc, config_dir)
45
+ logger.info(f"Restarted service: {name}")
46
+ except Exception as e:
47
+ logger.error(f"Failed to restart {name}: {e}")
48
+
49
+ await asyncio.sleep(interval)