diff --git a/integrations/beyondtrust-password-safe/manifest.yaml b/integrations/beyondtrust-password-safe/manifest.yaml new file mode 100644 index 0000000..22606b2 --- /dev/null +++ b/integrations/beyondtrust-password-safe/manifest.yaml @@ -0,0 +1,77 @@ +id: beyondtrust_password_safe +name: BeyondTrust Password Safe +version: 1.0.0 +description: "BeyondTrust Password Safe (Secrets Safe REST API v3) — privileged access and credential retrieval: list managed accounts and systems, request a credential release, and retrieve the credential. API-key (PS-Auth) session authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: list managed accounts/systems, create release request, get credential." +category: identity + +# Per-instance configuration. Auth signs in with an API key + runas user +# (header 'Authorization: PS-Auth key=; runas=;'), which +# establishes a session reused for the request. +config_schema: + properties: + base_url: + type: string + description: "BeyondTrust URL (e.g. https://beyondtrust.example.com)" + api_key: + type: string + description: "API registration key" + x-soar-sensitive: true + runas_user: + type: string + description: "Username to run as" + insecure: + type: boolean + description: "Trust any TLS certificate (not secure)" + default: false + required: + - base_url + - api_key + - runas_user + +commands: + - id: list_managed_accounts + name: beyondtrust-list-managed-accounts + description: "List managed accounts." + risk: read + inputs_schema: + properties: + account_name: { type: string, description: "Optional account name filter" } + required: [] + outputs_schema: { properties: {} } + - id: list_managed_systems + name: beyondtrust-list-managed-systems + description: "List managed systems." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + - id: create_release_request + name: beyondtrust-create-release-request + description: "Request a credential release for a managed account." + inputs_schema: + properties: + system_id: { type: string, description: "Managed system ID" } + account_id: { type: string, description: "Managed account ID" } + duration_minutes: { type: number, description: "Access duration in minutes (default 30)" } + reason: { type: string, description: "Reason for the request" } + required: [system_id, account_id] + outputs_schema: { properties: {} } + - id: get_credential + name: beyondtrust-get-credential + description: "Retrieve the credential for an approved request." + inputs_schema: + properties: + request_id: { type: string, description: "Request ID (from create-release-request)" } + required: [request_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: beyondtrust-test-connection + description: "Verify the sign-in (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/beyondtrust-password-safe/scripts/create_release_request.py b/integrations/beyondtrust-password-safe/scripts/create_release_request.py new file mode 100644 index 0000000..c1c97e1 --- /dev/null +++ b/integrations/beyondtrust-password-safe/scripts/create_release_request.py @@ -0,0 +1,103 @@ +import json, os, sys, ssl, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _ctx(cfg): + if cfg.get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3" + ctx = _ctx(cfg) + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()), + urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(), + ) + + def _auth_header(self): + return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";" + + def signin(self): + req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"", + headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST") + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def call(self, method, path, body=None, params=None): + url = self.base + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": self._auth_header()} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def signout(self): + try: + req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST") + self.opener.open(req, timeout=30).read() + except Exception: + pass + + +def _run(fn): + try: + cfg = _cfg() + inputs = _inputs() + client = Client(cfg) + client.signin() + try: + result = fn(client, inputs) + finally: + client.signout() + print(json.dumps(result)) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(client, inputs): + system_id = inputs.get("system_id") + account_id = inputs.get("account_id") + duration_minutes = inputs.get("duration_minutes") + reason = inputs.get("reason") + if not system_id: + raise Exception("system_id is required") + if not account_id: + raise Exception("account_id is required") + body = { + "SystemId": int(system_id), + "AccountId": int(account_id), + "DurationMinutes": int(duration_minutes) if duration_minutes else 30, + "Reason": reason or "Riposte SOAR", + "AccessType": "View", + } + return client.call("POST", "/Requests", body=body) + + +_run(main) diff --git a/integrations/beyondtrust-password-safe/scripts/get_credential.py b/integrations/beyondtrust-password-safe/scripts/get_credential.py new file mode 100644 index 0000000..8c5a781 --- /dev/null +++ b/integrations/beyondtrust-password-safe/scripts/get_credential.py @@ -0,0 +1,93 @@ +import json, os, sys, ssl, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _ctx(cfg): + if cfg.get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3" + ctx = _ctx(cfg) + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()), + urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(), + ) + + def _auth_header(self): + return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";" + + def signin(self): + req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"", + headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST") + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def call(self, method, path, body=None, params=None): + url = self.base + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": self._auth_header()} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def signout(self): + try: + req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST") + self.opener.open(req, timeout=30).read() + except Exception: + pass + + +def _run(fn): + try: + cfg = _cfg() + inputs = _inputs() + client = Client(cfg) + client.signin() + try: + result = fn(client, inputs) + finally: + client.signout() + print(json.dumps(result)) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(client, inputs): + request_id = inputs.get("request_id") + if not request_id: + raise Exception("request_id is required") + q = lambda v: urllib.parse.quote(str(v), safe="") + response = client.call("GET", "/Credentials/" + q(request_id)) + return {"request_id": request_id, "credential": response} + + +_run(main) diff --git a/integrations/beyondtrust-password-safe/scripts/list_managed_accounts.py b/integrations/beyondtrust-password-safe/scripts/list_managed_accounts.py new file mode 100644 index 0000000..c4fc6cc --- /dev/null +++ b/integrations/beyondtrust-password-safe/scripts/list_managed_accounts.py @@ -0,0 +1,89 @@ +import json, os, sys, ssl, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _ctx(cfg): + if cfg.get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3" + ctx = _ctx(cfg) + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()), + urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(), + ) + + def _auth_header(self): + return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";" + + def signin(self): + req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"", + headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST") + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def call(self, method, path, body=None, params=None): + url = self.base + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": self._auth_header()} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def signout(self): + try: + req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST") + self.opener.open(req, timeout=30).read() + except Exception: + pass + + +def _run(fn): + try: + cfg = _cfg() + inputs = _inputs() + client = Client(cfg) + client.signin() + try: + result = fn(client, inputs) + finally: + client.signout() + print(json.dumps(result)) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(client, inputs): + account_name = inputs.get("account_name") + return client.call("GET", "/ManagedAccounts", params={"accountName": account_name}) + + +_run(main) diff --git a/integrations/beyondtrust-password-safe/scripts/list_managed_systems.py b/integrations/beyondtrust-password-safe/scripts/list_managed_systems.py new file mode 100644 index 0000000..cbc38c7 --- /dev/null +++ b/integrations/beyondtrust-password-safe/scripts/list_managed_systems.py @@ -0,0 +1,88 @@ +import json, os, sys, ssl, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _ctx(cfg): + if cfg.get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3" + ctx = _ctx(cfg) + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()), + urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(), + ) + + def _auth_header(self): + return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";" + + def signin(self): + req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"", + headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST") + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def call(self, method, path, body=None, params=None): + url = self.base + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": self._auth_header()} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def signout(self): + try: + req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST") + self.opener.open(req, timeout=30).read() + except Exception: + pass + + +def _run(fn): + try: + cfg = _cfg() + inputs = _inputs() + client = Client(cfg) + client.signin() + try: + result = fn(client, inputs) + finally: + client.signout() + print(json.dumps(result)) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(client, inputs): + return client.call("GET", "/ManagedSystems") + + +_run(main) diff --git a/integrations/beyondtrust-password-safe/scripts/test_connection.py b/integrations/beyondtrust-password-safe/scripts/test_connection.py new file mode 100644 index 0000000..26003f5 --- /dev/null +++ b/integrations/beyondtrust-password-safe/scripts/test_connection.py @@ -0,0 +1,89 @@ +import json, os, sys, ssl, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _ctx(cfg): + if cfg.get("insecure"): + c = ssl.create_default_context() + c.check_hostname = False + c.verify_mode = ssl.CERT_NONE + return c + return None + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3" + ctx = _ctx(cfg) + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()), + urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(), + ) + + def _auth_header(self): + return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";" + + def signin(self): + req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"", + headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST") + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def call(self, method, path, body=None, params=None): + url = self.base + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": self._auth_header()} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + def signout(self): + try: + req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST") + self.opener.open(req, timeout=30).read() + except Exception: + pass + + +def _run(fn): + try: + cfg = _cfg() + inputs = _inputs() + client = Client(cfg) + client.signin() + try: + result = fn(client, inputs) + finally: + client.signout() + print(json.dumps(result)) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(client, inputs): + client.call("GET", "/ManagedSystems") + return {"ok": True} + + +_run(main)