diff --git a/integrations/censys/manifest.yaml b/integrations/censys/manifest.yaml new file mode 100644 index 0000000..2b0ba1a --- /dev/null +++ b/integrations/censys/manifest.yaml @@ -0,0 +1,50 @@ +id: censys +name: Censys +version: 1.0.0 +description: "Censys Search (API v2) — host (IP) details from internet-wide scanning: open services/ports, software, certificates and location, plus a Censys Search Language host query. API ID + secret (Basic) authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: host lookup by IP and host search." +category: enrichment + +config_schema: + properties: + api_id: + type: string + description: "Censys API ID" + api_secret: + type: string + description: "Censys API secret" + x-soar-sensitive: true + required: + - api_id + - api_secret + +commands: + - id: host + name: censys-host + description: "Get the current view of a host by IP (services, ports, software, certificates, location)." + risk: read + inputs_schema: + properties: + ip: { type: string, description: "IP address" } + required: [ip] + outputs_schema: { properties: {} } + - id: host_search + name: censys-host-search + description: "Search hosts with a Censys Search Language query (e.g. services.service_name: HTTP and location.country: France)." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Censys Search Language query" } + per_page: { type: number, description: "Results per page (default 50, max 100)" } + cursor: { type: string, description: "Pagination cursor for the next page" } + required: [query] + outputs_schema: { properties: {} } + + - id: test_connection + name: censys-test-connection + description: "Verify the API credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/censys/scripts/host.py b/integrations/censys/scripts/host.py new file mode 100644 index 0000000..eb4efc1 --- /dev/null +++ b/integrations/censys/scripts/host.py @@ -0,0 +1,43 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://search.censys.io/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def request(method, path, params=None): + url = API + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + cfg = _cfg() + cred = (str(cfg.get("api_id") or "") + ":" + str(cfg.get("api_secret") or "")).encode("utf-8") + headers = {"Accept": "application/json", "Authorization": "Basic " + base64.b64encode(cred).decode("ascii")} + req = urllib.request.Request(url, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ip = inputs.get("ip") + if not ip: + raise Exception("ip is required") + result = request("GET", "/hosts/" + q(ip)) + print(json.dumps(result)) + + +try: + main() +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) diff --git a/integrations/censys/scripts/host_search.py b/integrations/censys/scripts/host_search.py new file mode 100644 index 0000000..ccf6474 --- /dev/null +++ b/integrations/censys/scripts/host_search.py @@ -0,0 +1,45 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://search.censys.io/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def request(method, path, params=None): + url = API + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + cfg = _cfg() + cred = (str(cfg.get("api_id") or "") + ":" + str(cfg.get("api_secret") or "")).encode("utf-8") + headers = {"Accept": "application/json", "Authorization": "Basic " + base64.b64encode(cred).decode("ascii")} + req = urllib.request.Request(url, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + query = inputs.get("query") + if not query: + raise Exception("query is required") + per_page = inputs.get("per_page") + cursor = inputs.get("cursor") + result = request("GET", "/hosts/search", {"q": query, "per_page": per_page or 50, "cursor": cursor or None}) + print(json.dumps(result)) + + +try: + main() +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) diff --git a/integrations/censys/scripts/test_connection.py b/integrations/censys/scripts/test_connection.py new file mode 100644 index 0000000..836e2cf --- /dev/null +++ b/integrations/censys/scripts/test_connection.py @@ -0,0 +1,41 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://search.censys.io/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def request(method, path, params=None): + url = API + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + cfg = _cfg() + cred = (str(cfg.get("api_id") or "") + ":" + str(cfg.get("api_secret") or "")).encode("utf-8") + headers = {"Accept": "application/json", "Authorization": "Basic " + base64.b64encode(cred).decode("ascii")} + req = urllib.request.Request(url, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + result = request("GET", "/hosts/8.8.8.8") + if not isinstance(result, dict) or "result" not in result: + raise Exception("unexpected response") + print(json.dumps({"ok": True})) + + +try: + main() +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)