Hoxhunt v2
Use the Hoxhunt integration to send feedback to reporters of incidents, set incident sensitivity, and apply SOC classification to incidents.
Email · Hoxhunt
Details
| ID | Hoxhunt v2 |
|---|---|
| Provider | Hoxhunt |
| Category | |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Use the Hoxhunt integration to send feedback to reporters of incidents, set incident sensitivity, and apply SOC classification to incidents.
This integration was integrated and tested with version August 2024 of Hoxhunt.
Configure Hoxhunt v2 in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | e.g. https://api.hoxhunt.com/graphql-external | True |
| API Key | Input your api key from Hoxhunt | True |
| Fetch incidents | False | |
| Incidents Fetch Interval | False | |
| Incident type | False | |
| First fetch timestamp | (<number> <time unit>, e.g., 12 hours, 7 days). Defaults to 7 days. | False |
| Fetch limit | Maxium number of incidents per fetch. Cap is 100. | False |
| Only fetch escalated incidents | If enabled, only escalated incidents will be fetched. Set up escalation rules in Hoxhunt Response -> Incident Rules | False |
| Only fetch open incidents | If enabled, only open incidents will be fetched. | False |
| Use system proxy settings | False | |
| Incident Mirroring Direction | Choose the direction to mirror the incident: Incoming (from Hoxhunt to Cortex XSOAR), Outgoing (from Cortex XSOAR to Hoxhunt), or Incoming and Outgoing (from/to Cortex XSOAR and Hoxhunt). | False |
| Trust any certificate (not secure) | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
hoxhunt-current-user-get
Gets the current user information from Hoxhunt.
Base Command
hoxhunt-current-user-get
Input
| Argument Name | Description | Required |
| — | — | — |
Command Example
!hoxhunt-current-user-get
Context Output
| Path | Type | Description |
|---|---|---|
| HoxHunt.CurrentUser | string | Current User information from Hoxhunt. |
hoxhunt-incident-threats-get
Gets threats from Hoxhunt.
Base Command
hoxhunt-incident-threats-get
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident the threats will be from. | Required |
| sort | Sorting strategy to use when returning threats from API. Defaults to createdAt_DESC. See Hoxhunt API documentation for more information. | Optional |
| limit | Limit the amount of threats returned. Defaults to 50, maximum is 100. | Optional |
| filter | Filter string to use to pick only threats of interest. See Hoxhunt API documentation for more information. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.Threats | string | Threats from Hoxhunt. |
Command Example
!hoxhunt-incident-threats-get incident_id="12345" limit=5 sort="createdAt_DESC"
hoxhunt-incident-note-add
Add Incident note.
Base Command
hoxhunt-incident-note-add
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident to which the note will be added. | Required |
| note | The text content of the note to add to the Hoxhunt incident. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.addIncidentNode | string | Add Incident note. |
Command Example
!hoxhunt-incident-note-add incident_id="12345" note="Investigated and escalated to the SOC team."
hoxhunt-incident-threats-remove
Remove all threats that belong to an incident.
Base Command
hoxhunt-incident-threats-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident from which threats will be removed. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.removeIncidentThreats | number | Returns number of removed threats. |
Command Example
!hoxhunt-incident-threats-remove incident_id="12345"
hoxhunt-incident-soc-feedback-send
Send feedback to reporters of incident about whether the reported email was safe, spam or malicious.
Base Command
hoxhunt-incident-soc-feedback-send
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident for which SOC feedback will be sent. | Required |
| custom_message | A custom message to include with the SOC feedback. | Required |
| threat_feedback_reported_at_limit | Datetime limit. Accepts (<number> <time unit>), e.g “7 days”, “one month” or a iso string (e.g. “2024-10-30T08:37:42.359Z”) | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.sendIncidentSocFeedback | string | The reporter will be informed the incident has been resolved and that no further actions are required from them. |
Command Example
!hoxhunt-incident-soc-feedback-send incident_id="12345" custom_message="User reported a phishing email." threat_feedback_reported_at_limit="2024-09-01T00:00:00Z"
hoxhunt-incident-set-sensitive
Set incident to contain sensitive information.
Base Command
hoxhunt-incident-set-sensitive
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident to be marked as sensitive or not sensitive. | Required |
| is_sensitive | True or False Boolean for whether the incident contains sensitive information. Possible values are: TRUE, FALSE. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.setIncidentSensitive | string | Incident sensitivity information. |
Command Example
!hoxhunt-incident-set-sensitive incident_id="12345" is_sensitive=true
hoxhunt-incident-set-soc-classification
Set soc classification for an incident.
Base Command
hoxhunt-incident-set-soc-classification
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident to classify. | Required |
| classification | The SOC classification to apply to the incident. Possible values are: MALICIOUS, SPAM, SAFE. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.setIncidentSocClassification | string | Incident SOC classification information. |
Command Example
!hoxhunt-incident-set-soc-classification incident_id="12345" classification="Malware"
hoxhunt-incident-update-state
Updates Incident state.
Base Command
hoxhunt-incident-update-state
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_id | The ID of the Hoxhunt incident to update. | Required |
| state | The new state of the incident. Possible values are: OPEN, RESOLVED. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Hoxhunt.updateIncidentState | string | Incident state information. |
Command Example
!hoxhunt-incident-update-state incident_id="12345" state="RESOLVED"
get-mapping-fields
Get mapping fields from remote incident. Please note that this method will not update the current incident. It’s here for debugging purposes.
Base Command
get-mapping-fields
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
There is no context output for this command.
Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and Hoxhunt v2 corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:
- Enable Fetching incidents in your instance configuration.
-
In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:
Option Description None Turns off incident mirroring. Incoming Any changes in Hoxhunt v2 events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. Outgoing Any changes in Cortex XSOAR incidents will be reflected in Hoxhunt v2 events (outgoing mirrored fields). Incoming And Outgoing Changes in Cortex XSOAR incidents and Hoxhunt v2 events will be reflected in both directions.
Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Hoxhunt v2.
Configuration parameters
url— Server URL (required)api_key— API Key (required)mirror_direction— Incident Mirroring Directioninsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch IntervalincidentType— Incident typefirst_fetch— First fetch timestampmax_fetch— Fetch limitonly_escalated_incidents— Only fetch escalated incidentsonly_open_incidents— Only fetch open incidents
Commands (9)
-
get-mapping-fieldsGet mapping fields from remote incident. Please note that this method will not update the current incident. It's here for debugging purposes.
-
hoxhunt-current-user-getGets the current user information from Hoxhunt.
-
hoxhunt-incident-note-addAdd Incident note.
-
hoxhunt-incident-set-sensitiveSet incident to contain sensitive information.
-
hoxhunt-incident-set-soc-classificationSet soc classification for an incident.
-
hoxhunt-incident-soc-feedback-sendSend feedback to reporters of incident about whether the reported email was safe, spam or malicious.
-
hoxhunt-incident-threats-getGets threats from Hoxhunt.
-
hoxhunt-incident-threats-removeRemove all threats that belong to an incident.
-
hoxhunt-incident-update-stateUpdates Incident state.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 """ IMPORTS """ from collections.abc import Callable from typing import Any import urllib3 # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ CURRENT_USER_FIELDS = """ _id emails { address } """ FETCH_INCIDENTS_FIELDS = """ _id createdAt updatedAt humanReadableId lastReportedAt firstReportedAt policyName state globalThreatCount responseUrl notes { _id user { emails { address } } text timestamp editedAt deletedAt } hoxhuntClassification: classification externalClassification: socClassification hasSensitiveInformation ruleMatches { incidentRule { _id name priority } actions timestamp } firstThreat: threats( sort: createdAt_ASC first: 1 ) { _id hoxhuntClassification: classification prediction { mlopsIocEmailMaliciousnessProbability } enrichments { links { label href score } } email { to { address name } from { address name } cc { address name } bcc { address name } sanitizedBody { signedUrl expiresAt } date messageId attachments { name type hash size content { signedUrl expiresAt } score } subject } createdAt } threatMetadata { likelyTargetEntity threatCount vipReportCount reportCountFromInbox phishReportCount userActions threatIndicators resources { hasLink hasAttachment } } """ FETCH_MODIFIED_INCIDENTS_FIELDS = """ _id """ CREATE_INCIDENT_NOTE_FIELDS = """ notes { _id text } """ SET_INCIDENT_SENSITIVE_FIELDS = """ _id hasSensitiveInformation """ SET_INCIDENT_SOC_CLASSIFICATION_FIELDS = """ _id humanReadableId updatedAt socClassification """ UPDATE_INCIDENT_STATE_FIELDS = """ _id humanReadableId updatedAt state """ GET_THREATS_FIELDS = """ _id createdAt updatedAt feedbackSentAt classification state organizationId severity userRequestedFeedback threatRedirectId isVipReport """ CAMPAIGN_INCIDENT_TYPE_NAME = "Hoxhunt Campaign" USER_ACTED_TYPE_NAME = "Hoxhunt User Acted" BEC_TYPE_NAME = "Hoxhunt BEC" HOXHUNT_STATE_TO_REMOTE_DATA_ENTRY_MAP = { "RESOLVED": { "Type": EntryType.NOTE, "Contents": {"closeReason": "Incident was resolved in Hoxhunt platform", "dbotIncidentClose": True}, "ContentsFormat": EntryFormat.JSON, }, "OPEN": { "Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON, }, } INCIDENT_MAPPING_FIELDS = { "state": {"description": "incident can be either open or resolved", "xsoar_field_name": "hoxstate"}, "socClassification": { "description": "external classification given for the incident by soc", "xsoar_field_name": "hoxexternalclassification", }, "isSensitive": { "description": "does the incident contain sensitive information", "xsoar_field_name": "hoxhassensitiveinformation", }, "shouldSendFeedback": {"description": "should feedback be sent to the reporter(s)", "xsoar_field_name": "hoxsendfeedback"}, "feedbackReportedAtLimit": {"description": "time limit for feedback", "xsoar_field_name": "hoxreportedatlimit"}, "socMessage": {"description": "custom message to send with feedback", "xsoar_field_name": "hoxmessagetousers"}, } QUERIES = { "GetCurrentUser": f""" query GetCurrentUser {{ currentUser {{ {CURRENT_USER_FIELDS} }} }} """, "GetIncidentById": f""" query GetIncidentById($id: ID!, $lastUpdate: Date!) {{ incidents(filter: {{ _id_eq: $id, updatedAt_gt: $lastUpdate }}) {{ {FETCH_INCIDENTS_FIELDS} }} }} """, "GetModifiedIncidents": f""" query GetModifiedIncidents($lastUpdate: Date!) {{ incidents(filter: {{ updatedAt_gt: $lastUpdate }}) {{ {FETCH_MODIFIED_INCIDENTS_FIELDS} }} }} """, } MUTATIONS = { "CreateIncidentNote": f""" mutation CreateIncidentNote($incidentId: ID!, $note: IncidentNoteInput!) {{ addIncidentNote( incidentId: $incidentId note: $note ) {{ {CREATE_INCIDENT_NOTE_FIELDS} }} }} """, "RemoveIncidentThreats": """ mutation RemoveIncidentThreats($incidentId: String!) { removeIncidentThreats(incidentId: $incidentId) } """, "SendIncidentSocFeedback": """ mutation SendIncidentSocFeedback($incidentId: String!, $customMessage: String, $threatFeedbackReportedAtLimit: Date) { sendIncidentSocFeedback( incidentId: $incidentId, customMessage: $customMessage, threatFeedbackReportedAtLimit: $threatFeedbackReportedAtLimit ) } """, "SetIncidentSensitive": f""" mutation SetIncidentSensitive($incidentId: String!, $isSensitive: Boolean!) {{ setIncidentSensitive( incidentId: $incidentId isSensitive: $isSensitive ) {{ {SET_INCIDENT_SENSITIVE_FIELDS} }} }} """, "SetIncidentSocClassification": f""" mutation SetIncidentSocClassification($incidentId: String!, $classification: SocClassification!) {{ setIncidentSocClassification( incidentId: $incidentId classification: $classification ) {{ {SET_INCIDENT_SOC_CLASSIFICATION_FIELDS} }} }} """, "UpdateIncidentState": f""" mutation UpdateIncidentState($incidentId: ID!, $state: IncidentState!) {{ updateIncidentState( incidentId: $incidentId state: $state ) {{ {UPDATE_INCIDENT_STATE_FIELDS} }} }} """, } MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}.get( demisto.params().get("mirror_direction") ) # type: ignore """ HELPER CLASSES """ class GqlResult: def __init__(self, data: dict[str, Any] | None = None, errors: list[str] | None = None): self.data = data or {} self.errors = errors or [] def has_errors(self) -> bool: return bool(self.errors) def http_error_handler(res): if res.status_code == 429: raise Exception("API rate limit") # Copied from CommonServerPython err_msg = f"Error in API call [{res.status_code}] - {res.reason}" try: # Try to parse json error response error_entry = res.json() err_msg += f"\n{json.dumps(error_entry)}" raise DemistoException(err_msg, res=res) except ValueError: err_msg += f"\n{res.text}" raise DemistoException(err_msg, res=res) """ CLIENT CLASS """ class Client(BaseClient): def query(self, data: str, variables=None) -> GqlResult: """ Fire GraphQl requests towards Hoxhunt external API :return: GqlResult containing .data (and .errors) :rtype: ``GqlResult`` """ variables = variables or {} response = self._http_request( method="POST", json_data={"query": data, "variables": variables}, error_handler=http_error_handler ) gql_result = GqlResult( data=response.get("data"), errors=[error.get("message", "") for error in response.get("errors", [])] ) return gql_result def add_incident_note(self, incident_id: str | None, note_text: str | None): return self.query(MUTATIONS["CreateIncidentNote"], {"incidentId": incident_id, "note": {"text": note_text}}) def remove_incident_threats(self, incident_id: str | None): return self.query(MUTATIONS["RemoveIncidentThreats"], {"incidentId": incident_id}) def send_incident_soc_feedback( self, incident_id: str | None, custom_message: str | None = None, threat_feedback_reported_at_limit: str | None = None ): return self.query( MUTATIONS["SendIncidentSocFeedback"], variables={ "incidentId": incident_id, "customMessage": custom_message, "threatFeedbackReportedAtLimit": threat_feedback_reported_at_limit, }, ) def set_incident_sensitive(self, incident_id: str | None, is_sensitive: bool) -> dict[str, Any]: response = self.query(MUTATIONS["SetIncidentSensitive"], {"incidentId": incident_id, "isSensitive": is_sensitive}) if response.has_errors(): raise ValueError(f"Failed to set incident sensitive: {response.errors}") data = response.data.get("setIncidentSensitive") if data is None: raise ValueError("Failed to set incident sensitive: No data returned.") return { "_id": data.get("_id"), "hasSensitiveInformation": data.get("hasSensitiveInformation"), } def set_incident_soc_classification(self, incident_id: str | None, classification: str | None): return self.query( MUTATIONS["SetIncidentSocClassification"], {"incidentId": incident_id, "classification": classification} ) def update_incident_state(self, incident_id: str | None, state: str | None): return self.query(MUTATIONS["UpdateIncidentState"], {"incidentId": incident_id, "state": state}) def get_current_user(self): return self.query(QUERIES["GetCurrentUser"]) def get_incident_by_id(self, incident_id: str, last_update: str): results = self.query(QUERIES["GetIncidentById"], {"lastUpdate": last_update, "id": incident_id}) incidents = results.data.get("incidents") if not incidents: return {} return incidents[0] def get_modified_incidents(self, last_update: str): return self.query(QUERIES["GetModifiedIncidents"], {"lastUpdate": last_update}) def update_changed_incident_fields(self, incident_id: str, delta: dict[str, Any]): if delta.get("socClassification"): self.set_incident_soc_classification(incident_id, delta.get("socClassification")) if delta.get("isSensitive"): self.set_incident_sensitive(incident_id, delta.get("isSensitive")) # type: ignore def handle_soc_feedback(self, incident_id: str, delta: dict[str, Any], incident_data: dict[str, Any]): should_send_feedback = incident_data.get("shouldSendFeedback") soc_message = incident_data.get("socMessage") feedback_reported_at_limit = incident_data.get("feedbackReportedAtLimit") updated_soc_classification = delta.get("socClassification") # send feedback if enabled and if soc classification has changed (initial set or update) if should_send_feedback and updated_soc_classification: self.send_incident_soc_feedback(incident_id, soc_message, feedback_reported_at_limit) def add_closing_incident_note(self, incident_id: str, incident_data: dict[str, Any]): xsoar_close_note = incident_data.get("xsoarCloseNotes") note = f"Incident closed via XSOAR{' - ' + xsoar_close_note if xsoar_close_note else ''}" self.add_incident_note(incident_id, note) """ TYPES """ DemistoCommandReturnType = Union[ CommandResults, PollResult, str, dict, BaseWidget, list, GetMappingFieldsResponse, GetModifiedRemoteDataResponse, GetRemoteDataResponse, ] HoxCommandType = Callable[[Client, dict, dict], DemistoCommandReturnType] HoxNonReturningCommandType = Callable[[Client, dict, dict], None] """ FNS """ def camel_to_title(text: str) -> str: result = [] for i, char in enumerate(text): if char.isupper() and (i == 0 or not text[i - 1].isupper()): result.append(" ") result.append(char) return "".join(result).strip().title() def create_output(results: dict[str, str], endpoint: str, key_field: str = "") -> CommandResults: human_readable = tableToMarkdown(name="Hoxhunt results", t=results, headerTransform=camel_to_title, removeNull=True) return CommandResults( outputs_prefix=f"Hoxhunt.{endpoint}", outputs_key_field=key_field, outputs=results, readable_output=human_readable ) def create_incident_from_log(incident: dict[str, Any], now: float) -> dict[str, Any]: occurred = incident.get("createdAt") human_readable_id = incident.get("humanReadableId") dbot_mirror_id = str(incident.get("_id")) incident["mirror_instance"] = demisto.integrationInstance() incident["mirror_direction"] = MIRROR_DIRECTION incident["last_mirrored_in"] = int(now * 1000) return {"name": human_readable_id, "dbotMirrorId": dbot_mirror_id, "rawJSON": json.dumps(incident), "occurred": occurred} def fetch_incidents( client: Client, first_fetch: str | None, max_fetch: str | None, only_open_incidents: bool, only_escalated_incidents: bool, now: float, last_run: dict[str, Any], ) -> tuple[list[dict], dict | None]: first_fetch = first_fetch or "7 days" max_fetch = max_fetch or "50" first_fetch_parsed = dateparser.parse(first_fetch) if first_fetch_parsed is None: raise ValueError("Invalid first_fetch format") max_fetch_parsed = min(int(max_fetch), 100) timefrom = first_fetch_parsed.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" last_run = demisto.getLastRun() start_time = last_run.get("start_time") if last_run and "start_time" in last_run else timefrom filters = [f'createdAt_gt: "{start_time}"'] if only_escalated_incidents: filters.append("escalation__escalatedAt_exists: true") if only_open_incidents: filters.append("state_eq: OPEN") filters_str = ", ".join(filters) query = f""" {{ incidents(first: {max_fetch_parsed}, sort: createdAt_DESC, filter: {{ {filters_str} }}) {{ {FETCH_INCIDENTS_FIELDS} }} }} """ result = client.query(query) if result.has_errors(): raise Exception(str(result.errors)) raw_incidents = result.data.get("incidents", []) next_run = None if raw_incidents: next_run = {"start_time": raw_incidents[0].get("createdAt")} incidents: list[dict] = [] for incident in raw_incidents: incidents.append(create_incident_from_log(incident, now)) return incidents, next_run def is_open_in_xsoar(xsoar_status: str) -> bool: return xsoar_status == IncidentStatus.ACTIVE or xsoar_status == IncidentStatus.PENDING def is_closed_in_xsoar(xsoar_status: str) -> bool: return xsoar_status == IncidentStatus.DONE """ COMMANDS """ def test_module_command(client: Client, args: dict, params: dict): """ Returning 'ok' indicates that the integration works like it suppose to. Connection to the service is successful. Args: client: Hoxhunt client Returns: 'ok' if test passed, anything else correlates to failing the test """ result = client.get_current_user() if result.has_errors(): return "Test failed: " + str(result) else: return "ok" def get_remote_data_command(client: Client, args: dict, params: dict): parsed_args = GetRemoteDataArgs(args) incident_id = parsed_args.remote_incident_id last_update = parsed_args.last_update last_update_parsed = dateparser.parse(last_update, settings={"TIMEZONE": "UTC"}) last_update_utc = last_update_parsed.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" # type: ignore new_incident_data: dict = client.get_incident_by_id(incident_id, last_update_utc) if "_id" in new_incident_data: new_incident_data["id"] = new_incident_data["_id"] entries = [] state = new_incident_data.get("state") if state in HOXHUNT_STATE_TO_REMOTE_DATA_ENTRY_MAP: entries.append(HOXHUNT_STATE_TO_REMOTE_DATA_ENTRY_MAP[state]) return GetRemoteDataResponse(new_incident_data, entries) def get_mapping_fields_command(client: Client, args: dict, params: dict): campaign_incident_type_scheme = SchemeTypeMapping(type_name=CAMPAIGN_INCIDENT_TYPE_NAME) user_acted_incident_type_scheme = SchemeTypeMapping(type_name=USER_ACTED_TYPE_NAME) bec_incident_type_scheme = SchemeTypeMapping(type_name=BEC_TYPE_NAME) for mapping_field in INCIDENT_MAPPING_FIELDS: campaign_incident_type_scheme.add_field( name=mapping_field, description=INCIDENT_MAPPING_FIELDS[mapping_field].get("description") ) # type: ignore user_acted_incident_type_scheme.add_field( name=mapping_field, description=INCIDENT_MAPPING_FIELDS[mapping_field].get("description") ) # type: ignore bec_incident_type_scheme.add_field( name=mapping_field, description=INCIDENT_MAPPING_FIELDS[mapping_field].get("description") ) # type: ignore mapping_response = GetMappingFieldsResponse() mapping_response.add_scheme_type(campaign_incident_type_scheme) mapping_response.add_scheme_type(user_acted_incident_type_scheme) mapping_response.add_scheme_type(bec_incident_type_scheme) return mapping_response def update_remote_system_command(client: Client, args: dict, params: dict) -> str: remote_args = UpdateRemoteSystemArgs(args) delta: dict[str, Any] = remote_args.delta incident_changed = remote_args.incident_changed xsoar_status = remote_args.inc_status incident_data: dict[str, Any] = remote_args.data incident_id = remote_args.remote_incident_id # incident was closed or opened in xsoar if incident_changed and delta: if is_open_in_xsoar(xsoar_status): client.update_incident_state(incident_id, "OPEN") client.update_changed_incident_fields(incident_id, delta) elif is_closed_in_xsoar(xsoar_status): client.update_incident_state(incident_id, "RESOLVED") client.update_changed_incident_fields(incident_id, delta) client.handle_soc_feedback(incident_id, delta, incident_data) client.add_closing_incident_note(incident_id, incident_data) return incident_id def get_modified_remote_data_command(client: Client, args: dict, params: dict): remote_args = GetModifiedRemoteDataArgs(args) last_update = remote_args.last_update last_update_parsed = dateparser.parse(last_update, settings={"TIMEZONE": "UTC"}) last_update_utc = last_update_parsed.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" # type: ignore result = client.get_modified_incidents(last_update_utc) if result.has_errors(): raise Exception(str(result.errors)) raw_incidents = result.data.get("incidents") or [] modified_incident_ids = [] for raw_incident in raw_incidents: incident_id = raw_incident.get("_id") modified_incident_ids.append(incident_id) return GetModifiedRemoteDataResponse(modified_incident_ids) def fetch_incidents_command(client: Client, args: dict, params: dict): first_fetch = params.get("first_fetch") max_fetch = params.get("max_fetch") only_open_incidents = argToBoolean(params.get("only_open_incidents") or True) only_escalated_incidents = argToBoolean(params.get("only_escalated_incidents") or False) incidents, next_run = fetch_incidents( client=client, first_fetch=first_fetch, max_fetch=max_fetch, only_open_incidents=only_open_incidents, only_escalated_incidents=only_escalated_incidents, last_run=demisto.getLastRun(), now=datetime.now().timestamp(), ) demisto.incidents(incidents) if next_run: demisto.setLastRun(next_run) # Note: fetch_incidents does not return results; it directly creates incidents. def hoxhunt_get_current_user_command(client: Client, args: dict, params: dict): results = client.get_current_user() if results.has_errors(): raise Exception(results.errors) data = results.data.get("currentUser", {}).get("emails") return create_output(data, "CurrentUser") def hoxhunt_get_incident_threats_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") if not incident_id: raise Exception("incident_id must be present") query_filter = args.get("filter") or "" query_sort = args.get("sort") or "createdAt_DESC" query_limit = min(int(args.get("limit") or 50), 100) query_options = f"filter: {{{query_filter}}}, sort: {query_sort}, first: {query_limit}" query = f'{{incidents(filter: {{_id_eq: "{incident_id}"}}) {{ threats({query_options}) {{ {GET_THREATS_FIELDS} }} }}}}' result = client.query(query) if result.has_errors(): raise Exception(result.errors) threats = result.data.get("incidents", [])[0].get("threats", []) return create_output(threats, "Threats", "_id") def hoxhunt_add_incident_note_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") note_text = args.get("note") response = client.add_incident_note(incident_id, note_text) if response.has_errors(): raise Exception(response.errors) note_data = response.data.get("addIncidentNote", {}).get("notes", []) note = note_data[-1] transformed_data = {"incident_id": incident_id, "note_id": note.get("_id", ""), "note": note.get("text", "")} return create_output(transformed_data, "IncidentNote", "note_id") def hoxhunt_remove_incident_threats_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") response = client.remove_incident_threats(incident_id) if response.has_errors(): raise Exception(response.errors) result_message = response.data.get("removeIncidentThreats", 0) result_data = {"incident_id": incident_id, "removed threats number": result_message} return create_output(result_data, "RemoveIncidentThreats") def hoxhunt_send_incident_soc_feedback_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") custom_message = args.get("custom_message") threat_feedback_reported_at_limit = args.get("threat_feedback_reported_at_limit") threat_feedback_reported_at_limit_parsed = arg_to_datetime(arg=threat_feedback_reported_at_limit, is_utc=True) date_limit_as_iso_string = ( threat_feedback_reported_at_limit_parsed.strftime( # type: ignore "%Y-%m-%dT%H:%M:%S.%f" )[:-3] + "Z" ) response = client.send_incident_soc_feedback(incident_id, custom_message, date_limit_as_iso_string) if response.has_errors(): raise Exception(response.errors) result_data: dict = {"_id": incident_id, "custom_message": custom_message, "limit date": threat_feedback_reported_at_limit} return create_output(result_data, "SendIncidentSocFeedback") def hoxhunt_set_incident_sensitive_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") is_sensitive_bool = argToBoolean(args.get("is_sensitive") or False) response = client.set_incident_sensitive(incident_id, is_sensitive_bool) sensitive_data = { "incident_id": str(response.get("_id", "")), "is_sensitive": str(response.get("hasSensitiveInformation", "")), } return create_output(sensitive_data, "SetIncidentSensitive", "incident_id") def hoxhunt_set_incident_soc_classification_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") classification = args.get("classification") response = client.set_incident_soc_classification(incident_id, classification) if response.has_errors(): raise Exception(response.errors) soc_classification_data = response.data.get("setIncidentSocClassification", {}) return create_output(soc_classification_data, "SetIncidentSocClassification", "_id") def hoxhunt_update_incident_state_command(client: Client, args: dict, params: dict): incident_id = args.get("incident_id") state = args.get("state") response = client.update_incident_state(incident_id, state) if response.has_errors(): raise Exception(response.errors) incident_state_data = response.data.get("updateIncidentState", {}) return create_output(incident_state_data, "UpdateIncidentState", "_id") """ MAIN """ def main(): # pragma: no cover """ PARSE AND VALIDATE INTEGRATION PARAMS """ args = demisto.args() params = demisto.params() command = demisto.command() base_url: str = params.get("url", "") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) api_key: str = params.get("api_key", "") if not all([base_url, api_key]): return return_error("url and api_key must be provided.") headers = {"Authorization": f"Bearer {api_key}"} demisto.info(f"Command being called is {command}") try: client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy) commands: dict[str, HoxCommandType] = { "test-module": test_module_command, "hoxhunt-current-user-get": hoxhunt_get_current_user_command, "hoxhunt-incident-note-add": hoxhunt_add_incident_note_command, "hoxhunt-incident-threats-get": hoxhunt_get_incident_threats_command, "hoxhunt-incident-threats-remove": hoxhunt_remove_incident_threats_command, "hoxhunt-incident-soc-feedback-send": hoxhunt_send_incident_soc_feedback_command, "hoxhunt-incident-set-sensitive": hoxhunt_set_incident_sensitive_command, "hoxhunt-incident-set-soc-classification": hoxhunt_set_incident_soc_classification_command, "hoxhunt-incident-update-state": hoxhunt_update_incident_state_command, "get-remote-data": get_remote_data_command, "get-mapping-fields": get_mapping_fields_command, "update-remote-system": update_remote_system_command, "get-modified-remote-data": get_modified_remote_data_command, } non_returning_commands: dict[str, HoxNonReturningCommandType] = { "fetch-incidents": fetch_incidents_command, } if command in commands: result = commands[command](client, args, params) return return_results(result) elif command in non_returning_commands: non_returning_commands[command](client, args, params) else: raise NotImplementedError(f"The command '{command}' is not recognized.") except Exception as e: # https://xsoar.pan.dev/docs/integrations/mirroring_integration#get-modified-remote-data # Let XSOAR know explicitly that we're being rate limited -> XSOAR has special handling for it if "API rate limit" in str(e): return_error("API rate limit") match command: # https://xsoar.pan.dev/docs/integrations/mirroring_integration#get-modified-remote-data # get-modified-remote-data should return skip update when it fails -> XSOAR has special handling for it case "get-modified-remote-data": return_error(f"skip update - {e}") case _: return_error(f"Failed to execute {command} command. Error: {e!s}") if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()