diff --git a/integrations/efficientip/manifest.yaml b/integrations/efficientip/manifest.yaml new file mode 100644 index 0000000..b0c91ff --- /dev/null +++ b/integrations/efficientip/manifest.yaml @@ -0,0 +1,78 @@ +id: efficientip +name: EfficientIP SOLIDserver +version: 1.0.0 +description: "EfficientIP SOLIDserver (REST API) — DNS/DDI containment and visibility: add an RPZ record to block a domain (DNS sinkhole), list DNS zones, search IP addresses, and list networks. HTTP Basic authentication; stdlib-only, no extra Python dependencies. (French vendor.)" +changelog: "1.0.0 — Initial release: add RPZ record, list DNS zones, search IP, list networks." +category: network + +# Per-instance configuration. HTTP Basic auth against the SOLIDserver REST API. +config_schema: + properties: + base_url: + type: string + description: "SOLIDserver URL (e.g. https://ipam.example.com)" + username: + type: string + description: "API username" + password: + type: string + description: "API password" + x-soar-sensitive: true + insecure: + type: boolean + description: "Trust any TLS certificate (not secure)" + default: false + required: + - base_url + - username + - password + +commands: + - id: add_rpz_record + name: efficientip-add-rpz-record + description: "Add an RPZ resource record to block a domain (DNS sinkhole)." + inputs_schema: + properties: + dns_name: { type: string, description: "DNS server name that hosts the RPZ zone" } + rpz_zone: { type: string, description: "RPZ zone name" } + record_name: { type: string, description: "Record name (the domain to block)" } + rr_type: { type: string, description: "Record type (default CNAME)" } + value: { type: string, description: "Record value (default '.' for NXDOMAIN)" } + required: [dns_name, rpz_zone, record_name] + outputs_schema: { properties: {} } + - id: list_dns_zones + name: efficientip-list-dns-zones + description: "List DNS zones." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max zones (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: search_ip + name: efficientip-search-ip + description: "Search IP addresses by value." + risk: read + inputs_schema: + properties: + ip: { type: string, description: "IP address to search" } + required: [ip] + outputs_schema: { properties: {} } + - id: list_networks + name: efficientip-list-networks + description: "List IP subnets/networks." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max networks (default 50)" } + required: [] + outputs_schema: { properties: {} } + + - id: test_connection + name: efficientip-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/efficientip/scripts/add_rpz_record.py b/integrations/efficientip/scripts/add_rpz_record.py new file mode 100644 index 0000000..daf9058 --- /dev/null +++ b/integrations/efficientip/scripts/add_rpz_record.py @@ -0,0 +1,74 @@ +import json, os, sys, base64, ssl, 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 + + +def _auth(cfg): + raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def request(method, path, cfg, params=None): + url = str(cfg.get("base_url", "")).rstrip("/") + "/rest" + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + req = urllib.request.Request(url, headers={"Authorization": _auth(cfg), "Accept": "application/json"}, method=method) + with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r: + raw = r.read() + try: + return json.loads(raw) if raw else {} + except Exception: + return {"result": raw.decode("utf-8", "replace")} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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(cfg, inputs): + dns_name = inputs.get("dns_name") + if not dns_name: + raise Exception("dns_name is required") + rpz_zone = inputs.get("rpz_zone") + if not rpz_zone: + raise Exception("rpz_zone is required") + record_name = inputs.get("record_name") + if not record_name: + raise Exception("record_name is required") + rr_type = inputs.get("rr_type") or "CNAME" + value = inputs.get("value") or "." + + return request("POST", "/dns_rr_add", cfg, params={ + "dns_name": dns_name, + "dns_rr_name": record_name, + "dns_rr_type": rr_type, + "dns_rr_value1": value, + "dns_zone_name": rpz_zone, + }) + + +_run(main) diff --git a/integrations/efficientip/scripts/list_dns_zones.py b/integrations/efficientip/scripts/list_dns_zones.py new file mode 100644 index 0000000..3661218 --- /dev/null +++ b/integrations/efficientip/scripts/list_dns_zones.py @@ -0,0 +1,59 @@ +import json, os, sys, base64, ssl, 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 + + +def _auth(cfg): + raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def request(method, path, cfg, params=None): + url = str(cfg.get("base_url", "")).rstrip("/") + "/rest" + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + req = urllib.request.Request(url, headers={"Authorization": _auth(cfg), "Accept": "application/json"}, method=method) + with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r: + raw = r.read() + try: + return json.loads(raw) if raw else {} + except Exception: + return {"result": raw.decode("utf-8", "replace")} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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(cfg, inputs): + limit = inputs.get("limit") + limit = int(limit) if limit not in (None, "") else 50 + + return request("GET", "/dns_zone_list", cfg, params={"limit": limit}) + + +_run(main) diff --git a/integrations/efficientip/scripts/list_networks.py b/integrations/efficientip/scripts/list_networks.py new file mode 100644 index 0000000..fa5dc18 --- /dev/null +++ b/integrations/efficientip/scripts/list_networks.py @@ -0,0 +1,59 @@ +import json, os, sys, base64, ssl, 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 + + +def _auth(cfg): + raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def request(method, path, cfg, params=None): + url = str(cfg.get("base_url", "")).rstrip("/") + "/rest" + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + req = urllib.request.Request(url, headers={"Authorization": _auth(cfg), "Accept": "application/json"}, method=method) + with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r: + raw = r.read() + try: + return json.loads(raw) if raw else {} + except Exception: + return {"result": raw.decode("utf-8", "replace")} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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(cfg, inputs): + limit = inputs.get("limit") + limit = int(limit) if limit not in (None, "") else 50 + + return request("GET", "/ip_subnet_list", cfg, params={"limit": limit}) + + +_run(main) diff --git a/integrations/efficientip/scripts/search_ip.py b/integrations/efficientip/scripts/search_ip.py new file mode 100644 index 0000000..4990b14 --- /dev/null +++ b/integrations/efficientip/scripts/search_ip.py @@ -0,0 +1,60 @@ +import json, os, sys, base64, ssl, 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 + + +def _auth(cfg): + raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def request(method, path, cfg, params=None): + url = str(cfg.get("base_url", "")).rstrip("/") + "/rest" + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + req = urllib.request.Request(url, headers={"Authorization": _auth(cfg), "Accept": "application/json"}, method=method) + with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r: + raw = r.read() + try: + return json.loads(raw) if raw else {} + except Exception: + return {"result": raw.decode("utf-8", "replace")} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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(cfg, inputs): + ip = inputs.get("ip") + if not ip: + raise Exception("ip is required") + + return request("GET", "/ip_address_list", cfg, params={"WHERE": "hostaddr='" + ip + "'"}) + + +_run(main) diff --git a/integrations/efficientip/scripts/test_connection.py b/integrations/efficientip/scripts/test_connection.py new file mode 100644 index 0000000..d454f71 --- /dev/null +++ b/integrations/efficientip/scripts/test_connection.py @@ -0,0 +1,57 @@ +import json, os, sys, base64, ssl, 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 + + +def _auth(cfg): + raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def request(method, path, cfg, params=None): + url = str(cfg.get("base_url", "")).rstrip("/") + "/rest" + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + req = urllib.request.Request(url, headers={"Authorization": _auth(cfg), "Accept": "application/json"}, method=method) + with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r: + raw = r.read() + try: + return json.loads(raw) if raw else {} + except Exception: + return {"result": raw.decode("utf-8", "replace")} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + 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(cfg, inputs): + request("GET", "/dns_server_list", cfg, params={"limit": 1}) + return {"ok": True} + + +_run(main)