From 8359d48cba6077be8293eb7b634592d30292b7e8 Mon Sep 17 00:00:00 2001 From: Tal Carmeli <158452762+tcarmeli1@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:02:47 +0200 Subject: [PATCH 001/197] change code owner for xdr (#37876) --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d55ea578d2ee..bb877e7a5abd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -58,9 +58,9 @@ validation_config.toml @YuvHayun # XDR Related -/Packs/CortexXDR/Integrations/ @maimorag -/Packs/Core/Integrations/ @maimorag -/Packs/ApiModules/Scripts/CoreIRApiModule/* @maimorag +/Packs/CortexXDR/Integrations/ @tcarmeli1 +/Packs/Core/Integrations/ @tcarmeli1 +/Packs/ApiModules/Scripts/CoreIRApiModule/* @tcarmeli1 # Demisto Class Packs/ApiModules/Scripts/DemistoClassApiModule/DemistoClassApiModule.py @dantavori From d13a20406850354f9277e665e2daf99d9e3ed0be Mon Sep 17 00:00:00 2001 From: content-bot <55035720+content-bot@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:19:13 +0200 Subject: [PATCH 002/197] Update Pack: CTM360 (#37743) (#37967) * Modified pack-ignore * squash pack update commits * Ignore `unsearchable` * Correct ID and remove unnecessary fields * Address requested changes * Address requested changes 2 * Address requested changes 3 * Address requested changes 4 Unnecessary decorator * Empty-Commit * Downgrade docker image to latest 3.11.x --------- Co-authored-by: S. AlQasim D. <136978057+edx-sayed-salem@users.noreply.github.com> Co-authored-by: MLainer1 <93524335+MLainer1@users.noreply.github.com> --- Packs/CTM360-CyberBlindspot/.pack-ignore | 11 +- Packs/CTM360-CyberBlindspot/.secrets-ignore | 9 +- ...ier-mapper-incoming-CTM360-HackerView.json | 76 ++ .../Dashboards/dashboard-HackerView.json | 525 +++++++++++ .../incidentfields-Asset_Type.json | 31 + .../incidentfields-Potential_Attack.json | 31 + .../incidentfields-Potential_Impact.json | 31 + .../incidentfields-Technologies.json | 31 + .../IncidentTypes/HackerView_Incident.json | 32 + .../CyberBlindspot/CyberBlindspot.yml | 4 +- .../Integrations/HackerView/HackerView.py | 878 ++++++++++++++++++ .../Integrations/HackerView/HackerView.yml | 178 ++++ .../HackerView/HackerView_description.md | 4 + .../HackerView/HackerView_image.png | Bin 0 -> 2455 bytes .../HackerView/HackerView_test.py | 810 ++++++++++++++++ .../Integrations/HackerView/README.md | 204 ++++ .../Integrations/HackerView/command_examples | 0 .../fetch_incidents_response_invalid.json | 6 + .../fetch_incidents_response_valid.json | 102 ++ .../incident_details_response_valid.json | 36 + .../incident_list_cmd_result_valid.json | 108 +++ ...ent_status_change_response_invalid_id.json | 4 + ...status_change_response_invalid_status.json | 4 + ...incident_status_change_response_valid.json | 4 + .../test_data/invalid_api_key_response.json | 5 + ...laybook-HackerView_Incident_Management.yml | 655 +++++++++++++ ...k-HackerView_Incident_Management_README.md | 40 + .../ReleaseNotes/2_0_0.md | 70 ++ .../HackerView_Incident_Management.png | Bin 0 -> 266853 bytes .../CTM360-CyberBlindspot/pack_metadata.json | 7 +- 30 files changed, 3889 insertions(+), 7 deletions(-) create mode 100644 Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json create mode 100644 Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView.json create mode 100644 Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Asset_Type.json create mode 100644 Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Attack.json create mode 100644 Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Impact.json create mode 100644 Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Technologies.json create mode 100644 Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView_description.md create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView_image.png create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView_test.py create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/README.md create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/command_examples create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/fetch_incidents_response_invalid.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/fetch_incidents_response_valid.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/incident_details_response_valid.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/incident_list_cmd_result_valid.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/incident_status_change_response_invalid_id.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/incident_status_change_response_invalid_status.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/incident_status_change_response_valid.json create mode 100644 Packs/CTM360-CyberBlindspot/Integrations/HackerView/test_data/invalid_api_key_response.json create mode 100644 Packs/CTM360-CyberBlindspot/Playbooks/playbook-HackerView_Incident_Management.yml create mode 100644 Packs/CTM360-CyberBlindspot/Playbooks/playbook-HackerView_Incident_Management_README.md create mode 100644 Packs/CTM360-CyberBlindspot/ReleaseNotes/2_0_0.md create mode 100644 Packs/CTM360-CyberBlindspot/doc_files/HackerView_Incident_Management.png diff --git a/Packs/CTM360-CyberBlindspot/.pack-ignore b/Packs/CTM360-CyberBlindspot/.pack-ignore index 3774cb49ebbd..408e8adc9fc1 100644 --- a/Packs/CTM360-CyberBlindspot/.pack-ignore +++ b/Packs/CTM360-CyberBlindspot/.pack-ignore @@ -1,5 +1,13 @@ [file:incidentfields-Course_Of_Action.json] ignore=IF115 +[file:incidentfields-Potential_Impact.json] +ignore=IF115 +[file:incidentfields-Technologies.json] +ignore=IF115 +[file:incidentfields-Asset_Type.json] +ignore=IF115 +[file:incidentfields-Potential_Attack.json] +ignore=IF115 [known_words] D @@ -10,4 +18,5 @@ URLs pytest plugin mappers -CyberBlindspot \ No newline at end of file +CyberBlindspot +HackerView \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/.secrets-ignore b/Packs/CTM360-CyberBlindspot/.secrets-ignore index f614752e0293..2e1d1f1954c4 100644 --- a/Packs/CTM360-CyberBlindspot/.secrets-ignore +++ b/Packs/CTM360-CyberBlindspot/.secrets-ignore @@ -1,2 +1,9 @@ xsoar@ctm360.com -https://cbs.ctm360.com \ No newline at end of file +https://cbs.ctm360.com +https://hackerview.ctm360.com +https://platform.ctm360.com +154.134.143.229 +229.13.147.132 +173.53.166.40 +10.161.216.126 +242.230.133.23 \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json new file mode 100644 index 000000000000..9a1663502116 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json @@ -0,0 +1,76 @@ +{ + "description": "Maps incoming CTM360 HackerView incidents.", + "feed": false, + "id": "CTM360-HackerView - Incoming Mapper", + "mapping": { + "HackerView Incident": { + "dontMapEventToLabels": false, + "internalMapping": { + "Affected Hosts": { + "simple": "CustomFields.host" + }, + "CTM360 HackerView Asset Type": { + "simple": "CustomFields.asset_type" + }, + "CTM360 HackerView Potential Attack Types": { + "simple": "CustomFields.potential_attack_type" + }, + "CTM360 HackerView Potential Impact": { + "simple": "CustomFields.potential_impact" + }, + "CTM360 HackerView Technologies": { + "simple": "CustomFields.technologies" + }, + "CVE ID": { + "simple": "CustomFields.cve_id" + }, + "Categories": { + "simple": "CustomFields.issue_category" + }, + "External Confidence": { + "simple": "CustomFields.confidence" + }, + "External Link": { + "simple": "CustomFields.hackerview_link" + }, + "External Status": { + "simple": "external_status" + }, + "dbotMirrorDirection": { + "simple": "xsoar_mirroring.mirror_direction" + }, + "dbotMirrorId": { + "simple": "xsoar_mirroring.mirror_id" + }, + "dbotMirrorInstance": { + "simple": "xsoar_mirroring.mirror_instance" + }, + "name": { + "complex": { + "filters": [], + "root": "name", + "transformers": [] + } + }, + "occurred": { + "complex": { + "filters": [], + "root": "occurred", + "transformers": [] + } + }, + "severity": { + "complex": { + "filters": [], + "root": "severity", + "transformers": [] + } + } + } + } + }, + "name": "CTM360-HackerView - Incoming Mapper", + "type": "mapping-incoming", + "version": -1, + "fromVersion": "6.10.0" +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView.json b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView.json new file mode 100644 index 000000000000..0888d8332dca --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView.json @@ -0,0 +1,525 @@ +{ + "id": "HackerView Incident Dashboard", + "description": "Dashboard to find metrics regarding the HackerView module", + "isPredefined": false, + "version": -1, + "fromVersion": "6.10.0", + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z", + "name": "HackerView Incident Dashboard", + "layout": [ + { + "id": "82290230-9808-11ee-be91-bd9f71ef874f", + "forceRange": false, + "x": 6, + "y": 2, + "i": "82290230-9808-11ee-be91-bd9f71ef874f", + "w": 6, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2023-12-10T11:20:59.026314772+03:00", + "created": "2023-12-10T11:20:52.129395461+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.39", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents - Line chart", + "prevName": "Active Incidents - Line chart", + "dataType": "incidents", + "widgetType": "line", + "query": "-category:job and -status:archived and -status:closed type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "occurred(d)", + "rawType" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "db208da0-aec8-11ee-82fa-dd707358474c", + "forceRange": false, + "x": 4, + "y": 2, + "i": "db208da0-aec8-11ee-82fa-dd707358474c", + "w": 2, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "severity" + ], + "showGraphValues": false + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "32d91b60-aeca-11ee-82fa-dd707358474c", + "forceRange": false, + "x": 0, + "y": 0, + "i": "32d91b60-aeca-11ee-82fa-dd707358474c", + "w": 6, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Incidents Pending Action - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "customGroupBy": [ + { + "New": { + "conditions": [ + [ + { + "field": "externalstatus", + "operator": "isEqualCaseString", + "right": "New", + "type": "string" + } + ] + ], + "name": "New" + }, + "Other": { + "name": "Other" + } + } + ], + "groupBy": [ + "externalstatus" + ], + "showGraphValues": false, + "showOthers": true + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "e6f69960-aef7-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 6, + "y": 0, + "i": "e6f69960-aef7-11ee-ac47-ef32c7f05a79", + "w": 3, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Active", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "4a71f9d0-aef8-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 9, + "y": 0, + "i": "4a71f9d0-aef8-11ee-ac47-ef32c7f05a79", + "w": 3, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.273909715+03:00", + "created": "2024-01-07T08:36:16.821190202+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Running playbooks", + "prevName": "Running playbooks", + "dataType": "incidents", + "widgetType": "number", + "query": "runStatus:running playbook:\"HackerView Incident Management\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": null, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 0 + }, + "#FAC100": { + "value": 5 + }, + "#FF1B15": { + "value": 20 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "7d10a7b0-aef8-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 6, + "y": 1, + "i": "7d10a7b0-aef8-11ee-ac47-ef32c7f05a79", + "w": 3, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Closed Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Closed", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "8c864490-af78-11ee-9dec-eb27c047d071", + "forceRange": false, + "x": 0, + "y": 2, + "i": "8c864490-af78-11ee-9dec-eb27c047d071", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.27343997+03:00", + "created": "2024-01-07T08:36:16.820119695+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "size": 5, + "Cache": null, + "name": "Top Active Playbooks (Past Week)", + "prevName": "Top Active Playbooks", + "dataType": "incidents", + "widgetType": "column", + "query": "status:active -category:job and type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "playbookId" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "0a967ed0-af7a-11ee-9dec-eb27c047d071", + "forceRange": false, + "x": 9, + "y": 1, + "i": "0a967ed0-af7a-11ee-9dec-eb27c047d071", + "w": 3, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274785325+03:00", + "created": "2024-01-07T08:36:16.823354842+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "6.2.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Avg. runtime for Playbooks (Past Week)", + "prevName": "Average runtime for Playbooks", + "dataType": "metrics", + "widgetType": "duration", + "query": "type:playbook and incidentType:\"HackerView Incident\"", + "isPredefined": false, + "description": "Average runtime for Playbooks", + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "keys": [ + "avg|duration" + ], + "tableColumns": [], + "valuesFormat": "minutes" + }, + "category": "" + }, + "reflectDimensions": true + } + ] +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Asset_Type.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Asset_Type.json new file mode 100644 index 000000000000..9677d679fcd2 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Asset_Type.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_assettype", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Asset Type", + "ownerOnly": false, + "description": "Type of affected asset(s).", + "cliName": "ctm360hackerviewassettype", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Attack.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Attack.json new file mode 100644 index 000000000000..28c7ef26e959 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Attack.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_potentialattack", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Potential Attack Types", + "ownerOnly": false, + "description": "Attacks that may be attempted.", + "cliName": "ctm360hackerviewpotentialattack", + "type": "multiSelect", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Impact.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Impact.json new file mode 100644 index 000000000000..3dccc20abadf --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Potential_Impact.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_potentialimpact", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Potential Impact", + "ownerOnly": false, + "description": "Actions that can be taken on a HackerView incident.", + "cliName": "ctm360hackerviewpotentialimpact", + "type": "multiSelect", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Technologies.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Technologies.json new file mode 100644 index 000000000000..d7f4c5082c25 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Technologies.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_technologies", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Technologies", + "ownerOnly": false, + "description": "Technologies associated with a HackerView incident.", + "cliName": "ctm360hackerviewtechnologies", + "type": "multiSelect", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json b/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json new file mode 100644 index 000000000000..641e8f3d022e --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json @@ -0,0 +1,32 @@ +{ + "id": "HackerView Incident", + "version": -1, + "fromVersion": "6.10.0", + "vcShouldIgnore": false, + "locked": false, + "name": "HackerView Incident", + "prevName": "HackerView Incident", + "color": "#9A1C90", + "playbookId": "HackerView Incident Management", + "hours": 0, + "days": 0, + "weeks": 0, + "hoursR": 0, + "daysR": 0, + "weeksR": 0, + "system": false, + "readonly": false, + "default": false, + "autorun": true, + "preProcessingScript": "", + "closureScript": "", + "disabled": false, + "reputationCalc": 0, + "onChangeRepAlg": 0, + "layout": "", + "detached": false, + "extractSettings": { + "mode": "All", + "fieldCliNameToExtractSettings": {} + } +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml index ddcde1596faf..b6fb8de1eccd 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml @@ -104,7 +104,7 @@ script: - contextPath: CyberBlindspot.RemoteIncident.coa description: The course of action to take. - contextPath: CyberBlindspot.RemoteIncident.class - description: The classification of the incident on remote server. + description: The classification of the incident on remote server. - contextPath: CyberBlindspot.RemoteIncident.status description: The current status of the incident on remote server. - contextPath: CyberBlindspot.RemoteIncident.severity @@ -150,7 +150,7 @@ script: script: '-' type: python subtype: python3 - dockerimage: demisto/python3:3.11.10.115186 + dockerimage: demisto/python3:3.11.10.116949 fromversion: 6.10.0 tests: - No tests (auto formatted) diff --git a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py new file mode 100644 index 000000000000..d97933bd6a98 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py @@ -0,0 +1,878 @@ +import demistomock as demisto # noqa: F401 +from CommonServerPython import * # noqa: F401 + +from CommonServerUserPython import * # noqa: F401 + +from typing import Any + + +''' IMPORTS ''' + +from inspect import getfullargspec +import urllib3 + +# Disable insecure warnings +urllib3.disable_warnings() # noqa: W0105 + +''' CONSTANTS ''' + +INFO = 'info' +ERROR = 'error' +DEBUG = 'debug' +MAX_RETRIES = 5 +MIRROR_LIMIT = 1000 +ABSOLUTE_MAX_FETCH = 200 +MAX_FETCH = arg_to_number(demisto.params().get('max_fetch')) or 25 +MAX_FETCH = min(MAX_FETCH, ABSOLUTE_MAX_FETCH) +DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' # ISO8601 format with UTC, default in XSOAR +HV_OUTGOING_DATE_FORMAT = '%d-%m-%Y %H:%M' +HV_INCOMING_DATE_FORMAT = '%d-%m-%Y %H:%M:%S' +HV_BASE_URL = 'https://hackerview.ctm360.com' +HV_API_ENDPOINT = '/api/v2/issues' +API = { + 'CHANGE_STATUS': '/progress_status/', +} +LOGGING_PREFIX = '[HACKERVIEW]' + +HV_INCIDENT_FIELDS = [ + {'name': 'id', 'description': 'Symbolic Incident ID.'}, + {'name': 'timestamp', 'description': 'DB timestamp.'}, + {'name': 'confidence', 'description': 'Confidence of report.'}, + {'name': 'cve_id', 'description': 'ID of associated CVE.'}, + {'name': 'cwe', 'description': 'List of associated CWEs.'}, + {'name': 'issue_category', 'description': 'Category of Incident'}, + {'name': 'issue_name', 'description': 'Name of Incident.'}, + {'name': 'potential_attack_type', 'description': 'Potential attack to make use of incident.'}, + {'name': 'potential_impact', 'description': 'Potential impact of incident.'}, + {'name': 'status', 'description': 'Active status of Incident.'}, + {'name': 'progress_status', 'description': 'Progress of incident response.'}, + {'name': 'severity', 'description': 'Severity of incident.'}, + {'name': 'resolved_ip', 'description': 'IP resolved on affected asset.'}, + {'name': 'first_seen', 'description': 'Incident creation date.'}, + {'name': 'last_seen', 'description': 'Last discovery date for incident.'}, + {'name': 'last_updated', 'description': 'Last update date for incident.'}, + {'name': 'environments', 'description': 'env.'}, + {'name': 'ticket_id', 'description': 'Ticket ID'}, + {'name': 'technologies', 'description': 'Technologies on affected asset.'}, + {'name': 'domain', 'description': 'domain of affected asset'}, + {'name': 'host', 'description': 'host of affected asset'}, + {'name': 'asset_type', 'description': 'affected asset type'}, + {'name': 'asset', 'description': 'affected asset'}, +] + +MIRROR_DIRECTION = { + 'None': None, + 'Incoming': 'In', + 'Outgoing': 'Out', + 'Incoming And Outgoing': 'Both' +}.get(demisto.params().get('mirror_direction', 'None')) + +INTEGRATION_INSTANCE = demisto.integrationInstance() + + +''' CLIENT CLASS ''' + + +class Client(BaseClient): + """Client class to interact with the service API + + This Client implements API calls, and does not contain any XSOAR logic. + Should only do requests and return data. + It inherits from BaseClient defined in CommonServer Python. + Most calls use _http_request() that handles proxy, SSL verification, etc. + For this implementation, no special attributes defined + """ + + def test_configuration(self, params: dict[str, Any]) -> list[dict[str, Any]]: + """Send request to test + + :param params: Parameters to be sent in the request + :type params: dict[str, Any] + :return: List containing the incidents + :rtype: list[dict[str, Any]] + """ + + response = self._http_request( + method='GET', + url_suffix=f'{HV_API_ENDPOINT}/xsoar', + params=params + ) + log(DEBUG, 'at client\'s test function') + if response.get('statusCode') != 200: + raise DemistoException(f'Error received: {response.get("errors")}') + return response + + def fetch_incidents(self, params: dict[str, Any]) -> list[dict[str, Any]]: + """Send request to fetch list of incidents + + :param params: Parameters to be sent in the request + :type params: dict[str, Any] + :return: List containing the incidents + :rtype: list[dict[str, Any]] + """ + + response = self._http_request( + method='GET', + retries=MAX_RETRIES, + backoff_factor=10, + status_list_to_retry=[400, 429, 500], + url_suffix=f'{HV_API_ENDPOINT}/xsoar', + params=params + ) + log(DEBUG, 'at client\'s fetch function') + if response.get('statusCode') != 200: + raise DemistoException(f'Error received: {response.get("message")}') + + incident_list = response.get('issues', []) + return incident_list + + def fetch_incident(self, params: dict[str, Any]) -> dict[str, Any]: + """Send a request to fetch a certain incident + + :param params: Parameters to be sent in the request + :type params: dict[str, Any] + :return: Dictionary containing the incident data + :rtype: dict[str, Any] + """ + response = self._http_request( + method='GET', + retries=MAX_RETRIES, + backoff_factor=10, + status_list_to_retry=[400, 429, 500], + url_suffix=f'{HV_API_ENDPOINT}/xsoar', + params=params + ) + log(DEBUG, 'at client\'s fetch function') + + incident_list = response.get('issues', []) + if not incident_list: + return {} + + return incident_list[0] + + def change_incident_status(self, args: dict[str, Any]) -> dict[str, Any]: + """Send incident close request for a certain incident + + :param args: Arguments to be sent in the request + :type args: dict[str, Any] + :return: Response from the remote server carrying the result of the request + :rtype: dict[str, Any] + """ + log(DEBUG, f'status change params {args=}') + response = self._http_request( + method='POST', + retries=MAX_RETRIES, + backoff_factor=10, + status_list_to_retry=[400, 429, 500], + url_suffix=HV_API_ENDPOINT + API['CHANGE_STATUS'], + data=args, + params={'t': datetime.now().timestamp()} + ) + log(DEBUG, f'Status Change returned status {response.get("success")}') + + if response.get('success') is False: + log(ERROR, f'Incident status could not be changed: Error {response.get("success")}') + log(ERROR, f'Response {response=}') + else: + log(INFO, response.get('message', '')) + return response + + +''' HELPER FUNCTIONS ''' + + +def log(level: str, msg: str) -> None: + """Logs messages to predefined level + + :param level: Log level + :type level: str + :param msg: Message to log + :type msg: str + """ + { + 'info': demisto.info, + 'error': demisto.error, + 'debug': demisto.debug, + }[level.lower()](f'{LOGGING_PREFIX} {msg}') + + +def convert_to_demisto_severity(severity: str) -> int | float: + """Converts the HackerView incident severity level + ('Unknown', 'Info', 'Low', 'Medium', 'High', 'Critical') to XSOAR + incident severity (0 to 4). + + :param severity: severity as returned from the HackerView API + :type severity: ``str`` + :return: _description_ + :rtype: ``int | float`` + """ + + return { + 'informational': IncidentSeverity.INFO, + 'info': IncidentSeverity.INFO, + 'low': IncidentSeverity.LOW, + 'medium': IncidentSeverity.MEDIUM, + 'high': IncidentSeverity.HIGH, + 'critical': IncidentSeverity.CRITICAL, + 'fyi': IncidentSeverity.UNKNOWN + }[severity.lower()] + + +def convert_time_string( + time_string: str | int, + input_format_string: str, + output_format: str = '', + timestamp: bool = False, + is_utc=False, + in_iso_format=False, + **parser_args) -> datetime | str | int: + """helper function to convert a time string into another format or get the timestamp from it + + :param time_string: Input time string + :type time_string: ``str``|``int`` + :param input_format_string: Format string of the input_time_string + :type input_format_string: ``str`` + :param output_format: The format string to convert a time string into, defaults to '' + :type output_format: str, optional + :param timestamp: A flag to signal whether or not to return a timestamp, defaults to False + :type timestamp: bool, optional + :return: A datetime object, a time string or timestamp integer + :rtype: ``datetime|str|int`` + """ + output: datetime | None + try: + if isinstance(time_string, int): + output = datetime.fromtimestamp(time_string / 1000) + else: + output = dateparser.parse(time_string, [input_format_string], **parser_args) + if not isinstance(output, datetime): + raise ValueError("The passed date string and/or format string is not valid") + if is_utc: + output = output.replace(tzinfo=timezone.utc) + if in_iso_format: + return output.isoformat() + if output_format: + return output.strftime(output_format) + if timestamp: + return int(output.timestamp() * 1000) + return output + except ValueError as err: + log(ERROR, f'An error was encountered at `convert_time_string()` {err=}') + return '' + + +def deduplicate_and_create_incidents( + fetched_incidents: list, + last_run_incident_identifiers: list[str]) -> tuple[list, list]: + """De-duplicates the fetched incidents and creates a list of actionable incidents. + + :param fetched_incidents: Context of the events fetched. + :type fetched_incidents: list + :param last_run_incident_identifiers: List of ids from the events fetched in last run. + :type last_run_incident_identifiers: list[str] + + :return: Returns updated list of event ids and unique incidents that should be created. + :rtype: ``tuple[list,list]`` + """ + log(DEBUG, "at Dedup function") + incidents: list[dict[str, Any]] = [] + new_incident_ids = [] + for incident in fetched_incidents: + try: + incident_id = incident.get('id') + new_incident_ids.append(incident_id) + except Exception as e: + log(ERROR, + f'Skipping insertion of current incident. Error while fetching ID from {incident=}' + f'Error: {str(e)}') + continue + if last_run_incident_identifiers and incident_id in last_run_incident_identifiers: + log(INFO, + 'Skipping insertion of current incident since it already exists.' + f' \n\n {incident=}') + continue + log(DEBUG, "Creating unique incident") + unique_mapped_incident = map_and_create_incident(incident) + incidents.append(unique_mapped_incident) + return new_incident_ids, incidents + + +def map_and_create_incident(unmapped_incident: dict) -> dict: + """Use dictionary of unmapped fetched incident to create a mapped dictionary + + :param unmapped_incident: Fetched incident (unmapped) + :type unmapped_incident: ``dict`` + :return: Incident in format ready for XSOAR + :rtype: ``dict`` + """ + # Remove unnecessary fields + unmapped_incident.pop('ticket_id', '') + unmapped_incident.pop('last_updated', '') + mapped_incident = { + 'name': unmapped_incident.pop('issue_name'), + 'occurred': convert_time_string( + unmapped_incident.pop('first_seen', ''), + HV_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True), + 'external_status': unmapped_incident.pop('status', ''), + 'severity': convert_to_demisto_severity(unmapped_incident.pop('severity', 'low')), + 'CustomFields': { + 'updated_date': convert_time_string( + unmapped_incident.pop('last_seen', ''), + HV_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True), + **unmapped_incident + } + } + + if MIRROR_DIRECTION: + mapped_incident['xsoar_mirroring'] = { + 'mirror_direction': MIRROR_DIRECTION, + 'mirror_id': unmapped_incident.pop('id'), + 'mirror_instance': INTEGRATION_INSTANCE, + } + + mapped_incident['rawJson'] = json.dumps(mapped_incident) + return mapped_incident + + +def to_snake_case(input_string: str) -> str: + """helper function to return passed strings in snake_case + + :param input_string: Input string to change into snake case + :type input_string: ``str`` + :return: A string in snake case + :rtype: ``str`` + """ + return ''.join(['_' + i.lower() if i.isupper() else i for i in input_string]).lstrip('_') + + +''' COMMAND FUNCTIONS ''' + + +def test_module(client: Client, params) -> str: + """Tests API connectivity and authentication' + + Returning 'ok' indicates that the integration works like it is supposed to. + Connection to the service is successful. + Raises exceptions if something goes wrong. + + :type client: ``Client`` + :param Client: client to use + + :return: 'ok' if test passed, anything else will fail the test. + :rtype: ``str`` + """ + + message: str = '' + args: dict[str, Any] = {} + try: + mirror_direction = params.get('mirror_direction', '') + first_fetch = params.get('first_fetch', '') + max_fetch = arg_to_number(params.get('max_fetch', '')) + date_from = params.get('date_from', '') + date_to = params.get('date_to', '') + api_key = params.get('api_key', {}).get('password', '') + + if mirror_direction not in ['None', 'Incoming', 'Outgoing', 'Incoming And Outgoing']: + log(INFO, 'Invalid "Mirror Direction" Value') + raise DemistoException('Invalid "Mirroring Direction" Value') + if first_fetch and not dateparser.parse(first_fetch): + log(INFO, 'Invalid "First Fetch" Value') + raise DemistoException('Invalid "First Fetch" Value') + if max_fetch and (max_fetch <= 0 or max_fetch > ABSOLUTE_MAX_FETCH): + log(INFO, f'Invalid "Max Fetch" Value. Should be between 1 to {ABSOLUTE_MAX_FETCH}') + raise DemistoException( + f'Invalid "Max Fetch" Value. Should be between 1 to {ABSOLUTE_MAX_FETCH}' + ) + args['max_hits'] = max_fetch + if date_from and not dateparser.parse(date_from, [HV_OUTGOING_DATE_FORMAT]): + log(INFO, 'Invalid "Date From" Value (Does not match format "%d-%m-%Y %H:%M")') + raise DemistoException( + 'Invalid "Date From" Value (Does not match format "%d-%m-%Y %H:%M")' + ) + if date_from: + args['date_from'] = convert_time_string( + date_from, + HV_OUTGOING_DATE_FORMAT, + timestamp=True + ) + if date_to and not dateparser.parse(date_to, [HV_OUTGOING_DATE_FORMAT]): + log(INFO, 'Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")') + raise DemistoException( + 'Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")' + ) + if date_to: + args['date_to'] = convert_time_string(date_to, HV_OUTGOING_DATE_FORMAT, timestamp=True) + if not api_key: + log(INFO, 'Invalid "API Key" Value') + raise DemistoException('Invalid "API Key" Value') + incidents = client.test_configuration(args) + if max_fetch and len(incidents) > max_fetch: + log(INFO, + 'Incidents fetched exceed the limit, ' + f'removing the excess {len(incidents) - max_fetch} incidents.') + incidents = incidents[::max_fetch - 1] + message = 'ok' + except DemistoException as e: + expected_words = [ + "Forbidden", + "Authorization", + "Mirroring Direction", + "First Fetch", + "Max Fetch", + "Date From", + "Date To", + "API Key", + "does not match format '%d-%m-%Y %H:%M'" + ] + for word in expected_words: + log(DEBUG, 'Exception in test_module()') + if word in str(e.message): + message = e.message + else: + raise e + return message + + +def fetch_incidents( + client: Client, + last_fetch_ids: list[str] | Any, + params: dict[str, Any], + last_run: dict[str, Any]) -> tuple[dict, list[dict]]: + """Helper function to call client's `fetch_incidents` function. + + Args: + client (Client): client + last_fetch_ids (list[str]): The list of ids of unique incident IDs + that were fetched in the last run + params (dict[str, Any]): GET params to send with the fetch request + + Returns: + tuple[dict, list[dict]]: The next run object and the list of incidents + minus incidents from last run + """ + incidents = client.fetch_incidents(params) + + max_fetch = arg_to_number(params.get('max_hits')) or MAX_FETCH + if max_fetch and len(incidents) > max_fetch: + log( + INFO, + 'Incidents fetched exceed the limit' + f', removing the excess {len(incidents) - max_fetch} incidents.' + ) + incidents = incidents[::max_fetch - 1] + + if not incidents and not last_run: + log(INFO, 'No incidents returned, and no last run data was found') + return {}, [] + + incident_ids, unique_incidents = deduplicate_and_create_incidents(incidents, last_fetch_ids) + log(INFO, f'Received {len(incidents) - len(unique_incidents)} duplicates incidents to skip.') + log(INFO, f'Calculated {len(incident_ids)} ids(s).') + + dates = sorted({d['CustomFields']['timestamp'] for d in unique_incidents}) + log(INFO, f'sorted dates are {dates=}') + last_fetched_timestamp = dates[-1] if dates else last_run.get('last_fetched_timestamp') + + log(INFO, f'setting last fetched timestamp - {last_fetched_timestamp=}') + next_run = {'last_fetched_timestamp': last_fetched_timestamp, 'last_fetch_ids': incident_ids} + return next_run, unique_incidents + + +def get_remote_data_command(client: Client, args: dict) -> GetRemoteDataResponse: + """ get-remote-data command: Returns an updated incident and error entry (if needed) + + Args: + client: client + args (dict): The command arguments + close_incident (bool): Indicates whether to close the corresponding + XSOAR incident if the incident has been closed on HV's end. + close_end_statuses (bool): Specifies whether "End Status" statuses on HV + should be closed when mirroring. + + Returns: + GetRemoteDataResponse: The Response containing the updated incident to mirror + and its entries + """ + entries = [] + entry = {'Type': EntryType.NOTE, 'ContentsFormat': EntryFormat.TEXT} + updated_incident = {} + remote_args = GetRemoteDataArgs(args) + remote_incident_id = remote_args.remote_incident_id + + log(DEBUG, f'Performing get-remote-data command for the incident: {remote_incident_id}') + + params = { + 'ticket_id': remote_incident_id, + 't': datetime.now().timestamp() * 1000 + } + + updated_incident = client.fetch_incident(params) + log(DEBUG, f'{updated_incident=}') + new_status = updated_incident.get('status', '').lower() + progress_status = updated_incident.get('progress_status', '').lower() + last_updated = updated_incident.get('last_updated', 0) + last_seen = convert_time_string( + updated_incident.get('last_seen', 0), + HV_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True, timestamp=True) + + log(INFO, f'Updating incident {remote_incident_id} to status: {new_status}') + if last_updated == last_seen: + # Active status has changed + if new_status == 'active': + log(DEBUG, 'Incident seems to have been re-opened.') + close_reason = 'Incident was re-opened.' + entries.append({ + 'Type': EntryType.NOTE, + "Contents": { + "dbotIncidentReopen": True + }, + "ContentsFormat": EntryFormat.JSON + }) + elif new_status == 'inactive': + log(DEBUG, 'Incident seems to be closed. Adding closing entry') + close_reason = f'Incident was {"resolved" if progress_status == "fixed" else "closed"}.' + entries.append({ + 'Type': EntryType.NOTE, + 'Contents': { + 'dbotIncidentClose': True, + 'closeReason': close_reason + }, + 'ContentsFormat': EntryFormat.JSON + }) + elif new_status == 'active': + if progress_status == 'investigating': + log(DEBUG, 'Incident seems to have ongoing investigation. Adding investigation entry') + + note = 'Your team is actively investigating and examining this particular issue.' + entries.append(entry | {'Contents': note}) + elif progress_status == 'in progress': + log(DEBUG, 'Incident seems to be undergoing a process. Adding processing entry') + + note = 'Your team has concluded its investigation and has shifted its focus to resolution.' + entries.append(entry | {'Contents': note}) + elif progress_status == 'fixed': + log(DEBUG, 'Incident seems to be resolved. Adding resolution entry') + + note = 'Your team has implemented all necessary changes and fixes to address this issue.' + \ + ' Our system will automatically label issues as resolved once they are no longer present.' + entries.append(entry | {'Contents': note}) + elif progress_status == 'acceptable risk': + log(DEBUG, 'Incident seems to be have ongoing monitoring. Adding monitoring entry') + + note = 'your organization deems any risk(s) linked to a particular issue as acceptable.' + \ + ' This categorization is intended for internal classification purposes.' + entries.append(entry | {'Contents': note}) + elif progress_status == 'false positive': + log(DEBUG, 'Incident seems to be have ongoing monitoring. Adding monitoring entry') + + note = 'The identified problem was incorrectly tagged and may not be relevant.' + entries.append(entry | {'Contents': note}) + + else: + log( + DEBUG, + f'This status value `{updated_incident.get("status")}`' + f'for incident {remote_incident_id}.' + ) + return GetRemoteDataResponse([], []) + log(DEBUG, f'Updated incident {remote_incident_id}') + mapped_updated_incident = map_and_create_incident(updated_incident) + return GetRemoteDataResponse(mirrored_object=mapped_updated_incident, entries=entries) + + +def get_modified_remote_data_command(client: Client, args) -> GetModifiedRemoteDataResponse: + """ Gets the list of all incidents that have changed since a given timestamp + + Args: + client: Client + args (dict): The command arguments + + Returns: + GetModifiedRemoteDataResponse: The response containing the list of ids of incidents changed + """ + modified_incident_ids: list = [] + remote_args = GetModifiedRemoteDataArgs(args) + last_timestamp = convert_time_string(remote_args.last_update, '', timestamp=True) + log( + DEBUG, + 'Performing get-modified-remote-data command with : ' + f'{last_timestamp}({remote_args.last_update})' + ) + + params = { + 'date_field': 'last_updated', + 'order': 'asc', + 'date_from': last_timestamp, + 'max_hits': ABSOLUTE_MAX_FETCH, + 't': datetime.now().timestamp() * 1000 + } + modified_incident_ids.extend(item['id'] for item in client.fetch_incidents(params)) + if len(modified_incident_ids) >= MIRROR_LIMIT: + log( + INFO, + f'Warning: More than {MIRROR_LIMIT} incidents have been modified since last updated.' + ) + return GetModifiedRemoteDataResponse(modified_incident_ids=modified_incident_ids) + + +def update_remote_system_command(client: Client, args: dict) -> str: + """Outgoing Mirroring + + :param client: Client + :type client: Client + :param args: _description_ + :type args: dict + :return: _description_ + :rtype: _type_ + """ + + parsed_args = UpdateRemoteSystemArgs(args) + log(DEBUG, f'Got the following update args:\n\n{parsed_args.entries=},') + log(DEBUG, f'\n\n{parsed_args.data=}, ') + log(DEBUG, f'\n\n{parsed_args.incident_changed=}, ') + log(DEBUG, f'\n\n{parsed_args.inc_status=}, ') + log(DEBUG, f'\n\n{parsed_args.remote_incident_id=}, ') + log(DEBUG, f'\n\n{parsed_args.delta=}') + if parsed_args.delta: + log(DEBUG, f'Got the following delta keys {str(list(parsed_args.delta.keys()))}') + + remote_incident_id = parsed_args.remote_incident_id + + try: + + if parsed_args.incident_changed: + issue_status: str = parsed_args.inc_status + log(DEBUG, f'Incident {remote_incident_id} status changed to {issue_status}') + if issue_status == IncidentStatus.DONE: + log(DEBUG, f'Closing incident {remote_incident_id}') + client.change_incident_status({ + 'issue_id': remote_incident_id, + 't': datetime.now().timestamp() + }) + else: + log( + DEBUG, + f'Modification to {remote_incident_id} ' + 'is not configured for outgoing mirroring..' + ) + else: + log(DEBUG, f'Incident {remote_incident_id} was not modified locally..') + except DemistoException as e: + log(INFO, f'Incident {remote_incident_id} could not be updated..{e.message=}') + + return remote_incident_id + + +def get_mapping_fields_command() -> GetMappingFieldsResponse: + """Return field mapping + + :return: List of field mappings + :rtype: GetMappingFieldsResponse + """ + incident_type_scheme = SchemeTypeMapping(type_name='HackerView Incident') + for field in HV_INCIDENT_FIELDS: + incident_type_scheme.add_field(name=field['name'], description=field['description']) + + return GetMappingFieldsResponse([incident_type_scheme]) + + +def ctm360_hv_incident_list_command(client: Client, args: dict[str, Any]) -> CommandResults: + """_summary_ + + :param client: _description_ + :type client: Client + :param args: _description_ + :type args: dict[str, Any] + :return: _description_ + :rtype: CommandResults + """ + if args.get('dateFrom'): + args['dateFrom'] = convert_time_string( + args['dateFrom'], + HV_OUTGOING_DATE_FORMAT, + timestamp=True + ) + if args.get('dateTo'): + args['dateTo'] = convert_time_string( + args['dateTo'], + HV_OUTGOING_DATE_FORMAT, + timestamp=True + ) + params = {to_snake_case(key): v for key, v in args.items()} + params |= {'date_field': '@timestamp', 't': datetime.now().timestamp()} + result = client.fetch_incidents(params) + log(INFO, f'Received {len(result)} incidents') + if len(result) > 0: + result = [map_and_create_incident(item) for item in result] + + return CommandResults( + outputs_prefix='HackerView.IncidentList', + outputs_key_field='id', + outputs=result, + readable_output=tableToMarkdown( + "HackerView.IncidentList", + result, + headers=result[0].keys(), + is_auto_json_transform=True + ) if len(result) > 0 else "No incidents within these dates", + ) + + +def ctm360_hv_incident_details_command(client: Client, args: dict[str, Any]) -> CommandResults: + """_summary_ + + :param client: _description_ + :type client: Client + :param args: _description_ + :type args: dict[str, Any] + :return: _description_ + :rtype: CommandResults + """ + params = {to_snake_case(key): v for key, v in args.items()} + params['t'] = datetime.now().timestamp() + result = client.fetch_incident(params) + log(INFO, f'Received {result}') + + return CommandResults( + outputs_prefix='HackerView.RemoteIncident', + outputs_key_field='id', + outputs=result, + readable_output=tableToMarkdown( + "HackerView.RemoteIncident", + result, + headers=result.keys(), + is_auto_json_transform=True + ) if result.keys() else "No incident with that ID", + ) + + +def ctm360_hv_incident_status_change_command( + client: Client, + args: dict[str, Any]) -> CommandResults: + """Change status of an incident on remote platform + + :param client: HTTP client + :type client: Client + :param args: Command arguments + :type args: dict[str, Any] + :return: Results of the command + :rtype: CommandResults + """ + params = {to_snake_case(key): v for key, v in args.items()} + params = {'issue_id': params.pop('ticket_id', ''), 'issue_status': params.pop('ticket_status', ''), **params} + result = client.change_incident_status(params) + msg = result.get('message', '') + log( + INFO, + f'Request to change status of incident {args["ticketId"]} ' + f'{"was " if result else "was un"}successful.' + ) + + return CommandResults( + readable_output=msg + ) + + +''' MAIN FUNCTION ''' + + +def main() -> None: + """main function, parses params and runs command functions + + :return: + :rtype: + """ + + try: + + log(DEBUG, "at main func") + demisto_args = demisto.args() + demisto_params = demisto.params() + demisto_command = demisto.command() + + log(DEBUG, f'Command being called is {demisto_command}') + log(DEBUG, f'Demisto Args are {demisto_args=}') + + client = Client( + base_url=HV_BASE_URL, + verify=not demisto_params.get('insecure', False), + headers={'api-key': demisto_params.get('api_key', {}).get('password')}, + proxy=demisto_params.get('proxy', False) + ) + + hv_commands: dict[str, Any] = { + 'test-module': test_module, + 'get-mapping-fields': get_mapping_fields_command, + 'get-remote-data': get_remote_data_command, + 'get-modified-remote-data': get_modified_remote_data_command, + 'update-remote-system': update_remote_system_command, + 'ctm360-hv-incident-list': ctm360_hv_incident_list_command, + 'ctm360-hv-incident-details': ctm360_hv_incident_details_command, + 'ctm360-hv-incident-status-change': ctm360_hv_incident_status_change_command, + } + + if demisto_command == 'fetch-incidents': + log(DEBUG, "at fetch command") + last_run = demisto.getLastRun() + last_fetched_timestamp = last_run.get('last_fetched_timestamp', '') + last_fetch_ids = last_run.get('last_fetch_ids', []) + first_fetch = demisto_params.get('first_fetch', '7 days') + + try: + dateparser.parse(f'{first_fetch} UTC') + except Exception: + log(DEBUG, 'first_fetch is not parsable, setting to `7 days`') + first_fetch = '7 days' + + if not last_fetched_timestamp: + log(DEBUG, f'Fetch is set to fetch incidents from the {first_fetch} ago.') + + params = { + 'date_field': '@timestamp', + 'order': 'asc', + 'max_hits': MAX_FETCH, + 'date_from': last_fetched_timestamp + if last_fetched_timestamp else convert_time_string( + f'{first_fetch} UTC', '', timestamp=True + ), + 't': datetime.now().timestamp() * 1000 + } + + log(DEBUG, f'{demisto_params.get("date_from")=}') + + log(INFO, f'Will be fetching {MAX_FETCH} incidents.') + + log( + DEBUG, + f'LastRun was {last_fetched_timestamp if last_fetched_timestamp else "NOT FOUND"}' + ) + log(DEBUG, f'last run\'s ids were {last_run.get("last_fetch_ids")}') + + log(DEBUG, f'Calling fetch with the following: {params=}') + log(DEBUG, f'Mirroring set as: {MIRROR_DIRECTION}') + + next_run, incidents = fetch_incidents(client, last_fetch_ids, params, last_run) + + log(DEBUG, 'Setting incidents and last run') + log(DEBUG, f'{next_run=}') + log(DEBUG, f'Fetched {len(incidents)} incidents') + demisto.setLastRun(next_run) + demisto.incidents(incidents) + elif demisto_command == 'test-module': + return_results(test_module(client, demisto_params)) + else: + if getfullargspec(hv_commands[demisto_command]).args: + return_results(hv_commands[demisto_command](client, demisto_args)) + else: + return_results(hv_commands[demisto_command]()) + + # Log exceptions and return errors + except Exception as e: + return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}') + + +''' ENTRY POINT ''' + + +if __name__ in ('__main__', '__builtin__', 'builtins'): + main() diff --git a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml new file mode 100644 index 000000000000..a454f6a95ea4 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml @@ -0,0 +1,178 @@ +category: Network Security +commonfields: + id: CTM360_HackerView + version: -1 +configuration: +- additionalinfo: "Choose the direction to mirror the incident: Incoming (from HackerView to Cortex XSOAR), Outgoing (from Cortex XSOAR to HackerView), or Incoming and Outgoing (from/to Cortex XSOAR and HackerView)." + defaultvalue: Incoming And Outgoing + display: Incident Mirroring Direction + name: mirror_direction + options: + - None + - Incoming + - Outgoing + - Incoming And Outgoing + type: 15 + hidden: + - marketplacev2 + required: false +- defaultvalue: 7 days + display: First fetch (