From fcf516ca82d0e88323dea7ec854da9cacfab0a20 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 27 Jun 2026 15:19:40 +0200 Subject: [PATCH] feat(recorded-future): Recorded Future + ASI integrations Recorded Future (enrichment): native ConnectAPI v2 (X-RFToken). ip/domain/url/ file/cve risk reputation, full entity intelligence, and alert ingestion (alerts search) with an OCSF mapper and a 'Recorded Future Alert' default type, plus alert lookup and alert-rule search. Recorded Future ASI (enrichment): Attack Surface Intelligence (SecurityTrails API, APIKEY header, project-scoped). Project issue ingestion (project_issues) with an OCSF mapper and a 'Recorded Future ASI Issue' default type, filtered by a configurable minimum severity, plus recent-issues and recent-issues-by-host queries. The XSOAR-gateway packs (alerts/lists) were re-implemented against Recorded Future's native ConnectAPI rather than the XSOAR-coupled gateway protocol. Co-Authored-By: Claude Opus 4.8 --- .../recorded-future-asi-issue.yaml | 3 + .../recorded-future-asi/manifest.yaml | 84 ++++++++++ .../mappers/project_issues.yaml | 18 +++ .../scripts/project_issues.py | 51 ++++++ .../scripts/recent_issues.py | 47 ++++++ .../scripts/recent_issues_by_host.py | 48 ++++++ .../scripts/test_connection.py | 40 +++++ .../incident-types/recorded-future-alert.yaml | 3 + integrations/recorded-future/manifest.yaml | 148 ++++++++++++++++++ .../recorded-future/mappers/alerts.yaml | 19 +++ .../recorded-future/scripts/alert_lookup.py | 35 +++++ .../recorded-future/scripts/alert_rules.py | 38 +++++ .../recorded-future/scripts/alerts.py | 50 ++++++ integrations/recorded-future/scripts/cve.py | 39 +++++ .../recorded-future/scripts/domain.py | 39 +++++ integrations/recorded-future/scripts/file.py | 39 +++++ .../recorded-future/scripts/intelligence.py | 47 ++++++ integrations/recorded-future/scripts/ip.py | 39 +++++ .../scripts/test_connection.py | 36 +++++ integrations/recorded-future/scripts/url.py | 39 +++++ 20 files changed, 862 insertions(+) create mode 100644 integrations/recorded-future-asi/incident-types/recorded-future-asi-issue.yaml create mode 100644 integrations/recorded-future-asi/manifest.yaml create mode 100644 integrations/recorded-future-asi/mappers/project_issues.yaml create mode 100644 integrations/recorded-future-asi/scripts/project_issues.py create mode 100644 integrations/recorded-future-asi/scripts/recent_issues.py create mode 100644 integrations/recorded-future-asi/scripts/recent_issues_by_host.py create mode 100644 integrations/recorded-future-asi/scripts/test_connection.py create mode 100644 integrations/recorded-future/incident-types/recorded-future-alert.yaml create mode 100644 integrations/recorded-future/manifest.yaml create mode 100644 integrations/recorded-future/mappers/alerts.yaml create mode 100644 integrations/recorded-future/scripts/alert_lookup.py create mode 100644 integrations/recorded-future/scripts/alert_rules.py create mode 100644 integrations/recorded-future/scripts/alerts.py create mode 100644 integrations/recorded-future/scripts/cve.py create mode 100644 integrations/recorded-future/scripts/domain.py create mode 100644 integrations/recorded-future/scripts/file.py create mode 100644 integrations/recorded-future/scripts/intelligence.py create mode 100644 integrations/recorded-future/scripts/ip.py create mode 100644 integrations/recorded-future/scripts/test_connection.py create mode 100644 integrations/recorded-future/scripts/url.py diff --git a/integrations/recorded-future-asi/incident-types/recorded-future-asi-issue.yaml b/integrations/recorded-future-asi/incident-types/recorded-future-asi-issue.yaml new file mode 100644 index 0000000..bb4f8f8 --- /dev/null +++ b/integrations/recorded-future-asi/incident-types/recorded-future-asi-issue.yaml @@ -0,0 +1,3 @@ +name: "Recorded Future ASI Issue" +color: "#f4a261" +icon: "alert" diff --git a/integrations/recorded-future-asi/manifest.yaml b/integrations/recorded-future-asi/manifest.yaml new file mode 100644 index 0000000..d644a55 --- /dev/null +++ b/integrations/recorded-future-asi/manifest.yaml @@ -0,0 +1,84 @@ +id: recorded_future_asi +name: Recorded Future ASI +version: 1.0.0 +description: "Recorded Future Attack Surface Intelligence (SecurityTrails) — fetch attack-surface risk issues for a project (ingestion), and query recent added issues globally or grouped by host." +changelog: "1.0.0 — Initial release: project issue ingestion (project_issues) with an OCSF mapper, plus recent-issues and recent-issues-by-host queries." +category: enrichment + +# Per-instance configuration. The SecurityTrails ASI API is authenticated with an +# API key sent in the APIKEY header and scoped to a project. +config_schema: + properties: + api_key: + type: string + description: "SecurityTrails / ASI API key" + x-soar-sensitive: true + project_id: + type: string + description: "ASI Project ID to fetch issues from" + min_severity: + type: string + description: "Minimum issue severity to fetch: Informational, Moderate or Critical (default Moderate)" + default: Moderate + required: + - api_key + - project_id + +auth: + - id: apikey + type: api_key + in: header + name: APIKEY + value_template: "{{secret}}" + secret_field: api_key + +commands: + # ── Ingestion ─────────────────────────────────────────────────────────────── + - id: project_issues + name: asi-project-issues + description: "Fetch the current attack-surface risk issues for the project. Used for ingestion: results path = data. Issues below the configured minimum severity are filtered out." + risk: read + inputs_schema: + properties: + snapshot: { type: string, description: "Snapshot to read (default 'recent')" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: data + dedup_key: name + + - id: recent_issues + name: asi-recent-issues + description: "List risk issues added to the project since a given time, filtered by the configured minimum severity." + risk: read + inputs_schema: + properties: + start: { type: string, description: "Lower bound: a timestamp or snapshot date" } + required: [] + outputs_schema: { properties: {} } + + - id: recent_issues_by_host + name: asi-recent-issues-by-host + description: "List hosts with risk issues added since a given time, with per-host risk-score changes." + risk: read + inputs_schema: + properties: + last_checked: { type: string, description: "Lower bound: a timestamp or snapshot date" } + limit: { type: number, description: "Maximum number of hosts to return (default 200)" } + required: [] + outputs_schema: { properties: {} } + + # ── Connectivity test ───────────────────────────────────────────────────── + - id: test_connection + name: asi-test-connection + description: "Verify connectivity, credentials and project access (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: project_issues + mapper: project_issues + default_incident_type: "Recorded Future ASI Issue" diff --git a/integrations/recorded-future-asi/mappers/project_issues.yaml b/integrations/recorded-future-asi/mappers/project_issues.yaml new file mode 100644 index 0000000..681a971 --- /dev/null +++ b/integrations/recorded-future-asi/mappers/project_issues.yaml @@ -0,0 +1,18 @@ +name: "Recorded Future ASI Issues → OCSF" +description: "Maps a Recorded Future ASI risk issue (project issues, results_path = data) to OCSF Detection Finding fields. Each issue is a triggered risk rule with example affected hosts." +field_mappings: + title: "name" + description: "description" + # _severity is injected by the fetch script (high=5, moderate=3, informational=1). + severity: "_severity" +# results_path = data; source_path is JSONata over ONE issue object. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "name", ocsf_field: "finding_info.uid" } + - { source_path: "name", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "classification", ocsf_field: "activity_name" } + - { source_path: "rule_metadata.references[0]", ocsf_field: "finding_info.src_url" } + # ── Example affected host ───────────────────────────────────────── + - { source_path: "example_entities.domains[0].example", ocsf_field: "src_endpoint.hostname" } + - { source_path: "example_entities.ips[0].example", ocsf_field: "src_endpoint.ip" } diff --git a/integrations/recorded-future-asi/scripts/project_issues.py b/integrations/recorded-future-asi/scripts/project_issues.py new file mode 100644 index 0000000..58475c7 --- /dev/null +++ b/integrations/recorded-future-asi/scripts/project_issues.py @@ -0,0 +1,51 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +MIN_SEVERITY = {"Informational": {"high", "moderate", "informational"}, "Moderate": {"high", "moderate"}, "Critical": {"high"}} +SEV_NUM = {"high": 5, "moderate": 3, "informational": 1} + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = "https://api.securitytrails.com/v1/asi" + headers = {"APIKEY": s.get("api_key", ""), "Accept": "application/json"} + project = str(s.get("project_id", "")) + min_sev = str(s.get("min_severity") or "Moderate") + return base, headers, project, min_sev + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers, _, _ = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, _, project, min_sev = _cfg() + snapshot = _inputs().get("snapshot") or "recent" + out = request("/rules/" + urllib.parse.quote(project, safe="") + "/" + urllib.parse.quote(snapshot, safe="") + "/issues") + allowed = MIN_SEVERITY.get(min_sev, MIN_SEVERITY["Moderate"]) + issues = [] + for issue in out.get("data", []): + cls = str(issue.get("classification", "")).lower() + if cls and cls not in allowed: + continue + # Inject a 1-5 severity so the mapper can map it without string comparisons. + issue["_severity"] = SEV_NUM.get(cls, 2) + issues.append(issue) + print(json.dumps({"data": issues, "meta": out.get("meta", {})})) + + +try: + run() +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/recorded-future-asi/scripts/recent_issues.py b/integrations/recorded-future-asi/scripts/recent_issues.py new file mode 100644 index 0000000..9645b2e --- /dev/null +++ b/integrations/recorded-future-asi/scripts/recent_issues.py @@ -0,0 +1,47 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +MIN_SEVERITY = {"Informational": "high,moderate,informational", "Moderate": "high,moderate", "Critical": "high"} + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = "https://api.securitytrails.com/v1/asi" + headers = {"APIKEY": s.get("api_key", ""), "Accept": "application/json"} + project = str(s.get("project_id", "")) + min_sev = str(s.get("min_severity") or "Moderate") + return base, headers, project, min_sev + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers, _, _ = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, _, project, min_sev = _cfg() + inp = _inputs() + params = { + "rule_action": "added", + "start": inp.get("start"), + "classification": MIN_SEVERITY.get(min_sev, "high,moderate"), + } + print(json.dumps(request("/rules/history/" + urllib.parse.quote(project, safe="") + "/activity", params))) + + +try: + run() +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/recorded-future-asi/scripts/recent_issues_by_host.py b/integrations/recorded-future-asi/scripts/recent_issues_by_host.py new file mode 100644 index 0000000..d397315 --- /dev/null +++ b/integrations/recorded-future-asi/scripts/recent_issues_by_host.py @@ -0,0 +1,48 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +MIN_SEVERITY = {"Informational": "high,moderate,informational", "Moderate": "high,moderate", "Critical": "high"} + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = "https://api.securitytrails.com/v1/asi" + headers = {"APIKEY": s.get("api_key", ""), "Accept": "application/json"} + project = str(s.get("project_id", "")) + min_sev = str(s.get("min_severity") or "Moderate") + return base, headers, project, min_sev + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers, _, _ = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, _, project, min_sev = _cfg() + inp = _inputs() + params = { + "rule_action": "added", + "last_checked": inp.get("last_checked"), + "classification": MIN_SEVERITY.get(min_sev, "high,moderate"), + "limit": inp.get("limit") or 200, + } + print(json.dumps(request("/rules/history/" + urllib.parse.quote(project, safe="") + "/activity/by_host/compare", params))) + + +try: + run() +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/recorded-future-asi/scripts/test_connection.py b/integrations/recorded-future-asi/scripts/test_connection.py new file mode 100644 index 0000000..57a044d --- /dev/null +++ b/integrations/recorded-future-asi/scripts/test_connection.py @@ -0,0 +1,40 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = "https://api.securitytrails.com/v1/asi" + headers = {"APIKEY": s.get("api_key", ""), "Accept": "application/json"} + project = str(s.get("project_id", "")) + return base, headers, project + + +def request(path): + base, headers, _ = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + _, _, project = _cfg() + request("/rules/" + urllib.parse.quote(project, safe="") + "/recent/issues") + print(json.dumps({"ok": True})) + + +try: + run() +except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", "replace") + if e.code in (401, 403): + msg = "API key is not valid or has no access to this project." + elif e.code == 404: + msg = "Project not found. Check the Project ID." + else: + msg = "HTTP " + str(e.code) + print(json.dumps({"ok": False, "error": msg, "detail": detail})) + sys.exit(1) +except Exception as e: + print(json.dumps({"ok": False, "error": str(e)})) + sys.exit(1) diff --git a/integrations/recorded-future/incident-types/recorded-future-alert.yaml b/integrations/recorded-future/incident-types/recorded-future-alert.yaml new file mode 100644 index 0000000..0e136cc --- /dev/null +++ b/integrations/recorded-future/incident-types/recorded-future-alert.yaml @@ -0,0 +1,3 @@ +name: "Recorded Future Alert" +color: "#e63946" +icon: "alert" diff --git a/integrations/recorded-future/manifest.yaml b/integrations/recorded-future/manifest.yaml new file mode 100644 index 0000000..cf8b1fd --- /dev/null +++ b/integrations/recorded-future/manifest.yaml @@ -0,0 +1,148 @@ +id: recorded_future +name: Recorded Future +version: 1.0.0 +description: "Recorded Future ConnectAPI (v2) — risk reputation for IPs, domains, URLs, file hashes and vulnerabilities, full entity intelligence (risk rules, related entities, threat lists), and alert ingestion (search/lookup/rules)." +changelog: "1.0.0 — Initial release: ip/domain/url/file/cve reputation, entity intelligence, alert ingestion (search) with an OCSF mapper, alert lookup and alert-rule search." +category: enrichment + +# Per-instance configuration. The ConnectAPI is authenticated with an +# Organization API token sent in the X-RFToken header. +config_schema: + properties: + server_url: + type: string + description: "Recorded Future API base URL" + default: https://api.recordedfuture.com + api_token: + type: string + description: "Recorded Future API token" + x-soar-sensitive: true + required: + - api_token + +auth: + - id: token + type: api_key + in: header + name: X-RFToken + value_template: "{{secret}}" + secret_field: api_token + +commands: + # ── Reputation ────────────────────────────────────────────────────────────── + - id: ip + name: recordedfuture-ip + description: "Risk reputation for an IP address (risk score, level and triggered risk rules)." + risk: read + inputs_schema: + properties: + ip: { type: string, description: "IP address" } + required: [ip] + outputs_schema: { properties: {} } + + - id: domain + name: recordedfuture-domain + description: "Risk reputation for a domain (risk score, level and triggered risk rules)." + risk: read + inputs_schema: + properties: + domain: { type: string, description: "Domain name" } + required: [domain] + outputs_schema: { properties: {} } + + - id: url + name: recordedfuture-url + description: "Risk reputation for a URL (risk score, level and triggered risk rules)." + risk: read + inputs_schema: + properties: + url: { type: string, description: "URL" } + required: [url] + outputs_schema: { properties: {} } + + - id: file + name: recordedfuture-file + description: "Risk reputation for a file hash (MD5, SHA1, SHA256, SHA512)." + risk: read + inputs_schema: + properties: + file: { type: string, description: "File hash" } + required: [file] + outputs_schema: { properties: {} } + + - id: cve + name: recordedfuture-cve + description: "Risk reputation for a vulnerability (CVE)." + risk: read + inputs_schema: + properties: + cve: { type: string, description: "CVE identifier (e.g. CVE-2021-44228)" } + required: [cve] + outputs_schema: { properties: {} } + + - id: intelligence + name: recordedfuture-intelligence + description: "Full intelligence for a single entity: risk, triggered rules, related entities, metrics and threat lists." + risk: read + inputs_schema: + properties: + entity_type: { type: string, description: "Entity type: ip, domain, url, file or cve" } + entity: { type: string, description: "Entity value (IP, domain, URL, hash or CVE)" } + required: [entity_type, entity] + outputs_schema: { properties: {} } + + # ── Alert ingestion ───────────────────────────────────────────────────────── + - id: alerts + name: recordedfuture-alerts + description: "Search alerts. Used for ingestion: results path = data.results. Filter by triggered time, status and alert rule." + risk: read + inputs_schema: + properties: + triggered: { type: string, description: "Triggered-time filter. A bare timestamp/relative value is treated as the lower bound ('[,]'); a range '[from,to]' is passed through. Incremental fetch watermark." } + status: { type: string, description: "Review status (unassigned, assigned, pending, actionable, no-action, tuning)" } + alert_rule: { type: string, description: "Alert rule ID to filter by" } + freetext: { type: string, description: "Free-text search" } + direction: { type: string, description: "Sort direction (asc, desc). Default desc." } + limit: { type: number, description: "Maximum number of alerts (default 50)" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: data.results + dedup_key: id + incremental_field: triggered + + - id: alert_lookup + name: recordedfuture-alert-lookup + description: "Look up a single alert by ID." + risk: read + inputs_schema: + properties: + alert_id: { type: string, description: "Alert ID" } + required: [alert_id] + outputs_schema: { properties: {} } + + - id: alert_rules + name: recordedfuture-alert-rules + description: "Search alert rule IDs by name." + risk: read + inputs_schema: + properties: + rule_name: { type: string, description: "Rule name to search (partial allowed)" } + limit: { type: number, description: "Maximum number of rules (default 10)" } + required: [] + outputs_schema: { properties: {} } + + # ── Connectivity test ───────────────────────────────────────────────────── + - id: test_connection + name: recordedfuture-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: alerts + mapper: alerts + default_incident_type: "Recorded Future Alert" diff --git a/integrations/recorded-future/mappers/alerts.yaml b/integrations/recorded-future/mappers/alerts.yaml new file mode 100644 index 0000000..70c1da9 --- /dev/null +++ b/integrations/recorded-future/mappers/alerts.yaml @@ -0,0 +1,19 @@ +name: "Recorded Future Alerts → OCSF" +description: "Maps a Recorded Future alert (/v2/alert/search, results_path = data.results) to OCSF Detection Finding fields. Alerts are generated by alerting rules over Recorded Future intelligence." +field_mappings: + title: "title" +# results_path = data.results; source_path is JSONata over ONE alert object. +# Paths absent from a given alert return nothing and are skipped. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "title", ocsf_field: "finding_info.title" } + - { source_path: "triggered", ocsf_field: "finding_info.created_time" } + - { source_path: "url", ocsf_field: "finding_info.src_url" } + # ── Triggering rule (analytic) ──────────────────────────────────── + - { source_path: "rule.name", ocsf_field: "finding_info.analytic.name" } + - { source_path: "rule.id", ocsf_field: "finding_info.analytic.uid" } + # ── Incident state ──────────────────────────────────────────────── + - { source_path: "review.status", ocsf_field: "status" } + - { source_path: "type", ocsf_field: "activity_name" } + - { source_path: "review.assignee", ocsf_field: "assignee.name" } diff --git a/integrations/recorded-future/scripts/alert_lookup.py b/integrations/recorded-future/scripts/alert_lookup.py new file mode 100644 index 0000000..efa28b5 --- /dev/null +++ b/integrations/recorded-future/scripts/alert_lookup.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + alert_id = urllib.parse.quote(str(_inputs().get("alert_id", "")), safe="") + print(json.dumps(request("/v2/alert/" + alert_id))) + + +try: + run() +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/recorded-future/scripts/alert_rules.py b/integrations/recorded-future/scripts/alert_rules.py new file mode 100644 index 0000000..8f3c72c --- /dev/null +++ b/integrations/recorded-future/scripts/alert_rules.py @@ -0,0 +1,38 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + params = {"freetext": inp.get("rule_name"), "limit": inp.get("limit") or 10} + print(json.dumps(request("/v2/alert/rule", params))) + + +try: + run() +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/recorded-future/scripts/alerts.py b/integrations/recorded-future/scripts/alerts.py new file mode 100644 index 0000000..d9fba1f --- /dev/null +++ b/integrations/recorded-future/scripts/alerts.py @@ -0,0 +1,50 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + params = { + "limit": inp.get("limit") or 50, + "orderby": "triggered", + "direction": inp.get("direction") or "desc", + "status": inp.get("status"), + "alertRule": inp.get("alert_rule"), + "freetext": inp.get("freetext"), + } + trig = inp.get("triggered") + if trig: + t = str(trig) + # A bare timestamp/relative value becomes the lower bound of a range. + params["triggered"] = t if ("[" in t or "," in t) else ("[" + t + ",]") + print(json.dumps(request("/v2/alert/search", params))) + + +try: + run() +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/recorded-future/scripts/cve.py b/integrations/recorded-future/scripts/cve.py new file mode 100644 index 0000000..d1b21c2 --- /dev/null +++ b/integrations/recorded-future/scripts/cve.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + value = str(_inputs().get("cve", "")) + out = request("/v2/vulnerability/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps,intelCard,cvssv3,nvdDescription"}) + print(json.dumps(out)) + + +try: + run() +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/recorded-future/scripts/domain.py b/integrations/recorded-future/scripts/domain.py new file mode 100644 index 0000000..0501051 --- /dev/null +++ b/integrations/recorded-future/scripts/domain.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + value = str(_inputs().get("domain", "")) + out = request("/v2/domain/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps,intelCard"}) + print(json.dumps(out)) + + +try: + run() +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/recorded-future/scripts/file.py b/integrations/recorded-future/scripts/file.py new file mode 100644 index 0000000..58ca304 --- /dev/null +++ b/integrations/recorded-future/scripts/file.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + value = str(_inputs().get("file", "")) + out = request("/v2/hash/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps,intelCard,hashAlgorithm"}) + print(json.dumps(out)) + + +try: + run() +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/recorded-future/scripts/intelligence.py b/integrations/recorded-future/scripts/intelligence.py new file mode 100644 index 0000000..4ee1ba5 --- /dev/null +++ b/integrations/recorded-future/scripts/intelligence.py @@ -0,0 +1,47 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + +# Map a user-facing entity type to the ConnectAPI path segment. +TYPE_MAP = {"ip": "ip", "domain": "domain", "url": "url", "file": "hash", "hash": "hash", "cve": "vulnerability", "vulnerability": "vulnerability"} + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + rf_type = TYPE_MAP.get(str(inp.get("entity_type", "")).lower()) + if not rf_type: + print(json.dumps({"error": "Unsupported entity_type. Use ip, domain, url, file or cve."})) + sys.exit(1) + value = str(inp.get("entity", "")) + out = request("/v2/" + rf_type + "/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps,intelCard,relatedEntities,metrics,threatLists"}) + print(json.dumps(out)) + + +try: + run() +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/recorded-future/scripts/ip.py b/integrations/recorded-future/scripts/ip.py new file mode 100644 index 0000000..f9da146 --- /dev/null +++ b/integrations/recorded-future/scripts/ip.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + value = str(_inputs().get("ip", "")) + out = request("/v2/ip/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps,intelCard,location,riskyCIDRIPs"}) + print(json.dumps(out)) + + +try: + run() +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/recorded-future/scripts/test_connection.py b/integrations/recorded-future/scripts/test_connection.py new file mode 100644 index 0000000..437b8ca --- /dev/null +++ b/integrations/recorded-future/scripts/test_connection.py @@ -0,0 +1,36 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + # A minimal entity lookup validates both connectivity and the token. + request("/v2/ip/8.8.8.8", {"fields": "entity"}) + print(json.dumps({"ok": True})) + + +try: + run() +except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", "replace") + msg = "API token is not valid." if e.code in (401, 403) else "HTTP " + str(e.code) + print(json.dumps({"ok": False, "error": msg, "detail": detail})) + sys.exit(1) +except Exception as e: + print(json.dumps({"ok": False, "error": str(e)})) + sys.exit(1) diff --git a/integrations/recorded-future/scripts/url.py b/integrations/recorded-future/scripts/url.py new file mode 100644 index 0000000..e928d59 --- /dev/null +++ b/integrations/recorded-future/scripts/url.py @@ -0,0 +1,39 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("server_url") or "https://api.recordedfuture.com").rstrip("/") + headers = {"X-RFToken": s.get("api_token", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + value = str(_inputs().get("url", "")) + out = request("/v2/url/" + urllib.parse.quote(value, safe=""), + {"fields": "risk,entity,timestamps"}) + print(json.dumps(out)) + + +try: + run() +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)