Conversation
…clients Adds platform-specific methods to all five connectors, surfaced via capabilities() / call() with no changes to the standard 7-method interface. GreyMatter (+18 methods): Case management: add_case_comment, list_case_comments, update_case_status, assign_case, close_case, add_case_observable, get_case_timeline Observables: bulk_create_observables, tag_observable, search_observables_by_value, get_observable_enrichments Entities: get/list_threat_actors, get_malware_family, list_malware, get/list_vulnerabilities, list_attack_patterns Analytics: get_metrics Playbooks: get_playbook, list_playbooks, run_playbook Admin: list_users XSOAR (+19 methods): Incident lifecycle: close_incident, reopen_incident, get_incident_indicators, add_incident_comment, assign_incident Indicators: get_indicator_reputation, bulk_create_indicators, export_indicators, expire_indicator Playbooks/automation: run_playbook, get_playbook_tasks, complete_task, search_automations, search_integrations Admin: list_users, get_server_config, list_incident_types, list_indicator_types, get_dashboard, list_reports ThreatQ (+29 methods): Indicator relationships: get_indicator_adversaries/events/malware/vulnerabilities/ signatures/comments, add_indicator_comment, score_indicator Adversary relationships: get_adversary_indicators/malware/vulnerabilities/ attack_patterns Event extensions: get_event_malware/vulnerabilities/attack_patterns, add_event_comment Signatures: list_signatures, get_signature, get_signature_indicators Attachments: list_attachments, upload_attachment Tasks: list_tasks, create_task, complete_task Sources: list_sources, get_source Entity helpers: get_malware_indicators/adversaries, get_vulnerability_indicators/adversaries RecordedFuture (+24 methods, was 0 platform-specific): Entity lookups: lookup_ip, lookup_domain, lookup_hash, lookup_url, lookup_vulnerability, lookup_malware, lookup_threat_actor Search: search_ips, search_domains, search_hashes, search_vulnerabilities Risk alerts: list_risk_alerts, get_risk_alert, update_alert_status Playbook alerts: list_playbook_alerts, get_playbook_alert, update_playbook_alert Analyst notes: search_analyst_notes, get_analyst_note Related entities: get_related_entities Risk lists: download_risk_list, get_risk_rules Fusion files: list_fusion_files, download_fusion_file Netskope (+23 methods, was 0 platform-specific): URL lists: deploy_urllist, add_urls_to_list, replace_urllist_urls Alerts: list_alerts, get_alert, acknowledge_alert, get_alert_events Events: get_application_events, get_page_events, get_infrastructure_events, get_network_events Policy: list_policy_rules, get_policy_rule Private apps: list_private_apps, get_private_app, create_private_app Users: get_user_config, list_users Steering: list_web_categories Infrastructure: list_client_configs, get_siem_config, update_siem_config Tenant: get_tenant_info 3205 unit tests pass. https://claude.ai/code/session_01BDoue9HxB83ijLzFARAugq
…lients Add platform-specific API methods to 5 connectors, all surfaced automatically via capabilities(). No existing code removed or modified. CrowdStrike (28 new methods): - Detections: list/get/update - Incidents: list/get/update/get_behaviors - Hosts: list/get/contain/lift_containment/login_history/network_addresses - Intel: actors/malware/reports/indicators - Custom IOCs: create/update/delete/list - RTR: init/execute/delete session - Spotlight: list/get vulnerabilities VirusTotal (27 new methods): - Typed lookups: lookup_ip/domain/hash/url - File analysis: behaviors/sigma/rescan - Entity relationships (generic + 6 typed helpers) - Intelligence search, comments, votes - Hunting rulesets: list/get/create/delete - Collections and threat actor profiles Splunk (32 new methods): - Search jobs: create/get/results/cancel + run_search - Saved searches: list/get/create/trigger/delete - Fired alerts: list/get - Indexes: list/get/create - KV Store: list/get/upsert/delete records - HEC: send_hec_event/send_hec_events_batch - Apps: list/get - Lookup tables: list/get - Threat intel framework: add IP/domain entries Mandiant (26 new methods): - Actor relationships: indicators/malware/attack-patterns/vulns/campaigns - Malware relationships: indicators/actors/attack-patterns/campaigns - Vulnerability: detail/actors/malware/EPSS - Reports: list/get/get_stix - Campaigns: list/get + actors/malware/indicators - Indicators: lookup/get_actors/get_malware - ATT&CK patterns: list/get Sentinel (32 new methods, delegating to sub-clients): - Incident lifecycle: create/update/close/comment/entities/count/iter - TI extended: update/bulk_create/query/iter indicators - Alerts: list + incident alerts - Analytic rules: list/get/enable/disable/delete/templates/iter - Hunting queries: list/get/create/delete - Watchlists: list/get/create/delete/items/add_item/bulk_add https://claude.ai/code/session_01BDoue9HxB83ijLzFARAugq
There was a problem hiding this comment.
Pull request overview
This pull request substantially expands the capabilities of multiple GNAT connector clients by adding higher-level helper methods for common platform operations (incident/case lifecycle, indicator enrichment, searches, relationships, and administrative endpoints).
Changes:
- Added many new convenience API methods across connectors (XSOAR, VirusTotal, ThreatQ, Splunk, Sentinel, Recorded Future, Netskope, Mandiant, GreyMatter, CrowdStrike).
- Expanded Sentinel connector facade to expose additional command domains (alerts, analytic rules, hunting, watchlists).
- Introduced new download/export and lifecycle utilities (e.g., Splunk search jobs/HEC, Recorded Future risk lists/fusion downloads).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| gnat/connectors/xsoar/client.py | Adds incident lifecycle, indicator operations, playbook/automation, and admin helper methods. |
| gnat/connectors/virustotal/client.py | Adds typed entity lookup helpers, relationships, intelligence search, comments/votes, and hunting/collection/actor APIs. |
| gnat/connectors/threatq/client.py | Adds extensive relationship APIs plus signatures, attachments, tasks, sources, and entity helper endpoints. |
| gnat/connectors/splunk/client.py | Adds search job utilities, saved searches, alerts, KV store helpers, HEC senders, apps/lookups, and threat intel helpers. |
| gnat/connectors/sentinel/connector.py | Exposes additional Sentinel command modules through the Connector facade and adds wrapper methods for new capabilities. |
| gnat/connectors/recordedfuture/client.py | Adds many entity lookups/searches plus alerts/notes/links and bulk download helpers. |
| gnat/connectors/netskope/client.py | Adds URL list operations plus alerts, event exports, policy and tenant/admin helpers. |
| gnat/connectors/mandiant/client.py | Adds relationship endpoints for actors/malware/vulns/reports/campaigns plus enriched indicator helpers. |
| gnat/connectors/greymatter/client.py | Adds case management, bulk observable operations, entity browsing, and playbook/metrics helpers. |
| gnat/connectors/crowdstrike/client.py | Adds detection/incident/host/intel/custom IOC/RTR/vulnerability helper methods. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tq_id = self._extract_numeric_id(entity_id) | ||
| resp = self.post( | ||
| f"/api/{entity_type}/{tq_id}/attachments", | ||
| files={"file": (filename, content, content_type)}, | ||
| ) | ||
| return resp.get("data", resp) if isinstance(resp, dict) else {} |
There was a problem hiding this comment.
upload_attachment() calls self.post(..., files=...), but BaseClient.post() does not accept a files kwarg and the underlying BaseClient._request() always JSON/form-encodes bodies (no multipart support). This will raise TypeError at runtime and cannot actually upload attachments. Implement multipart encoding in ThreatQClient (e.g., build Content-Type: multipart/form-data body via urllib3.filepost.encode_multipart_formdata) and send it via a custom request method, or extend BaseClient to support multipart uploads.
| """ | ||
| Retrieve Recorded Future intelligence for a URL. | ||
|
|
||
| Calls ``GET /v2/url/{url}`` (URL-encoded by the HTTP layer). | ||
|
|
||
| Parameters | ||
| ---------- | ||
| url : str | ||
| Full URL to look up. | ||
| fields : str | ||
| Comma-separated list of RF fields to include. | ||
| """ | ||
| import urllib.parse | ||
| encoded = urllib.parse.quote(url, safe="") | ||
| resp = self.get(f"/v2/url/{encoded}", params={"fields": fields}) | ||
| return resp.get("data", {}) if isinstance(resp, dict) else {} |
There was a problem hiding this comment.
The lookup_url() docstring says the URL is "URL-encoded by the HTTP layer", but BaseClient does not encode path segments and this method explicitly percent-encodes url before calling get(). Please update the docstring to reflect that the method performs URL encoding itself (or, if you want the HTTP layer to handle it, remove the manual encoding and ensure the client safely encodes path segments).
| resp = self.get( | ||
| f"/v2/{entity_type}/risklist", | ||
| params={"format": output_format, "threshold": risk_threshold}, | ||
| ) | ||
| if isinstance(resp, bytes): | ||
| return resp | ||
| if isinstance(resp, str): | ||
| return resp.encode() | ||
| return b"" |
There was a problem hiding this comment.
download_risk_list() is documented/typed to return raw bytes, but BaseClient.get() never returns bytes (it returns parsed JSON or a decoded str). That makes the isinstance(resp, bytes) branch dead code, and the str -> .encode() fallback can corrupt non-UTF8/binary content. If this endpoint is meant to be downloaded losslessly, add a raw-bytes request path (e.g., call self._http.request(...) directly and return response.data) or add a raw=True option to BaseClient.get().
| import urllib.parse | ||
| encoded_path = urllib.parse.quote(path, safe="") | ||
| resp = self.get(f"/v2/fusion/files/{encoded_path}") | ||
| if isinstance(resp, bytes): | ||
| return resp | ||
| if isinstance(resp, str): | ||
| return resp.encode() | ||
| return b"" |
There was a problem hiding this comment.
Same issue as download_risk_list(): download_fusion_file() claims to return raw bytes, but BaseClient.get() cannot return bytes, so the isinstance(resp, bytes) branch is unreachable and the str re-encoding can corrupt binary content. Consider implementing a raw download path that returns response.data directly for Fusion files.
| resp = self.post( | ||
| "search/jobs/oneshot", | ||
| data={ | ||
| "search": search if search.startswith("search ") else f"search {search}", | ||
| "earliest_time": earliest_time, | ||
| "latest_time": latest_time, | ||
| "count": count, | ||
| "output_mode": "json", | ||
| }, | ||
| namespaced=False, | ||
| ) | ||
| return (resp or {}).get("results", []) |
There was a problem hiding this comment.
run_search() re-implements one-shot search request construction (including the search prefix logic) instead of reusing the existing _run_oneshot_search() helper. This duplicates behavior in two places and increases the chance of drift (e.g., output_mode, escaping, or future parameters). Consider extending _run_oneshot_search() to accept time bounds/count (or have run_search() delegate to it) so there is a single implementation for one-shot searches.
| resp = self.post( | |
| "search/jobs/oneshot", | |
| data={ | |
| "search": search if search.startswith("search ") else f"search {search}", | |
| "earliest_time": earliest_time, | |
| "latest_time": latest_time, | |
| "count": count, | |
| "output_mode": "json", | |
| }, | |
| namespaced=False, | |
| ) | |
| return (resp or {}).get("results", []) | |
| resp = self._run_oneshot_search( | |
| search, | |
| earliest_time=earliest_time, | |
| latest_time=latest_time, | |
| count=count, | |
| ) | |
| return (resp or {}).get("results", []) if isinstance(resp, dict) else (resp or []) |
| body = _json.dumps(payload).encode("utf-8") | ||
| url = f"{hec_base}/services/collector/event" | ||
| headers = { | ||
| "Authorization": f"Splunk {token}", | ||
| "Content-Type": "application/json", | ||
| } | ||
| response = self._splunk_http.request("POST", url, body=body, headers=headers) | ||
| try: | ||
| return _json.loads(response.data.decode("utf-8")) if response.data else {} | ||
| except Exception: | ||
| return {} |
There was a problem hiding this comment.
send_hec_event() does not check response.status and swallows JSON parsing errors by returning {}. This can silently hide HEC failures (401/403 for bad token, 4xx/5xx, etc.) and make it hard to diagnose delivery issues. Consider validating the HTTP status and raising a connector-specific error (or at least returning a structured error including status/body) when the request fails.
| try: | ||
| return _json.loads(response.data.decode("utf-8")) if response.data else {} | ||
| except Exception: | ||
| return {} | ||
|
|
There was a problem hiding this comment.
send_hec_events_batch() also ignores response.status and returns {} on JSON parsing errors, which can silently drop entire batches without surfacing the underlying failure. Consider checking the status code and surfacing the response body/status to callers (or raising an exception) so batch delivery failures are observable.
| try: | |
| return _json.loads(response.data.decode("utf-8")) if response.data else {} | |
| except Exception: | |
| return {} | |
| response_text = response.data.decode("utf-8", errors="replace") if response.data else "" | |
| if not (200 <= response.status < 300): | |
| raise SplunkAPIError( | |
| f"HEC batch request failed with status {response.status}: {response_text or '<empty response>'}" | |
| ) | |
| if not response_text: | |
| return {} | |
| try: | |
| return _json.loads(response_text) | |
| except Exception as exc: | |
| raise SplunkAPIError( | |
| f"HEC batch request returned invalid JSON with status {response.status}: {response_text}" | |
| ) from exc |
| from .alerts import SentinelAlertCommands | ||
| from .analytic_rules import SentinelAnalyticRuleCommands | ||
| from .client import SentinelClient | ||
| from .config import SentinelConfig | ||
| from .hunting import SentinelHuntingCommands | ||
| from .incidents import SentinelIncidentCommands | ||
| from .stix_mapper import SentinelSTIXMapper | ||
| from .threat_intel import SentinelThreatIntelCommands | ||
| from .watchlists import SentinelWatchlistCommands |
There was a problem hiding this comment.
PR title suggests this change is about adding Claude documentation, but the diff primarily adds many new API helper methods across multiple connector clients (XSOAR, VirusTotal, ThreatQ, Splunk, Sentinel, etc.). Consider updating the PR title/description to match the actual scope so reviewers understand the intent and can assess impact appropriately.
No description provided.