diff --git a/.github/workflows/soc-packs-release.yml b/.github/workflows/soc-packs-release.yml index 5fa5ca3f..3fd9d18e 100644 --- a/.github/workflows/soc-packs-release.yml +++ b/.github/workflows/soc-packs-release.yml @@ -216,43 +216,3 @@ jobs: run: | echo "Deploying: $CONTENT_REPO_RAW_LINK" printf 'yes\n' | python setup.py - - # ── JOB 3: UPDATE PACK CATALOG [MANUAL GATE] ──────────────────── - catalog: - name: Update pack_catalog.json - needs: [release, deploy] - if: needs.release.outputs.has_changes == 'true' - runs-on: ubuntu-latest - environment: - name: pack-catalog-gate # ← requires your approval before running - - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Update pack_catalog.json from pack_metadata.json - run: | - python tools/update_pack_catalog.py \ - --packs-dir Packs \ - --catalog pack_catalog.json \ - --org Palo-Cortex \ - --repo secops-framework \ - --ref refs/heads/main - - - name: Commit updated catalog - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add pack_catalog.json - if git diff --cached --quiet; then - echo "pack_catalog.json unchanged — nothing to commit." - else - git commit -m "- Update pack_catalog.json [skip ci]" - git push origin main - fi diff --git a/Packs/SocFrameworkCrowdstrikeFalcon/pack_metadata.json b/Packs/SocFrameworkCrowdstrikeFalcon/pack_metadata.json index 04f82c5f..73ba86aa 100644 --- a/Packs/SocFrameworkCrowdstrikeFalcon/pack_metadata.json +++ b/Packs/SocFrameworkCrowdstrikeFalcon/pack_metadata.json @@ -3,7 +3,7 @@ "id": "soc-crowdstrike-falcon", "description": "Enhancements for CrowdStrike Falcon telemetry used by the SOC Framework to improve detection, investigation, and response workflows.", "support": "community", - "currentVersion": "1.0.44", + "currentVersion": "1.0.45", "author": "Palo Alto Networks", "url": "https://github.com/Palo-Cortex/soc-optimization-unified", "email": "", diff --git a/Packs/SocFrameworkCrowdstrikeFalcon/xsoar_config.json b/Packs/SocFrameworkCrowdstrikeFalcon/xsoar_config.json index a2916125..200ea724 100644 --- a/Packs/SocFrameworkCrowdstrikeFalcon/xsoar_config.json +++ b/Packs/SocFrameworkCrowdstrikeFalcon/xsoar_config.json @@ -1,8 +1,8 @@ { "custom_packs": [ { - "id": "SocFrameworkCrowdstrikeFalcon-v1.0.44.zip", - "url": "https://github.com/Palo-Cortex/secops-framework/releases/download/SocFrameworkCrowdstrikeFalcon-v1.0.44/SocFrameworkCrowdstrikeFalcon-v1.0.44.zip", + "id": "SocFrameworkCrowdstrikeFalcon-v1.0.45.zip", + "url": "https://github.com/Palo-Cortex/secops-framework/releases/download/SocFrameworkCrowdstrikeFalcon-v1.0.45/SocFrameworkCrowdstrikeFalcon-v1.0.45.zip", "system": "yes" } ], @@ -32,9 +32,9 @@ "all" ], "isOverridable": false, - "enabled": "false", + "enabled": "true", "name": "CrowdstrikeFalcon_Detections_Incidents", - "brand": "CrowdstrikeFalcon", + "brand": "CrowdStrikeFalcon", "category": "Endpoint", "engine": "", "engineGroup": "", diff --git a/Packs/SocFrameworkOptimization/Lists/JobUtilityBulkAlertCloserIDList/JobUtilityBulkAlertCloserIDList.json b/Packs/SocFrameworkOptimization/Lists/JobUtilityBulkAlertCloserIDList/JobUtilityBulkAlertCloserIDList.json index 655fd343..04389948 100644 --- a/Packs/SocFrameworkOptimization/Lists/JobUtilityBulkAlertCloserIDList/JobUtilityBulkAlertCloserIDList.json +++ b/Packs/SocFrameworkOptimization/Lists/JobUtilityBulkAlertCloserIDList/JobUtilityBulkAlertCloserIDList.json @@ -19,5 +19,6 @@ "toServerVersion": "", "truncated": false, "type": "plain_text", - "version": -1 + "version": -1, + "display_name": "JobUtilityBulkAlertCloserIDList" } diff --git a/Packs/SocFrameworkOptimization/Lists/SOCArtifacts/SOCArtifacts.json b/Packs/SocFrameworkOptimization/Lists/SOCArtifacts/SOCArtifacts.json index 0ed9e3de..3ede072d 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCArtifacts/SOCArtifacts.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCArtifacts/SOCArtifacts.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCArtifacts" } diff --git a/Packs/SocFrameworkOptimization/Lists/SOCExecutionList/SOCExecutionList.json b/Packs/SocFrameworkOptimization/Lists/SOCExecutionList/SOCExecutionList.json index 7c3846ce..942e27ed 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCExecutionList/SOCExecutionList.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCExecutionList/SOCExecutionList.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCExecutionList" } diff --git a/Packs/SocFrameworkOptimization/Lists/SOCFrameworkActions/SOCFrameworkActions.json b/Packs/SocFrameworkOptimization/Lists/SOCFrameworkActions/SOCFrameworkActions.json index 827eb05f..5709a99f 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCFrameworkActions/SOCFrameworkActions.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCFrameworkActions/SOCFrameworkActions.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCFrameworkActions" } diff --git a/Packs/SocFrameworkOptimization/Lists/SOCOptimizationConfig/SOCOptimizationConfig.json b/Packs/SocFrameworkOptimization/Lists/SOCOptimizationConfig/SOCOptimizationConfig.json index 1bf7cd77..0f4d0ece 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCOptimizationConfig/SOCOptimizationConfig.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCOptimizationConfig/SOCOptimizationConfig.json @@ -1,27 +1,28 @@ { - "allRead": true, - "allReadWrite": true, - "cacheVersn": 0, - "data": "-", - "definitionId": "", - "description": "", - "detached": false, - "fromServerVersion": "", - "id": "SOCOptimizationConfig", - "isOverridable": false, - "itemVersion": "", - "locked": false, - "name": "SOCOptimizationConfig", - "fromVersion": "6.5.0", - "nameLocked": false, - "packID": "", - "packName": "", - "previousAllRead": true, - "previousAllReadWrite": true, - "system": false, - "tags": null, - "toServerVersion": "", - "truncated": false, - "type": "json", - "version": -1 -} \ No newline at end of file + "allRead": true, + "allReadWrite": true, + "cacheVersn": 0, + "data": "-", + "definitionId": "", + "description": "", + "detached": false, + "fromServerVersion": "", + "id": "SOCOptimizationConfig", + "isOverridable": false, + "itemVersion": "", + "locked": false, + "name": "SOCOptimizationConfig", + "fromVersion": "6.5.0", + "nameLocked": false, + "packID": "", + "packName": "", + "previousAllRead": true, + "previousAllReadWrite": true, + "system": false, + "tags": null, + "toServerVersion": "", + "truncated": false, + "type": "json", + "version": -1, + "display_name": "SOCOptimizationConfig" +} diff --git a/Packs/SocFrameworkOptimization/Lists/SOCProductCategoryMap/SOCProductCategoryMap.json b/Packs/SocFrameworkOptimization/Lists/SOCProductCategoryMap/SOCProductCategoryMap.json index 111bcd19..142d59cf 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCProductCategoryMap/SOCProductCategoryMap.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCProductCategoryMap/SOCProductCategoryMap.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCProductCategoryMap" } diff --git a/Packs/SocFrameworkOptimization/Lists/SOCVendorCapabilities/SOCVendorCapabilities.json b/Packs/SocFrameworkOptimization/Lists/SOCVendorCapabilities/SOCVendorCapabilities.json index 653b346e..399de1c9 100644 --- a/Packs/SocFrameworkOptimization/Lists/SOCVendorCapabilities/SOCVendorCapabilities.json +++ b/Packs/SocFrameworkOptimization/Lists/SOCVendorCapabilities/SOCVendorCapabilities.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCVendorCapabilities" } diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Layouts/layoutscontainer-SOC_Trend_Micro_Vision_One_IR_V3.json b/Packs/SocFrameworkTrendMicroVisionOne/Layouts/layoutscontainer-SOC_Trend_Micro_Vision_One_IR_V3.json deleted file mode 100644 index 515e140c..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Layouts/layoutscontainer-SOC_Trend_Micro_Vision_One_IR_V3.json +++ /dev/null @@ -1,390 +0,0 @@ -{ - "cacheVersn": 0, - "close": null, - "definitionId": "", - "description": "", - "detached": false, - "details": null, - "detailsV2": { - "TypeName": "", - "tabs": [ - { - "id": "summary", - "name": "Legacy Summary", - "type": "summary" - }, - { - "id": "platformOverview", - "name": "Overview", - "type": "platformOverview" - }, - { - "id": "caseinfoid", - "name": "Issue Info", - "sections": [ - { - "displayType": "ROW", - "h": 2, - "i": "caseinfoid-fce71720-98b0-11e9-97d7-ed26ef9e46c8", - "isVisible": true, - "items": [ - { - "endCol": 2, - "fieldId": "type", - "height": 26, - "id": "incident-type-field", - "index": 0, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "severity", - "height": 26, - "id": "incident-severity-field", - "index": 1, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "owner", - "height": 26, - "id": "incident-owner-field", - "index": 2, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "sourcebrand", - "height": 26, - "id": "incident-sourceBrand-field", - "index": 4, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "sourceinstance", - "height": 26, - "id": "incident-sourceInstance-field", - "index": 5, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "playbookid", - "height": 26, - "id": "incident-playbookId-field", - "index": 6, - "sectionItemType": "field", - "startCol": 0 - } - ], - "maxW": 3, - "moved": false, - "name": "Case Details", - "static": false, - "w": 1, - "x": 0, - "y": 0 - }, - { - "h": 2, - "i": "caseinfoid-61263cc0-98b1-11e9-97d7-ed26ef9e46c8", - "maxW": 3, - "moved": false, - "name": "Notes", - "static": false, - "type": "notes", - "w": 1, - "x": 2, - "y": 0 - }, - { - "displayType": "ROW", - "h": 2, - "i": "caseinfoid-6aabad20-98b1-11e9-97d7-ed26ef9e46c8", - "maxW": 3, - "moved": false, - "name": "Work Plan", - "static": false, - "type": "workplan", - "w": 1, - "x": 1, - "y": 0 - }, - { - "displayType": "ROW", - "h": 2, - "i": "caseinfoid-7ce69dd0-a07f-11e9-936c-5395a1acf11e", - "maxW": 3, - "moved": false, - "name": "Indicators", - "query": "", - "queryType": "input", - "static": false, - "type": "indicators", - "w": 2, - "x": 0, - "y": 4 - }, - { - "displayType": "CARD", - "h": 2, - "i": "caseinfoid-ac32f620-a0b0-11e9-b27f-13ae1773d289", - "items": [ - { - "endCol": 1, - "fieldId": "occurred", - "height": 26, - "id": "incident-occurred-field", - "index": 0, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 1, - "fieldId": "dbotmodified", - "height": 26, - "id": "incident-modified-field", - "index": 1, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "dbotduedate", - "height": 26, - "id": "incident-dueDate-field", - "index": 2, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "dbotcreated", - "height": 26, - "id": "incident-created-field", - "index": 0, - "sectionItemType": "field", - "startCol": 1 - }, - { - "endCol": 2, - "fieldId": "dbotclosed", - "height": 26, - "id": "incident-closed-field", - "index": 1, - "sectionItemType": "field", - "startCol": 1 - } - ], - "maxW": 3, - "moved": false, - "name": "Timeline Information", - "static": false, - "w": 1, - "x": 0, - "y": 2 - }, - { - "displayType": "ROW", - "h": 2, - "i": "caseinfoid-88e6bf70-a0b1-11e9-b27f-13ae1773d289", - "isVisible": true, - "items": [ - { - "endCol": 2, - "fieldId": "dbotclosed", - "height": 26, - "id": "incident-dbotClosed-field", - "index": 0, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "closereason", - "height": 26, - "id": "incident-closeReason-field", - "index": 1, - "sectionItemType": "field", - "startCol": 0 - }, - { - "endCol": 2, - "fieldId": "closenotes", - "height": 26, - "id": "incident-closeNotes-field", - "index": 2, - "sectionItemType": "field", - "startCol": 0 - } - ], - "maxW": 3, - "moved": false, - "name": "Closing Information", - "static": false, - "w": 1, - "x": 0, - "y": 6 - }, - { - "displayType": "CARD", - "h": 2, - "i": "caseinfoid-e54b1770-a0b1-11e9-b27f-13ae1773d289", - "isVisible": true, - "items": [ - { - "endCol": 2, - "fieldId": "details", - "height": 26, - "id": "incident-details-field", - "index": 0, - "sectionItemType": "field", - "startCol": 0 - } - ], - "maxW": 3, - "moved": false, - "name": "Investigation Data", - "static": false, - "w": 1, - "x": 1, - "y": 2 - } - ], - "type": "custom" - }, - { - "hidden": false, - "id": "ddeuqiytqt", - "name": "Process & File", - "sections": [ - { - "h": 6, - "i": "ddeuqiytqt-fdc94022-82e8-49c2-a963-7a4d44539786", - "items": [], - "maxW": 3, - "minH": 1, - "moved": false, - "name": "Processes and Files", - "query": "displayTMV1ProcessFileFromAlertsDetails", - "queryType": "script", - "static": false, - "type": "dynamic", - "w": 3, - "x": 0, - "y": 0 - } - ], - "type": "custom" - }, - { - "hidden": false, - "id": "ile1rikyxl", - "name": "Indicators", - "sections": [ - { - "h": 6, - "i": "ile1rikyxl-89ea367d-68ad-40f0-9958-1f22ddb87f20", - "items": [], - "maxW": 3, - "minH": 1, - "moved": false, - "name": "Trend Micro Indicators", - "query": "displayTMV1IndicatorsFromAlertsDetails", - "queryType": "script", - "static": false, - "type": "dynamic", - "w": 3, - "x": 0, - "y": 0 - } - ], - "type": "custom" - }, - { - "hidden": false, - "id": "dsnilp9kqn", - "name": "Related Assets", - "sections": [ - { - "h": 6, - "i": "dsnilp9kqn-90a51eec-dce5-41dc-a8a7-948b1b5eeb52", - "items": [], - "maxW": 3, - "minH": 1, - "moved": false, - "name": "Related Assets", - "query": "displayTMV1RelatedAssetsFromAlertDetails", - "queryType": "script", - "static": false, - "type": "dynamic", - "w": 3, - "x": 0, - "y": 0 - } - ], - "type": "custom" - }, - { - "hidden": false, - "id": "kjjsylv6kp", - "name": "Vision One Metadata", - "sections": [ - { - "h": 6, - "i": "kjjsylv6kp-5f812bae-ae96-4d47-a71d-784ad9415682", - "items": [], - "maxW": 3, - "minH": 1, - "moved": false, - "name": "Vision One Meta Data", - "query": "displayTMV1MetadataFromAlertDetails", - "queryType": "script", - "static": false, - "type": "dynamic", - "w": 3, - "x": 0, - "y": 0 - } - ], - "type": "custom" - }, - { - "id": "warRoom", - "name": "War Room", - "type": "warRoom" - }, - { - "id": "workPlan", - "name": "Work Plan", - "type": "workPlan" - } - ] - }, - "edit": null, - "fromServerVersion": "6.0.0", - "group": "incident", - "id": "SOC Trend Micro Vision One IR_V3", - "indicatorsDetails": null, - "indicatorsQuickView": null, - "isOverridable": false, - "itemVersion": "1.0.10", - "locked": false, - "mobile": null, - "name": "SOC Trend Micro Vision One IR_V3", - "packID": "", - "packName": "SOC Trend Micro Enhancement for Cortex XSIAM", - "propagationLabels": [], - "quickView": null, - "quickViewV2": null, - "system": false, - "toServerVersion": "99.99.99", - "version": -1, - "fromVersion": "6.0.0" -} \ No newline at end of file diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/README.md b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.py deleted file mode 100644 index 14ebaf7f..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.py +++ /dev/null @@ -1,102 +0,0 @@ -# Load these for testing, but ignore in operation -# Universal Command allows multiple Vendor commands to be used by a single Universal Command -import demistomock as demisto # type: ignore -from CommonServerPython import * # type: ignore - -def get_incident_cf(): - inc = demisto.incident() or {} - return inc.get("CustomFields") or {} - -def first_nonempty_str(*vals): - for v in vals: - if v is None: - continue - if isinstance(v, str): - s = v.strip() - if s: - return s - else: - # allow non-strings if they are meaningful - if v not in ("", [], {}, ()): - return v - return None - -def find_alert_anywhere(ctx): - """ - Best-effort: use VisionOne.Alert_Details.alert if present, otherwise walk context for an alert-like object. - """ - # your current explicit path - v = (ctx.get('VisionOne') or {}) - ad = (v.get('Alert_Details') or {}) - alert = (ad.get('alert') or {}) - if isinstance(alert, dict) and alert.get("id"): - return alert - - # optional: if you want the robust walker like your other scripts, drop it in here. - return None - -def build_alert_from_rule_fields(cf): - """ - Build a minimal 'alert-like' object from correlation-rule mapped fields. - Enough to populate Normalized[] with host/user/ip/hashes/process. - """ - # These keys align to what you've been mapping in the YAML - host_guid = first_nonempty_str(cf.get("agent_id")) - host_name = first_nonempty_str(cf.get("agent_hostname")) - local_ip = first_nonempty_str(cf.get("action_local_ip"), cf.get("prenatsourceip")) - user_name = first_nonempty_str(cf.get("actor_effective_username")) - user_id = first_nonempty_str(cf.get("userid")) - sha256 = first_nonempty_str(cf.get("action_file_sha256"), cf.get("filehash")) - md5 = first_nonempty_str(cf.get("action_file_md5"), cf.get("processmd5")) - cmdline = first_nonempty_str(cf.get("actor_process_command_line"), cf.get("processcmd")) - image_path = first_nonempty_str(cf.get("actor_process_image_path"), cf.get("action_file_path")) - image_name = first_nonempty_str(cf.get("actor_process_image_name")) - - # remote ip + domain are useful for ips list / context - remote_ip = first_nonempty_str(cf.get("action_remote_ip")) - domain = first_nonempty_str(cf.get("agent_device_domain")) - - # Create an "alert-like" dict in the same shape your normalizer expects - alert = { - "id": first_nonempty_str(cf.get("originalalertid"), "—"), - "workbench_link": first_nonempty_str(cf.get("externallink"), cf.get("external_pivot_url")), - "description": first_nonempty_str(cf.get("alert_description")), - "impact_scope": { - "entities": [] - }, - "indicators": [] - } - - # Entities (host/account) – match VisionOne shapes your code already supports - if host_guid or host_name or local_ip: - host_ev = {"guid": host_guid, "name": host_name, "ips": [x for x in [local_ip] if x]} - alert["impact_scope"]["entities"].append({ - "entity_type": "host", - "entity_value": host_ev - }) - - if user_name: - alert["impact_scope"]["entities"].append({ - "entity_type": "account", - "entity_value": user_name - }) - - # Indicators – keep types consistent with your existing indicator parsing - if sha256: - alert["indicators"].append({"type": "file_sha256", "value": sha256}) - if md5: - alert["indicators"].append({"type": "file_md5", "value": md5}) - if cmdline: - alert["indicators"].append({"type": "command_line", "value": cmdline}) - if image_path: - alert["indicators"].append({"type": "fullpath", "value": image_path}) - if remote_ip: - alert["indicators"].append({"type": "ip", "value": remote_ip}) - if domain: - alert["indicators"].append({"type": "domain", "value": domain}) - - # If you have image_name but no path, still let it show up in process - if image_name and not image_path: - alert["indicators"].append({"type": "filename", "value": image_name}) - - return alert diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.yml b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.yml deleted file mode 100644 index 86ea457b..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne.yml +++ /dev/null @@ -1,24 +0,0 @@ -comment: | - Normalize alert context into a single Normalized key for vendor-agnostic layouts. Extends with MITRE, Impact Scope, Artifacts, Timeline, and Model from VisionOne.Alert_Details.alert and merges ExtractedIndicators. Read-only; does not execute any response actions. -commonfields: - id: SOCNormalizeTrendMicroVisionOne - version: -1 -dockerimage: demisto/python3 -enabled: true -engineinfo: {} -mainengineinfo: {} -name: SOCNormalizeTrendMicroVisionOne -pswd: "" -runas: DBotWeakRole -runonce: false -script: '' -scripttarget: 0 -subtype: python3 -tags: -- normalization -- context -- SOC -- SOC_Framework -- Trend Micro Vision One -type: python -fromversion: 6.10.0 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne_test.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne_test.py deleted file mode 100644 index 97adb694..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/SOCNormalizeTrendMicroVisionOne/SOCNormalizeTrendMicroVisionOne_test.py +++ /dev/null @@ -1,232 +0,0 @@ -import sys -import types - -# ---- Mock XSOAR runtime modules before importing the script ---- - -demisto_mock = types.SimpleNamespace() -demisto_mock.context = lambda: {} -demisto_mock.incident = lambda: {} -demisto_mock.results = lambda x: x -demisto_mock.error = lambda x: None - -sys.modules["demistomock"] = demisto_mock - -common = types.ModuleType("CommonServerPython") -sys.modules["CommonServerPython"] = common - -import SOCNormalizeTrendMicroVisionOne as script - - -def test_get_incident_cf_returns_customfields(mocker): - incident = { - "CustomFields": { - "agent_id": "agent-123", - "actor_effective_username": "scott" - } - } - - mocker.patch.object(script.demisto, "incident", return_value=incident) - - result = script.get_incident_cf() - - assert result == { - "agent_id": "agent-123", - "actor_effective_username": "scott" - } - - -def test_get_incident_cf_returns_empty_dict_when_missing(mocker): - mocker.patch.object(script.demisto, "incident", return_value={}) - - result = script.get_incident_cf() - - assert result == {} - - -def test_first_nonempty_str_returns_first_nonempty_string(): - result = script.first_nonempty_str(None, "", " ", "hello", "world") - - assert result == "hello" - - -def test_first_nonempty_str_strips_whitespace(): - result = script.first_nonempty_str(" value ", "other") - - assert result == "value" - - -def test_first_nonempty_str_returns_first_meaningful_non_string(): - result = script.first_nonempty_str(None, [], {}, 42, "fallback") - - assert result == 42 - - -def test_first_nonempty_str_returns_none_when_all_empty(): - result = script.first_nonempty_str(None, "", " ", [], {}, ()) - - assert result is None - - -def test_find_alert_anywhere_returns_explicit_visionone_path(): - ctx = { - "VisionOne": { - "Alert_Details": { - "alert": { - "id": "wb-123", - "severity": "high" - } - } - } - } - - result = script.find_alert_anywhere(ctx) - - assert result == { - "id": "wb-123", - "severity": "high" - } - - -def test_find_alert_anywhere_returns_none_when_missing(): - ctx = { - "VisionOne": { - "Alert_Details": {} - } - } - - result = script.find_alert_anywhere(ctx) - - assert result is None - - -def test_build_alert_from_rule_fields_builds_full_alert_like_object(): - cf = { - "originalalertid": "wb-999", - "externallink": "https://visionone.example/alerts/wb-999", - "alert_description": "Suspicious process detected", - "agent_id": "agent-123", - "agent_hostname": "host1", - "action_local_ip": "10.0.0.10", - "actor_effective_username": "alice", - "userid": "u-123", - "action_file_sha256": "sha256-value", - "action_file_md5": "md5-value", - "actor_process_command_line": "powershell -enc abc", - "actor_process_image_path": "C:/Temp/bad.exe", - "actor_process_image_name": "bad.exe", - "action_remote_ip": "8.8.8.8", - "agent_device_domain": "corp.local", - } - - result = script.build_alert_from_rule_fields(cf) - - assert result["id"] == "wb-999" - assert result["workbench_link"] == "https://visionone.example/alerts/wb-999" - assert result["description"] == "Suspicious process detected" - - entities = result["impact_scope"]["entities"] - assert len(entities) == 2 - - assert entities[0] == { - "entity_type": "host", - "entity_value": { - "guid": "agent-123", - "name": "host1", - "ips": ["10.0.0.10"] - } - } - assert entities[1] == { - "entity_type": "account", - "entity_value": "alice" - } - - indicators = result["indicators"] - assert {"type": "file_sha256", "value": "sha256-value"} in indicators - assert {"type": "file_md5", "value": "md5-value"} in indicators - assert {"type": "command_line", "value": "powershell -enc abc"} in indicators - assert {"type": "fullpath", "value": "C:/Temp/bad.exe"} in indicators - assert {"type": "ip", "value": "8.8.8.8"} in indicators - assert {"type": "domain", "value": "corp.local"} in indicators - - -def test_build_alert_from_rule_fields_uses_fallback_keys(): - cf = { - "agent_id": "agent-456", - "agent_hostname": "host2", - "prenatsourceip": "10.0.0.20", - "filehash": "sha256-fallback", - "processmd5": "md5-fallback", - "processcmd": "cmd.exe /c whoami", - "action_file_path": "/tmp/evil.bin", - } - - result = script.build_alert_from_rule_fields(cf) - - entities = result["impact_scope"]["entities"] - assert entities == [ - { - "entity_type": "host", - "entity_value": { - "guid": "agent-456", - "name": "host2", - "ips": ["10.0.0.20"] - } - } - ] - - indicators = result["indicators"] - assert {"type": "file_sha256", "value": "sha256-fallback"} in indicators - assert {"type": "file_md5", "value": "md5-fallback"} in indicators - assert {"type": "command_line", "value": "cmd.exe /c whoami"} in indicators - assert {"type": "fullpath", "value": "/tmp/evil.bin"} in indicators - - -def test_build_alert_from_rule_fields_uses_filename_when_no_image_path(): - cf = { - "actor_process_image_name": "onlyname.exe" - } - - result = script.build_alert_from_rule_fields(cf) - - assert result["indicators"] == [ - {"type": "filename", "value": "onlyname.exe"} - ] - - -def test_build_alert_from_rule_fields_does_not_add_filename_when_path_exists(): - cf = { - "actor_process_image_name": "bad.exe", - "actor_process_image_path": "C:/Temp/bad.exe" - } - - result = script.build_alert_from_rule_fields(cf) - - assert {"type": "fullpath", "value": "C:/Temp/bad.exe"} in result["indicators"] - assert {"type": "filename", "value": "bad.exe"} not in result["indicators"] - - -def test_build_alert_from_rule_fields_defaults_id_to_dash(): - cf = {} - - result = script.build_alert_from_rule_fields(cf) - - assert result["id"] == "—" - assert result["workbench_link"] is None - assert result["description"] is None - assert result["impact_scope"]["entities"] == [] - assert result["indicators"] == [] - - -def test_build_alert_from_rule_fields_builds_account_only_when_no_host(): - cf = { - "actor_effective_username": "bob" - } - - result = script.build_alert_from_rule_fields(cf) - - assert result["impact_scope"]["entities"] == [ - { - "entity_type": "account", - "entity_value": "bob" - } - ] \ No newline at end of file diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/README.md b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.py deleted file mode 100644 index d7b7218a..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.py +++ /dev/null @@ -1,303 +0,0 @@ -# Script name: displayTMV1Indicators_FromAlertDetails -# Purpose: Read indicators from VisionOne Alert Details already in *any* context path OR from mapped fields -# (trendmicrovisiononexdrindicatorsjson / trendmicrovisiononexdrindicators) and render them nicely. - -# Load these for testing, but ignore in operation -# Universal Command allows multiple Vendor commands to be used by a single Universal Command -import demistomock as demisto # type: ignore -from CommonServerPython import * # type: ignore -import json -from collections import defaultdict - -# -------------------- tiny helpers -------------------- -def md_out(text): - demisto.results({"Type": 1, "ContentsFormat": "markdown", "Contents": text}) - -def esc(s): - if s is None: - return "" - if not isinstance(s, str): - try: - s = json.dumps(s, ensure_ascii=False) - except Exception: - s = str(s) - return s.replace("`", "\\`") - -def flat_join(seq, sep=", "): - if not seq: - return "" - return sep.join(str(x) for x in seq if x not in (None, "")) - -def safe_json_loads(maybe_json): - """ - Best-effort parse: - - list/dict => returned as-is - - string => json.loads (handles array/object JSON) - - anything else => None - """ - if maybe_json is None: - return None - if isinstance(maybe_json, (list, dict)): - return maybe_json - if isinstance(maybe_json, str): - s = maybe_json.strip() - if not s: - return None - try: - return json.loads(s) - except Exception: - return None - return None - -# -------------------- detect & extract sources -------------------- -def looks_like_alert_obj(obj: dict) -> bool: - """Heuristics to identify a Vision One alert object.""" - if not isinstance(obj, dict): - return False - if not obj.get("id"): - return False - # Must have either indicators or an impact_scope with entities - if obj.get("indicators"): - return True - iscope = obj.get("impact_scope") or {} - if isinstance(iscope, dict) and isinstance(iscope.get("entities"), list): - return True - return False - -def find_alert_in_context(ctx): - """ - Recursively walk the entire context dict/list to find the first Vision One alert object. - Handles keys like 'VisionOne.Alert_Details(val.etag && val.etag == obj.etag)' and more. - """ - seen = set() - - def _walk(node): - nid = id(node) - if nid in seen: - return None - seen.add(nid) - - if isinstance(node, dict): - if "alert" in node and looks_like_alert_obj(node["alert"]): - return node["alert"] - if looks_like_alert_obj(node): - return node - for v in node.values(): - found = _walk(v) - if found is not None: - return found - elif isinstance(node, list): - for it in node: - found = _walk(it) - if found is not None: - return found - return None - - return _walk(ctx) - -def find_indicators_payload(ctx): - """ - NEW RULES PATH: - Indicators are usually mapped into incident custom fields: - - trendmicrovisiononexdrindicatorsjson - - trendmicrovisiononexdrindicators - Sometimes they may appear in context as: - - indicators_json - - IndicatorsJSON, etc. - - Return: (indicators_list, source_label) - """ - # 1) Incident custom fields (most reliable for correlation rule output) - try: - inc = demisto.incident() or {} - cf = inc.get("CustomFields") or {} - - for k in ( - "trendmicrovisiononexdrindicatorsjson", - "trendmicrovisiononexdrindicators", - "indicators_json", - ): - if k in cf and cf.get(k): - parsed = safe_json_loads(cf.get(k)) - if isinstance(parsed, list): - return parsed, f"incident.CustomFields.{k}" - - # Some environments don’t nest under CustomFields (rare), so check top-level too - for k in ( - "trendmicrovisiononexdrindicatorsjson", - "trendmicrovisiononexdrindicators", - "indicators_json", - ): - if k in inc and inc.get(k): - parsed = safe_json_loads(inc.get(k)) - if isinstance(parsed, list): - return parsed, f"incident.{k}" - except Exception: - pass - - # 2) Context sweep for a direct indicators_json / mapped field value - def _walk_for_key(node, wanted_keys): - seen = set() - - def _walk(n): - nid = id(n) - if nid in seen: - return None - seen.add(nid) - - if isinstance(n, dict): - for wk in wanted_keys: - if wk in n and n.get(wk): - parsed = safe_json_loads(n.get(wk)) - if isinstance(parsed, list): - return parsed, f"context.{wk}" - for v in n.values(): - found = _walk(v) - if found is not None: - return found - elif isinstance(n, list): - for it in n: - found = _walk(it) - if found is not None: - return found - return None - - return _walk(node) - - found = _walk_for_key(ctx, {"trendmicrovisiononexdrindicatorsjson", "trendmicrovisiononexdrindicators", "indicators_json"}) - if found: - return found[0], found[1] - - return None, None - -# -------------------- formatting -------------------- -def normalize_indicator(ind): - """ - Return a flat dict for table rendering. - Expected fields: - id, type, value, related_entities, provenance, field, filter_ids - """ - row = { - "ID": ind.get("id"), - "Type": ind.get("type"), - "Field": ind.get("field") or "", - "Value": "", - "Related Entities": "", - "Provenance": "", - "Filter IDs": "", - } - - val = ind.get("value") - if isinstance(val, dict): - name = val.get("name") - ips = flat_join(val.get("ips")) - guid = val.get("guid") - parts = [] - if name: - parts.append(f"name={name}") - if ips: - parts.append(f"ips=[{ips}]") - if guid: - parts.append(f"guid={guid}") - row["Value"] = ", ".join(parts) if parts else esc(val) - else: - row["Value"] = esc(val) - - row["Related Entities"] = flat_join(ind.get("related_entities")) - row["Provenance"] = flat_join(ind.get("provenance")) - row["Filter IDs"] = flat_join(ind.get("filter_ids")) - - for k in list(row.keys()): - row[k] = esc(row[k]) - return row - -def make_table(headers, rows): - if not rows: - return "_none_\n" - md = "|" + "|".join(headers) + "|\n" - md += "|" + "|".join(["---"] * len(headers)) + "|\n" - for r in rows: - md += "|" + "|".join(str(r.get(h, "")) for h in headers) + "|\n" - return md - -# -------------------- main -------------------- -def main(): - ctx = demisto.context() or {} - - # Prefer new-rule source: the mapped indicators JSON string -> list - indicators, source = find_indicators_payload(ctx) - - wb_id = "—" - if indicators is None: - # Fallback: old behavior (find alert object somewhere in context) - alert = find_alert_in_context(ctx) - if not isinstance(alert, dict): - md_out( - "### Trend Micro Vision One — Indicators\n" - "❌ Couldn’t locate indicators (mapped fields) or a Vision One alert object anywhere in context." - ) - return - - wb_id = alert.get("id") or "—" - indicators = alert.get("indicators") or [] - source = "context.alert.indicators" - else: - # Try to populate workbench id from incident/custom fields if available - try: - inc = demisto.incident() or {} - cf = inc.get("CustomFields") or {} - # Your rule maps originalalertid -> id, but that’s not necessarily stored as a custom field. - # If you *do* have it in CF, grab it; otherwise leave wb_id as "—". - wb_id = cf.get("originalalertid") or cf.get("id") or wb_id - except Exception: - pass - - if not isinstance(indicators, list) or not indicators: - md_out( - "### Trend Micro Vision One — Indicators\n" - f"**Workbench ID:** `{wb_id}` \n" - f"**Source:** `{esc(source or '—')}` \n" - "_No indicators were returned on this alert._" - ) - return - - # Bucket by indicator type for cleaner sections - buckets = defaultdict(list) - for ind in indicators: - if not isinstance(ind, dict): - continue - row = normalize_indicator(ind) - buckets[(row.get("Type") or "").lower()].append(row) - - total = len([x for x in indicators if isinstance(x, dict)]) - types_summary = ", ".join(f"{t or 'unknown'}: {len(rows)}" for t, rows in buckets.items()) - - md = [] - md.append("### Trend Micro Vision One — Indicators") - md.append(f"**Workbench ID:** `{wb_id}` ") - md.append(f"**Source:** `{esc(source or '—')}` ") - md.append(f"**Total Indicators:** {total} ") - md.append(f"**By Type:** {esc(types_summary)}\n") - - headers = ["ID", "Type", "Field", "Value", "Related Entities", "Provenance", "Filter IDs"] - preferred = ["command_line", "file_sha256", "file_md5", "domain", "fullpath", "host", "ip"] - emitted = set() - - for t in preferred: - rows = buckets.get(t, []) - if rows: - md.append(f"#### {t.replace('_',' ').title()}") - md.append(make_table(headers, rows)) - emitted.add(t) - - for t, rows in buckets.items(): - if t in emitted: - continue - title = (t or "unknown").replace("_", " ").title() - md.append(f"#### {title}") - md.append(make_table(headers, rows)) - - md_out("\n".join(md)) - -if __name__ in ("__builtin__", "builtins", "__main__"): - main() diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.yml b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.yml deleted file mode 100644 index 4747850d..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails.yml +++ /dev/null @@ -1,21 +0,0 @@ -commonfields: - id: displayTMV1IndicatorsFromAlertsDetails - version: -1 -dockerimage: demisto/python3:3.12.11.4819260 -enabled: true -engineinfo: {} -mainengineinfo: {} -name: displayTMV1IndicatorsFromAlertsDetails -pswd: "" -runas: DBotWeakRole -runonce: false -script: '' -scripttarget: 0 -subtype: python3 -tags: -- dynamic-section -- SOC -- SOC_Framework -- Trend Micro Vision One -type: python -fromversion: 6.10.0 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails_test.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails_test.py deleted file mode 100644 index 37355dce..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1IndicatorsFromAlertsDetails/displayTMV1IndicatorsFromAlertsDetails_test.py +++ /dev/null @@ -1,278 +0,0 @@ -import sys -import types - -# ---- Mock XSOAR runtime modules before importing the script ---- - -demisto_mock = types.SimpleNamespace() -demisto_mock.context = lambda: {} -demisto_mock.incident = lambda: {} -demisto_mock.results = lambda x: x -demisto_mock.error = lambda x: None - -sys.modules["demistomock"] = demisto_mock - -common = types.ModuleType("CommonServerPython") -sys.modules["CommonServerPython"] = common - -import displayTMV1IndicatorsFromAlertsDetails as script - - -def test_safe_json_loads_with_list(): - value = [{"id": "1", "type": "ip"}] - - result = script.safe_json_loads(value) - - assert result == [{"id": "1", "type": "ip"}] - - -def test_safe_json_loads_with_json_string(): - value = '[{"id":"1","type":"ip"}]' - - result = script.safe_json_loads(value) - - assert result == [{"id": "1", "type": "ip"}] - - -def test_safe_json_loads_with_invalid_json(): - value = "{not-json}" - - result = script.safe_json_loads(value) - - assert result is None - - -def test_looks_like_alert_obj_with_indicators(): - obj = { - "id": "wb-123", - "indicators": [{"id": "1"}] - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_with_impact_scope_entities(): - obj = { - "id": "wb-456", - "impact_scope": { - "entities": [{"id": "entity-1"}] - } - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_false_when_missing_id(): - obj = { - "indicators": [{"id": "1"}] - } - - assert script.looks_like_alert_obj(obj) is False - - -def test_find_alert_in_context_finds_nested_alert(): - ctx = { - "VisionOne.Alert_Details(val.etag && val.etag == obj.etag)": { - "alert": { - "id": "wb-123", - "indicators": [{"id": "1", "type": "ip"}] - } - } - } - - result = script.find_alert_in_context(ctx) - - assert result == { - "id": "wb-123", - "indicators": [{"id": "1", "type": "ip"}] - } - - -def test_find_indicators_payload_from_incident_customfields_json(mocker): - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": '[{"id":"1","type":"ip","value":"1.2.3.4"}]' - } - } - - mocker.patch.object(script.demisto, "incident", return_value=incident) - - indicators, source = script.find_indicators_payload({}) - - assert indicators == [{"id": "1", "type": "ip", "value": "1.2.3.4"}] - assert source == "incident.CustomFields.trendmicrovisiononexdrindicatorsjson" - - -def test_find_indicators_payload_from_incident_top_level(mocker): - incident = { - "trendmicrovisiononexdrindicators": '[{"id":"2","type":"domain","value":"bad.com"}]' - } - - mocker.patch.object(script.demisto, "incident", return_value=incident) - - indicators, source = script.find_indicators_payload({}) - - assert indicators == [{"id": "2", "type": "domain", "value": "bad.com"}] - assert source == "incident.trendmicrovisiononexdrindicators" - - -def test_find_indicators_payload_from_context(mocker): - mocker.patch.object(script.demisto, "incident", return_value={}) - ctx = { - "some": { - "nested": { - "indicators_json": '[{"id":"3","type":"host","value":"host1"}]' - } - } - } - - indicators, source = script.find_indicators_payload(ctx) - - assert indicators == [{"id": "3", "type": "host", "value": "host1"}] - assert source == "context.indicators_json" - - -def test_normalize_indicator_with_scalar_value(): - ind = { - "id": "1", - "type": "domain", - "field": "dst.domain", - "value": "bad.com", - "related_entities": ["entity-1"], - "provenance": ["vision-one"], - "filter_ids": ["f-1"] - } - - result = script.normalize_indicator(ind) - - assert result["ID"] == "1" - assert result["Type"] == "domain" - assert result["Field"] == "dst.domain" - assert result["Value"] == "bad.com" - assert result["Related Entities"] == "entity-1" - assert result["Provenance"] == "vision-one" - assert result["Filter IDs"] == "f-1" - - -def test_normalize_indicator_with_dict_value(): - ind = { - "id": "2", - "type": "host", - "value": { - "name": "server1", - "ips": ["10.0.0.10", "1.2.3.4"], - "guid": "guid-123" - } - } - - result = script.normalize_indicator(ind) - - assert result["ID"] == "2" - assert result["Type"] == "host" - assert "name=server1" in result["Value"] - assert "ips=[10.0.0.10, 1.2.3.4]" in result["Value"] - assert "guid=guid-123" in result["Value"] - - -def test_make_table_with_rows(): - headers = ["ID", "Type"] - rows = [{"ID": "1", "Type": "ip"}] - - result = script.make_table(headers, rows) - - assert "|ID|Type|" in result - assert "|1|ip|" in result - - -def test_main_outputs_no_indicators_message_when_none_found(mocker): - mocker.patch.object(script.demisto, "context", return_value={}) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - assert result["ContentsFormat"] == "markdown" - assert "Couldn’t locate indicators" in result["Contents"] - - -def test_main_outputs_no_indicators_when_payload_empty(mocker): - ctx = {} - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": "[]" - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - assert "No indicators were returned on this alert." in result["Contents"] - assert "incident.CustomFields.trendmicrovisiononexdrindicatorsjson" in result["Contents"] - - -def test_main_outputs_markdown_from_incident_customfields(mocker): - ctx = {} - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": """ - [ - {"id":"1","type":"ip","field":"src.ip","value":"1.2.3.4","related_entities":["endpoint-1"],"provenance":["vision-one"],"filter_ids":["f1"]}, - {"id":"2","type":"domain","field":"dns.question.name","value":"bad.com","related_entities":["endpoint-2"],"provenance":["vision-one"],"filter_ids":["f2"]} - ] - """, - "originalalertid": "wb-123" - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "Trend Micro Vision One — Indicators" in result["Contents"] - assert "**Workbench ID:** `wb-123`" in result["Contents"] - assert "**Total Indicators:** 2" in result["Contents"] - assert "#### Domain" in result["Contents"] - assert "#### Ip" in result["Contents"] - assert "bad.com" in result["Contents"] - assert "1.2.3.4" in result["Contents"] - - -def test_main_falls_back_to_alert_in_context(mocker): - ctx = { - "some_nested_key": { - "alert": { - "id": "wb-999", - "indicators": [ - { - "id": "9", - "type": "command_line", - "field": "process.command_line", - "value": "powershell -enc abc" - } - ] - } - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - assert "**Workbench ID:** `wb-999`" in result["Contents"] - assert "**Source:** `context.alert.indicators`" in result["Contents"] - assert "#### Command Line" in result["Contents"] - assert "powershell -enc abc" in result["Contents"] \ No newline at end of file diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/README.md b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.py deleted file mode 100644 index dac396c5..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.py +++ /dev/null @@ -1,303 +0,0 @@ -# Script name: displayTMV1Metadata_FromAlertDetails -# Purpose: Render Vision One "Metadata" from Alert Details already in context OR from rule-mapped fields. -# Notes: -# - No external calls. -# - Prefers full VisionOne alert object in context (matched_rules / impact_scope available). -# - Falls back to correlation-rule mapped incident custom fields when alert object isn't present. -# - Degrades gracefully when impact_scope / matched_rules aren't available. - -# Load these for testing, but ignore in operation -# Universal Command allows multiple Vendor commands to be used by a single Universal Command -import demistomock as demisto # type: ignore -from CommonServerPython import * # type: ignore -import json - -# -------------------- tiny helpers -------------------- -def md_out(text): - demisto.results({"Type": 1, "ContentsFormat": "markdown", "Contents": text}) - -def esc(s): - if s is None: - return "" - if not isinstance(s, str): - try: - s = json.dumps(s, ensure_ascii=False) - except Exception: - s = str(s) - return s.replace("`", "\\`") - -def flat_join(seq, sep=", "): - if not seq: - return "" - return sep.join(str(x) for x in seq if x not in (None, "")) - -def safe_json_loads(value): - """ - Safely load JSON from string or pass through dict/list. - """ - if value is None: - return None - - if isinstance(value, (dict, list)): - return value - - if isinstance(value, str): - try: - return json.loads(value) - except Exception: - return None - - return None - -# -------------------- alert discovery (old path) -------------------- -def looks_like_alert_obj(obj: dict) -> bool: - if not isinstance(obj, dict): - return False - if not obj.get("id"): - return False - if obj.get("impact_scope") or obj.get("indicators"): - return True - return False - -def find_alert_and_meta_in_context(ctx): - seen = set() - - def _walk(node): - nid = id(node) - if nid in seen: - return (None, None) - seen.add(nid) - - if isinstance(node, dict): - if "alert" in node and looks_like_alert_obj(node["alert"]): - meta = {} - if "etag" in node and isinstance(node["etag"], (str, int)): - meta["etag"] = node["etag"] - return (node["alert"], meta) - - if looks_like_alert_obj(node): - return (node, {}) - - for v in node.values(): - a, m = _walk(v) - if a is not None: - if not m and "etag" in node: - m = {"etag": node.get("etag")} - return (a, m) - - elif isinstance(node, list): - for it in node: - a, m = _walk(it) - if a is not None: - return (a, m) - - return (None, None) - - return _walk(ctx) - -# -------------------- markdown table helpers -------------------- -def make_kv_table(pairs): - rows = [(k, v) for (k, v) in pairs if v not in (None, "", [], {})] - if not rows: - return "_none_\n" - md = "|Key|Value|\n|---|---|\n" - for k, v in rows: - md += f"|{esc(k)}|{esc(v)}|\n" - return md - -def make_table(headers, rows): - if not rows: - return "_none_\n" - md = "|" + "|".join(headers) + "|\n" - md += "|" + "|".join(["---"] * len(headers)) + "|\n" - for r in rows: - md += "|" + "|".join(esc(str(r.get(h, ""))) for h in headers) + "|\n" - return md - -# -------------------- summary helpers (old path only) -------------------- -def summarize_impact_scope(iscope): - if not isinstance(iscope, dict): - return None, None - counts = [] - for key in ("desktop_count", "server_count", "account_count", "email_address_count", - "container_count", "cloud_identity_count"): - if key in iscope: - counts.append((key.replace("_", " ").title(), iscope.get(key))) - entities = iscope.get("entities") or [] - return counts, entities - -def summarize_matched_rules(alert): - out = [] - rules = alert.get("matched_rules") or [] - if not isinstance(rules, list): - return out - for r in rules: - rule_name = r.get("name") or r.get("id") or "" - mfs = r.get("matched_filters") or [] - if not isinstance(mfs, list): - continue - for f in mfs: - filt_name = f.get("name") or f.get("id") or "" - when = "" - evs = f.get("matched_events") or [] - if isinstance(evs, list) and evs: - when = evs[0].get("matched_date_time") or "" - techniques = flat_join(f.get("mitre_technique_ids")) - out.append({ - "Rule": rule_name, - "Filter": filt_name, - "When": when, - "MITRE": techniques, - }) - return out - -# -------------------- new rule fallback (incident fields) -------------------- -def get_incident_cf(): - inc = demisto.incident() or {} - return inc.get("CustomFields") or {} - -def get_first_nonempty(*vals): - for v in vals: - if v not in (None, "", [], {}): - return v - return "" - -def count_indicators_from_cf(cf): - for k in ("trendmicrovisiononexdrindicatorsjson", "trendmicrovisiononexdrindicators", "indicators_json"): - if cf.get(k): - parsed = safe_json_loads(cf.get(k)) - if isinstance(parsed, list): - return len(parsed) - return 0 - -# -------------------- main -------------------- -def main(): - ctx = demisto.context() or {} - alert, meta = find_alert_and_meta_in_context(ctx) - - cf = get_incident_cf() - - # If full alert exists, use it (old rich mode). Otherwise use rule-mapped fields (fallback mode). - mode = "context-alert" if isinstance(alert, dict) else "rule-mapped" - - if isinstance(alert, dict): - wb_id = alert.get("id") or "—" - workbench_link = alert.get("workbench_link") or "" - provider = alert.get("alert_provider") or alert.get("provider") or "" - model = alert.get("model") or "" - model_type = alert.get("model_type") or "" - model_id = alert.get("model_id") or "" - severity = alert.get("severity") or "" - score = alert.get("score") - schema_version = alert.get("schema_version") or "" - incident_id = alert.get("incident_id") or "" - case_id = alert.get("case_id") or "" - owner_ids = alert.get("owner_ids") - owner_txt = flat_join(owner_ids) if isinstance(owner_ids, list) else (owner_ids or "") - - status = alert.get("status") or "" - inv_status = alert.get("investigation_status") or "" - inv_result = alert.get("investigation_result") or "" - - t_created = alert.get("created_date_time") or "" - t_updated = alert.get("updated_date_time") or "" - t_first_investigated = alert.get("first_investigated_date_time") or "" - - counts, _entities = summarize_impact_scope(alert.get("impact_scope") or {}) - mr_rows = summarize_matched_rules(alert) - - indicators = alert.get("indicators") or [] - ind_count = len(indicators) if isinstance(indicators, list) else 0 - - etag = meta.get("etag") if isinstance(meta, dict) else None - - else: - # ---- Fallback to rule-mapped fields ---- - # These are based on your correlation-rule mappings: - # - originalalertid, externallink/external_pivot_url, externalstatus - # - trendmicrovisiononexdrpriorityscore, trendmicrovisiononexdrinvestigationstatus - # - source_insert_ts, severity - wb_id = get_first_nonempty(cf.get("originalalertid"), "—") - workbench_link = get_first_nonempty(cf.get("externallink"), cf.get("external_pivot_url"), "") - provider = get_first_nonempty(cf.get("originalalertsource"), "Trend Micro Vision One") - model = get_first_nonempty(cf.get("originalalertname"), "") - model_type = "" - model_id = "" - severity = get_first_nonempty(cf.get("severity"), "") - score = get_first_nonempty(cf.get("trendmicrovisiononexdrpriorityscore"), "") - schema_version = "" - incident_id = "" - case_id = "" - owner_txt = "" - - status = get_first_nonempty(cf.get("externalstatus"), "") - inv_status = get_first_nonempty(cf.get("trendmicrovisiononexdrinvestigationstatus"), "") - inv_result = "" # not mapped in your rule output unless you add it - - t_created = get_first_nonempty(cf.get("source_insert_ts"), "") - t_updated = "" - t_first_investigated = "" - - counts = None - mr_rows = [] # not available without matched_rules - ind_count = count_indicators_from_cf(cf) - etag = None - - # ----- Compose Markdown ----- - md = [] - md.append("### Trend Micro Vision One — Metadata") - md.append(f"**Mode:** `{esc(mode)}` ") - md.append(f"**Workbench ID:** `{esc(wb_id)}` ") - if workbench_link: - md.append(f"**Workbench Link:** {esc(workbench_link)} ") - md.append("") - - core_pairs = [ - ("Provider", provider), - ("Model", model), - ("Model Type", model_type), - ("Model ID", model_id), - ("Severity", severity), - ("Score", score), - ("Schema Version", schema_version), - ("Incident ID", incident_id), - ("Case ID", case_id), - ("Owner IDs", owner_txt), - ("Indicators (count)", ind_count), - ("ETag", etag), - ] - md.append("#### Core") - md.append(make_kv_table(core_pairs)) - - status_pairs = [ - ("Status", status), - ("Investigation Status", inv_status), - ("Investigation Result", inv_result), - ] - md.append("#### Status") - md.append(make_kv_table(status_pairs)) - - time_pairs = [ - ("Created", t_created), - ("Updated", t_updated), - ("First Investigated", t_first_investigated), - ] - md.append("#### Timestamps") - md.append(make_kv_table(time_pairs)) - - md.append("#### Impact Scope") - if counts: - md.append(make_kv_table(counts)) - else: - md.append("_none (impact_scope not available in rule-mapped mode)_\n") - - md.append("#### Matched Rules") - if mr_rows: - md.append(make_table(["Rule", "Filter", "When", "MITRE"], mr_rows)) - else: - md.append("_none (matched_rules not available in rule-mapped mode)_\n") - - md_out("\n".join(md)) - -if __name__ in ("__builtin__", "builtins", "__main__"): - main() diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.yml b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.yml deleted file mode 100644 index ff037ff3..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails.yml +++ /dev/null @@ -1,21 +0,0 @@ -commonfields: - id: displayTMV1MetadataFromAlertDetails - version: -1 -dockerimage: demisto/python3:3.12.11.4819260 -enabled: true -engineinfo: {} -mainengineinfo: {} -name: displayTMV1MetadataFromAlertDetails -pswd: "" -runas: DBotWeakRole -runonce: false -script: '' -scripttarget: 0 -subtype: python3 -tags: -- dynamic-section -- SOC -- SOC_Framework -- Trend Micro Vision One -type: python -fromversion: 6.10.0 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails_test.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails_test.py deleted file mode 100644 index 3187c796..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1MetadataFromAlertDetails/displayTMV1MetadataFromAlertDetails_test.py +++ /dev/null @@ -1,317 +0,0 @@ -import sys -import types - -# ---- Mock XSOAR runtime modules before importing the script ---- - -demisto_mock = types.SimpleNamespace() -demisto_mock.context = lambda: {} -demisto_mock.incident = lambda: {} -demisto_mock.results = lambda x: x -demisto_mock.error = lambda x: None - -sys.modules["demistomock"] = demisto_mock - -common = types.ModuleType("CommonServerPython") -sys.modules["CommonServerPython"] = common - -import displayTMV1MetadataFromAlertDetails as script - - -def test_safe_json_loads_with_dict(): - value = {"a": 1} - - result = script.safe_json_loads(value) - - assert result == {"a": 1} - - -def test_safe_json_loads_with_json_string(): - value = '{"a": 1}' - - result = script.safe_json_loads(value) - - assert result == {"a": 1} - - -def test_safe_json_loads_with_invalid_json(): - value = "{not-json}" - - result = script.safe_json_loads(value) - - assert result is None - - -def test_looks_like_alert_obj_true_with_impact_scope(): - obj = { - "id": "wb-123", - "impact_scope": {"desktop_count": 1} - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_true_with_indicators(): - obj = { - "id": "wb-123", - "indicators": [{"id": "1"}] - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_false_without_id(): - obj = { - "impact_scope": {"desktop_count": 1} - } - - assert script.looks_like_alert_obj(obj) is False - - -def test_find_alert_and_meta_in_context_with_wrapped_alert(): - ctx = { - "VisionOne.Alert_Details(val.etag && val.etag == obj.etag)": { - "etag": "etag-1", - "alert": { - "id": "wb-123", - "severity": "high", - "impact_scope": {"desktop_count": 2} - } - } - } - - alert, meta = script.find_alert_and_meta_in_context(ctx) - - assert alert["id"] == "wb-123" - assert meta == {"etag": "etag-1"} - - -def test_find_alert_and_meta_in_context_with_direct_alert(): - ctx = { - "nested": { - "id": "wb-456", - "indicators": [{"id": "1"}] - } - } - - alert, meta = script.find_alert_and_meta_in_context(ctx) - - assert alert["id"] == "wb-456" - assert meta == {} - - -def test_make_kv_table_filters_empty_values(): - pairs = [ - ("A", "1"), - ("B", ""), - ("C", None), - ("D", "2"), - ] - - result = script.make_kv_table(pairs) - - assert "|A|1|" in result - assert "|D|2|" in result - assert "|B|" not in result - assert "|C|" not in result - - -def test_summarize_impact_scope(): - iscope = { - "desktop_count": 2, - "server_count": 1, - "entities": [{"id": "e1"}] - } - - counts, entities = script.summarize_impact_scope(iscope) - - assert ("Desktop Count", 2) in counts - assert ("Server Count", 1) in counts - assert entities == [{"id": "e1"}] - - -def test_summarize_matched_rules(): - alert = { - "matched_rules": [ - { - "name": "Rule 1", - "matched_filters": [ - { - "name": "Filter A", - "matched_events": [{"matched_date_time": "2026-03-13T10:00:00Z"}], - "mitre_technique_ids": ["T1059", "T1110"] - } - ] - } - ] - } - - result = script.summarize_matched_rules(alert) - - assert result == [ - { - "Rule": "Rule 1", - "Filter": "Filter A", - "When": "2026-03-13T10:00:00Z", - "MITRE": "T1059, T1110", - } - ] - - -def test_get_first_nonempty(): - result = script.get_first_nonempty(None, "", [], "value", "other") - - assert result == "value" - - -def test_count_indicators_from_cf(): - cf = { - "trendmicrovisiononexdrindicatorsjson": '[{"id":"1"},{"id":"2"}]' - } - - result = script.count_indicators_from_cf(cf) - - assert result == 2 - - -def test_main_outputs_rule_mapped_mode(mocker): - ctx = {} - incident = { - "CustomFields": { - "originalalertid": "wb-100", - "externallink": "https://visionone.example/alerts/wb-100", - "originalalertsource": "Trend Micro Vision One", - "originalalertname": "Suspicious Process", - "severity": "high", - "trendmicrovisiononexdrpriorityscore": "85", - "externalstatus": "open", - "trendmicrovisiononexdrinvestigationstatus": "new", - "source_insert_ts": "2026-03-13T12:00:00Z", - "trendmicrovisiononexdrindicatorsjson": '[{"id":"1"},{"id":"2"},{"id":"3"}]' - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "### Trend Micro Vision One — Metadata" in result["Contents"] - assert "**Mode:** `rule-mapped`" in result["Contents"] - assert "**Workbench ID:** `wb-100`" in result["Contents"] - assert "https://visionone.example/alerts/wb-100" in result["Contents"] - assert "|Provider|Trend Micro Vision One|" in result["Contents"] - assert "|Model|Suspicious Process|" in result["Contents"] - assert "|Severity|high|" in result["Contents"] - assert "|Score|85|" in result["Contents"] - assert "|Indicators (count)|3|" in result["Contents"] - assert "|Status|open|" in result["Contents"] - assert "|Investigation Status|new|" in result["Contents"] - assert "|Created|2026-03-13T12:00:00Z|" in result["Contents"] - assert "_none (impact_scope not available in rule-mapped mode)_" in result["Contents"] - assert "_none (matched_rules not available in rule-mapped mode)_" in result["Contents"] - - -def test_main_outputs_context_alert_mode(mocker): - ctx = { - "some_nested_key": { - "etag": "etag-99", - "alert": { - "id": "wb-999", - "workbench_link": "https://visionone.example/alerts/wb-999", - "alert_provider": "Vision One", - "model": "Malware Detected", - "model_type": "behavior", - "model_id": "model-1", - "severity": "critical", - "score": 99, - "schema_version": "1.0", - "incident_id": "inc-1", - "case_id": "case-1", - "owner_ids": ["user-1", "user-2"], - "status": "open", - "investigation_status": "in_progress", - "investigation_result": "malicious", - "created_date_time": "2026-03-13T10:00:00Z", - "updated_date_time": "2026-03-13T11:00:00Z", - "first_investigated_date_time": "2026-03-13T10:30:00Z", - "impact_scope": { - "desktop_count": 2, - "server_count": 1, - "entities": [{"id": "e1"}] - }, - "matched_rules": [ - { - "name": "Rule A", - "matched_filters": [ - { - "name": "Filter 1", - "matched_events": [{"matched_date_time": "2026-03-13T10:05:00Z"}], - "mitre_technique_ids": ["T1059"] - } - ] - } - ], - "indicators": [{"id": "1"}, {"id": "2"}] - } - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "**Mode:** `context-alert`" in result["Contents"] - assert "**Workbench ID:** `wb-999`" in result["Contents"] - assert "|Provider|Vision One|" in result["Contents"] - assert "|Model|Malware Detected|" in result["Contents"] - assert "|Model Type|behavior|" in result["Contents"] - assert "|Model ID|model-1|" in result["Contents"] - assert "|Severity|critical|" in result["Contents"] - assert "|Score|99|" in result["Contents"] - assert "|Schema Version|1.0|" in result["Contents"] - assert "|Incident ID|inc-1|" in result["Contents"] - assert "|Case ID|case-1|" in result["Contents"] - assert "|Owner IDs|user-1, user-2|" in result["Contents"] - assert "|Indicators (count)|2|" in result["Contents"] - assert "|ETag|etag-99|" in result["Contents"] - assert "|Status|open|" in result["Contents"] - assert "|Investigation Status|in_progress|" in result["Contents"] - assert "|Investigation Result|malicious|" in result["Contents"] - assert "|Created|2026-03-13T10:00:00Z|" in result["Contents"] - assert "|Updated|2026-03-13T11:00:00Z|" in result["Contents"] - assert "|First Investigated|2026-03-13T10:30:00Z|" in result["Contents"] - assert "|Desktop Count|2|" in result["Contents"] - assert "|Server Count|1|" in result["Contents"] - assert "|Rule|Filter|When|MITRE|" in result["Contents"] - assert "|Rule A|Filter 1|2026-03-13T10:05:00Z|T1059|" in result["Contents"] - - -def test_main_rule_mapped_defaults_when_fields_missing(mocker): - ctx = {} - incident = { - "CustomFields": {} - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - - assert "**Mode:** `rule-mapped`" in result["Contents"] - assert "**Workbench ID:** `—`" in result["Contents"] - assert "|Provider|Trend Micro Vision One|" in result["Contents"] - assert "|Indicators (count)|0|" in result["Contents"] \ No newline at end of file diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/README.md b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.py deleted file mode 100644 index 383c26bd..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.py +++ /dev/null @@ -1,300 +0,0 @@ -# Script name: displayTMV1ProcessFile_FromAlertDetails -# Purpose: Render a Process & File view using VisionOne Alert Details already in context OR from mapped fields. -# Notes: -# - No external calls; reads what's already in context / incident custom fields. -# - Works with correlation-rule output where indicators are stored as JSON string. -# - Matched Process Events require the full alert object (matched_rules) to exist in context. - -# Load these for testing, but ignore in operation -# Universal Command allows multiple Vendor commands to be used by a single Universal Command -import demistomock as demisto # type: ignore -from CommonServerPython import * # type: ignore -from collections import defaultdict -import json - -# -------------------- tiny helpers -------------------- -def md_out(text): - demisto.results({"Type": 1, "ContentsFormat": "markdown", "Contents": text}) - -def esc(s): - if s is None: - return "" - if not isinstance(s, str): - try: - s = json.dumps(s, ensure_ascii=False) - except Exception: - s = str(s) - return s.replace("`", "\\`") - -def flat_join(seq, sep=", "): - if not seq: - return "" - return sep.join(str(x) for x in seq if x not in (None, "")) - -def safe_json_loads(value): - if value is None: - return None - - if isinstance(value, (dict, list)): - return value - - if isinstance(value, str): - try: - return json.loads(value.strip()) - except Exception: - return None - - return None - -# -------------------- alert discovery (old path) -------------------- -def looks_like_alert_obj(obj: dict) -> bool: - if not isinstance(obj, dict): - return False - if not obj.get("id"): - return False - if obj.get("indicators"): - return True - iscope = obj.get("impact_scope") or {} - if isinstance(iscope, dict) and isinstance(iscope.get("entities"), list): - return True - return False - -def find_alert_in_context(ctx): - seen = set() - - def _walk(node): - nid = id(node) - if nid in seen: - return None - seen.add(nid) - - if isinstance(node, dict): - if "alert" in node and looks_like_alert_obj(node["alert"]): - return node["alert"] - if looks_like_alert_obj(node): - return node - for v in node.values(): - found = _walk(v) - if found is not None: - return found - elif isinstance(node, list): - for it in node: - found = _walk(it) - if found is not None: - return found - return None - - return _walk(ctx) - -# -------------------- indicators discovery (new rule path) -------------------- -def find_indicators_payload(ctx): - incident = demisto.incident() or {} - cf = incident.get("CustomFields", {}) or {} - - # 1️⃣ Incident CF JSON - indicators = safe_json_loads( - cf.get("trendmicrovisiononexdrindicatorsjson") - or cf.get("trendmicrovisiononexdrindicators") - ) - - if indicators is not None: - return indicators, "incident.CustomFields.trendmicrovisiononexdrindicatorsjson" - - # 2️⃣ Context JSON - indicators = deep_find_indicators_json(ctx) - - if indicators is not None: - return indicators, "context.indicators_json" - - # 3️⃣ Search nested context (tests expect this) - for value in ctx.values(): - if isinstance(value, dict): - indicators = safe_json_loads(value.get("indicators_json")) - if indicators is not None: - return indicators, "context_nested" - - return None, None - -# -------------------- rendering helpers -------------------- -def make_table(headers, rows): - if not rows: - return "_none_\n" - md = "|" + "|".join(headers) + "|\n" - md += "|" + "|".join(["---"] * len(headers)) + "|\n" - for r in rows: - md += "|" + "|".join(str(r.get(h, "")) for h in headers) + "|\n" - return md - -def normalize_indicator(ind): - row = { - "ID": ind.get("id"), - "Type": ind.get("type"), - "Field": ind.get("field") or "", - "Value": "", - "Related Entities": "", - "Provenance": "", - "Filter IDs": "", - } - - val = ind.get("value") - if isinstance(val, dict): - name = val.get("name") - ips = flat_join(val.get("ips")) - guid = val.get("guid") - parts = [] - if name: - parts.append(f"name={name}") - if ips: - parts.append(f"ips=[{ips}]") - if guid: - parts.append(f"guid={guid}") - row["Value"] = ", ".join(parts) if parts else esc(val) - else: - row["Value"] = esc(val) - - row["Related Entities"] = flat_join(ind.get("related_entities")) - row["Provenance"] = flat_join(ind.get("provenance")) - row["Filter IDs"] = flat_join(ind.get("filter_ids")) - - for k in list(row.keys()): - row[k] = esc(row[k]) - return row - -def extract_matched_process_events(alert): - """ - Only works if full alert object exists and includes matched_rules[*].matched_filters[*].matched_events[*] - """ - rows = [] - rules = alert.get("matched_rules") or [] - if not isinstance(rules, list): - return rows - - for r in rules: - rule_name = r.get("name") or r.get("id") or "" - mfs = r.get("matched_filters") or [] - for f in mfs: - filter_name = f.get("name") or f.get("id") or "" - events = f.get("matched_events") or [] - for ev in events: - ev_type = ev.get("type") or "" - if "PROCESS" in ev_type.upper(): - rows.append({ - "Time": esc(ev.get("matched_date_time") or ""), - "Type": esc(ev_type), - "UUID": esc(ev.get("uuid") or ""), - "Filter": esc(filter_name), - "Rule": esc(rule_name), - }) - return rows - -def deep_find_indicators_json(obj): - if isinstance(obj, dict): - if "indicators_json" in obj: - parsed = safe_json_loads(obj["indicators_json"]) - if parsed is not None: - return parsed - for v in obj.values(): - result = deep_find_indicators_json(v) - if result is not None: - return result - - if isinstance(obj, list): - for v in obj: - result = deep_find_indicators_json(v) - if result is not None: - return result - - return None - - -# -------------------- main -------------------- -def main(): - ctx = demisto.context() or {} - - alert = find_alert_in_context(ctx) - source = None - wb_id = "—" - - if isinstance(alert, dict): - wb_id = alert.get("id") or "—" - indicators = alert.get("indicators") or [] - source = "context.alert.indicators" - else: - indicators, source = find_indicators_payload(ctx) - if indicators is None: - md_out( - "### Trend Micro Vision One — Process & File\n" - "❌ Couldn’t locate a Vision One alert object in context, and couldn’t find mapped indicators JSON " - "(trendmicrovisiononexdrindicatorsjson / trendmicrovisiononexdrindicators / indicators_json)." - ) - return - - if not isinstance(indicators, list): - indicators = [] - - # Normalize & bucket - buckets = defaultdict(list) - for ind in indicators: - if not isinstance(ind, dict): - continue - row = normalize_indicator(ind) - t = (row.get("Type") or "").lower() - buckets[t].append(row) - - # Additional “file path” bucket: VisionOne often uses fields (processFilePath/objectRegistryData/etc.) - file_path_rows = [] - for t, rows in buckets.items(): - for r in rows: - f = (r.get("Field") or "").lower() - if f in ("processfilepath", "objectregistrydata", "fullpath", "filename", "parentfilepath"): - file_path_rows.append(r) - - md = [] - md.append("### Trend Micro Vision One — Process & File") - md.append(f"**Workbench ID:** `{wb_id}` ") - md.append(f"**Source:** `{esc(source or '—')}` \n") - - # ---- Process section ---- - md.append("#### Process") - - cmd_headers = ["ID", "Field", "Value", "Related Entities", "Provenance", "Filter IDs"] - cmd_rows = [{k: r.get(k, "") for k in cmd_headers} for r in buckets.get("command_line", [])] - md.append("**Command Lines**") - md.append(make_table(cmd_headers, cmd_rows)) - - proc_evt_headers = ["Time", "Type", "UUID", "Filter", "Rule"] - if isinstance(alert, dict) and alert.get("matched_rules"): - proc_evt_rows = extract_matched_process_events(alert) - md.append("**Matched Process Events**") - md.append(make_table(proc_evt_headers, proc_evt_rows)) - else: - md.append("**Matched Process Events**") - md.append("_none (matched_rules not available in correlation-rule output)_\n") - - # ---- File section ---- - md.append("#### File") - - path_headers = ["ID", "Type", "Field", "Value", "Related Entities", "Provenance", "Filter IDs"] - path_rows = [{k: r.get(k, "") for k in path_headers} for r in file_path_rows] - md.append("**File Paths**") - md.append(make_table(path_headers, path_rows)) - - hash_headers = ["ID", "Type", "Value", "Related Entities", "Provenance", "Filter IDs"] - hash_rows = [] - for t in ("file_sha256", "file_sha1", "file_md5"): - for r in buckets.get(t, []): - hash_rows.append({ - "ID": r.get("ID", ""), - "Type": r.get("Type", ""), - "Value": r.get("Value", ""), - "Related Entities": r.get("Related Entities", ""), - "Provenance": r.get("Provenance", ""), - "Filter IDs": r.get("Filter IDs", ""), - }) - md.append("**File Hashes**") - md.append(make_table(hash_headers, hash_rows)) - - md_out("\n".join(md)) - -if __name__ in ("__builtin__", "builtins", "__main__"): - main() diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.yml b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.yml deleted file mode 100644 index 2eed2419..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails.yml +++ /dev/null @@ -1,21 +0,0 @@ -commonfields: - id: displayTMV1ProcessFileFromAlertsDetails - version: -1 -dockerimage: demisto/python3:3.12.11.4819260 -enabled: true -engineinfo: {} -mainengineinfo: {} -name: displayTMV1ProcessFileFromAlertsDetails -pswd: "" -runas: DBotWeakRole -runonce: false -script: '' -scripttarget: 0 -subtype: python3 -tags: -- dynamic-section -- SOC -- SOC_Framework -- Trend Micro Vision One -type: python -fromversion: 6.10.0 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails_test.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails_test.py deleted file mode 100644 index c16f1c18..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1ProcessFileFromAlertsDetails/displayTMV1ProcessFileFromAlertsDetails_test.py +++ /dev/null @@ -1,337 +0,0 @@ -import sys -import types - -# ---- Mock XSOAR runtime modules before importing the script ---- - -demisto_mock = types.SimpleNamespace() -demisto_mock.context = lambda: {} -demisto_mock.incident = lambda: {} -demisto_mock.results = lambda x: x -demisto_mock.error = lambda x: None - -sys.modules["demistomock"] = demisto_mock - -common = types.ModuleType("CommonServerPython") -sys.modules["CommonServerPython"] = common - -import displayTMV1ProcessFileFromAlertsDetails as script - - -def test_safe_json_loads_with_list(): - value = [{"id": "1", "type": "file_sha256"}] - - result = script.safe_json_loads(value) - - assert result == [{"id": "1", "type": "file_sha256"}] - - -def test_safe_json_loads_with_json_string(): - value = '[{"id":"1","type":"command_line"}]' - - result = script.safe_json_loads(value) - - assert result == [{"id": "1", "type": "command_line"}] - - -def test_safe_json_loads_with_invalid_json(): - value = "{bad-json}" - - result = script.safe_json_loads(value) - - assert result is None - - -def test_looks_like_alert_obj_with_indicators(): - obj = { - "id": "wb-123", - "indicators": [{"id": "1"}] - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_with_impact_scope_entities(): - obj = { - "id": "wb-123", - "impact_scope": { - "entities": [{"id": "e1"}] - } - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_find_alert_in_context_finds_nested_alert(): - ctx = { - "nested": { - "alert": { - "id": "wb-123", - "indicators": [{"id": "1", "type": "command_line"}] - } - } - } - - result = script.find_alert_in_context(ctx) - - assert result == { - "id": "wb-123", - "indicators": [{"id": "1", "type": "command_line"}] - } - - -def test_find_indicators_payload_from_incident_customfields(mocker): - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": '[{"id":"1","type":"file_sha256","value":"abc"}]' - } - } - - mocker.patch.object(script.demisto, "incident", return_value=incident) - - indicators, source = script.find_indicators_payload({}) - - assert indicators == [{"id": "1", "type": "file_sha256", "value": "abc"}] - assert source == "incident.CustomFields.trendmicrovisiononexdrindicatorsjson" - - -def test_find_indicators_payload_from_context(mocker): - mocker.patch.object(script.demisto, "incident", return_value={}) - ctx = { - "some": { - "nested": { - "indicators_json": '[{"id":"2","type":"fullpath","value":"C:/Temp/bad.exe"}]' - } - } - } - - indicators, source = script.find_indicators_payload(ctx) - - assert indicators == [{"id": "2", "type": "fullpath", "value": "C:/Temp/bad.exe"}] - assert source == "context.indicators_json" - - -def test_normalize_indicator_with_scalar_value(): - ind = { - "id": "1", - "type": "command_line", - "field": "process.command_line", - "value": "powershell -enc abc", - "related_entities": ["endpoint-1"], - "provenance": ["vision-one"], - "filter_ids": ["f1"] - } - - result = script.normalize_indicator(ind) - - assert result["ID"] == "1" - assert result["Type"] == "command_line" - assert result["Field"] == "process.command_line" - assert result["Value"] == "powershell -enc abc" - assert result["Related Entities"] == "endpoint-1" - assert result["Provenance"] == "vision-one" - assert result["Filter IDs"] == "f1" - - -def test_normalize_indicator_with_dict_value(): - ind = { - "id": "2", - "type": "host", - "value": { - "name": "server1", - "ips": ["10.0.0.10", "1.2.3.4"], - "guid": "guid-123" - } - } - - result = script.normalize_indicator(ind) - - assert result["ID"] == "2" - assert result["Type"] == "host" - assert "name=server1" in result["Value"] - assert "ips=[10.0.0.10, 1.2.3.4]" in result["Value"] - assert "guid=guid-123" in result["Value"] - - -def test_extract_matched_process_events(): - alert = { - "matched_rules": [ - { - "name": "Rule A", - "matched_filters": [ - { - "name": "Filter 1", - "matched_events": [ - { - "type": "PROCESS_CREATE", - "matched_date_time": "2026-03-13T10:05:00Z", - "uuid": "uuid-1" - }, - { - "type": "NETWORK_CONNECTION", - "matched_date_time": "2026-03-13T10:06:00Z", - "uuid": "uuid-2" - } - ] - } - ] - } - ] - } - - result = script.extract_matched_process_events(alert) - - assert result == [ - { - "Time": "2026-03-13T10:05:00Z", - "Type": "PROCESS_CREATE", - "UUID": "uuid-1", - "Filter": "Filter 1", - "Rule": "Rule A", - } - ] - - -def test_main_outputs_error_when_nothing_found(mocker): - mocker.patch.object(script.demisto, "context", return_value={}) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "Couldn’t locate a Vision One alert object in context" in result["Contents"] - - -def test_main_outputs_from_incident_mapped_indicators(mocker): - ctx = {} - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": """ - [ - {"id":"1","type":"command_line","field":"process.command_line","value":"powershell -enc abc","related_entities":["endpoint-1"],"provenance":["vision-one"],"filter_ids":["f1"]}, - {"id":"2","type":"file_sha256","field":"processFileHash","value":"abcdef123456","related_entities":["endpoint-1"],"provenance":["vision-one"],"filter_ids":["f2"]}, - {"id":"3","type":"fullpath","field":"fullpath","value":"C:/Temp/bad.exe","related_entities":["endpoint-1"],"provenance":["vision-one"],"filter_ids":["f3"]} - ] - """ - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "### Trend Micro Vision One — Process & File" in result["Contents"] - assert "**Workbench ID:** `—`" in result["Contents"] - assert "**Source:** `incident.CustomFields.trendmicrovisiononexdrindicatorsjson`" in result["Contents"] - assert "**Command Lines**" in result["Contents"] - assert "powershell -enc abc" in result["Contents"] - assert "**Matched Process Events**" in result["Contents"] - assert "_none (matched_rules not available in correlation-rule output)_" in result["Contents"] - assert "**File Paths**" in result["Contents"] - assert "C:/Temp/bad.exe" in result["Contents"] - assert "**File Hashes**" in result["Contents"] - assert "abcdef123456" in result["Contents"] - - -def test_main_outputs_from_context_alert_with_matched_rules(mocker): - ctx = { - "nested": { - "alert": { - "id": "wb-999", - "indicators": [ - { - "id": "1", - "type": "command_line", - "field": "process.command_line", - "value": "cmd.exe /c whoami", - "related_entities": ["endpoint-1"], - "provenance": ["vision-one"], - "filter_ids": ["f1"] - }, - { - "id": "2", - "type": "file_sha256", - "field": "file.hash.sha256", - "value": "deadbeef", - "related_entities": ["endpoint-1"], - "provenance": ["vision-one"], - "filter_ids": ["f2"] - }, - { - "id": "3", - "type": "fullpath", - "field": "fullpath", - "value": "/tmp/bad.bin", - "related_entities": ["endpoint-1"], - "provenance": ["vision-one"], - "filter_ids": ["f3"] - } - ], - "matched_rules": [ - { - "name": "Rule A", - "matched_filters": [ - { - "name": "Filter 1", - "matched_events": [ - { - "type": "PROCESS_CREATE", - "matched_date_time": "2026-03-13T10:05:00Z", - "uuid": "uuid-1" - } - ] - } - ] - } - ] - } - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert "**Workbench ID:** `wb-999`" in result["Contents"] - assert "**Source:** `context.alert.indicators`" in result["Contents"] - assert "cmd.exe /c whoami" in result["Contents"] - assert "/tmp/bad.bin" in result["Contents"] - assert "deadbeef" in result["Contents"] - assert "|Time|Type|UUID|Filter|Rule|" in result["Contents"] - assert "|2026-03-13T10:05:00Z|PROCESS_CREATE|uuid-1|Filter 1|Rule A|" in result["Contents"] - - -def test_main_outputs_empty_tables_when_indicators_empty(mocker): - ctx = {} - incident = { - "CustomFields": { - "trendmicrovisiononexdrindicatorsjson": "[]" - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - - assert "**Command Lines**" in result["Contents"] - assert "**File Paths**" in result["Contents"] - assert "**File Hashes**" in result["Contents"] - assert "_none_" in result["Contents"] \ No newline at end of file diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/README.md b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.py deleted file mode 100644 index 7cfc894d..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.py +++ /dev/null @@ -1,307 +0,0 @@ -# Script name: displayTMV1RelatedAssets_FromAlertDetails -# Purpose: Render "Related Assets" from VisionOne Alert Details in context OR from rule-mapped fields. -# Notes: -# - No external calls. -# - Prefers alert.impact_scope.entities[] when present. -# - Falls back to incident CustomFields (rule-mapped) when not present. -# - Fallback is best-effort; it will not include full entity graphs/relationships. - -# Load these for testing, but ignore in operation -# Universal Command allows multiple Vendor commands to be used by a single Universal Command -import demistomock as demisto # type: ignore -from CommonServerPython import * # type: ignore -from collections import defaultdict - -# -------------------- tiny helpers -------------------- -def md_out(text): - demisto.results({"Type": 1, "ContentsFormat": "markdown", "Contents": text}) - -def esc(s): - if s is None: - return "" - if not isinstance(s, str): - try: - s = json.dumps(s, ensure_ascii=False) - except Exception: - s = str(s) - return s.replace("`", "\\`") - -def flat_join(seq, sep=", "): - if not seq: - return "" - return sep.join(str(x) for x in seq if x not in (None, "")) - -# -------------------- alert discovery (old path) -------------------- -def looks_like_alert_obj(obj: dict) -> bool: - if not isinstance(obj, dict): - return False - if not obj.get("id"): - return False - iscope = obj.get("impact_scope") or {} - if isinstance(iscope, dict) and isinstance(iscope.get("entities"), list): - return True - if obj.get("indicators"): - return True - return False - -def find_alert_in_context(ctx): - seen = set() - def _walk(node): - nid = id(node) - if nid in seen: - return None - seen.add(nid) - - if isinstance(node, dict): - if "alert" in node and looks_like_alert_obj(node["alert"]): - return node["alert"] - if looks_like_alert_obj(node): - return node - for v in node.values(): - found = _walk(v) - if found is not None: - return found - elif isinstance(node, list): - for it in node: - found = _walk(it) - if found is not None: - return found - return None - return _walk(ctx) - -def make_table(headers, rows): - if not rows: - return "_none_\n" - md = "|" + "|".join(headers) + "|\n" - md += "|" + "|".join(["---"] * len(headers)) + "|\n" - for r in rows: - md += "|" + "|".join(str(r.get(h, "")) for h in headers) + "|\n" - return md - -# -------------------- normalization (old path entities[]) -------------------- -def norm_host_entity(e): - ev = e.get("entity_value") or {} - name = ev.get("name") or "" - ips = flat_join(ev.get("ips")) - guid = ev.get("guid") or "" - prov = flat_join(e.get("provenance")) - rel = flat_join(e.get("related_entities")) - return { - "GUID": esc(guid), - "Name": esc(name), - "IPs": esc(ips), - "Provenance": esc(prov), - "Related Entities": esc(rel), - } - -def norm_account_entity(e): - val = e.get("entity_value") - prov = flat_join(e.get("provenance")) - rel = flat_join(e.get("related_entities")) - return { - "Account": esc(val), - "Provenance": esc(prov), - "Related Entities": esc(rel), - } - -def norm_generic_entity(e): - ev = e.get("entity_value") - if isinstance(ev, dict): - pretty = [] - for k in ("name", "address", "id", "guid", "resource", "namespace"): - if ev.get(k): - v = ev.get(k) - if isinstance(v, list): - v = "[" + flat_join(v) + "]" - pretty.append(f"{k}={v}") - val_str = ", ".join(pretty) if pretty else esc(ev) - else: - val_str = esc(ev) - prov = flat_join(e.get("provenance")) - rel = flat_join(e.get("related_entities")) - return { - "Entity ID": esc(e.get("entity_id") or ""), - "Value": val_str, - "Provenance": esc(prov), - "Related Entities": esc(rel), - } - -def build_relationship_map(entities): - host_index = {} - for e in entities: - if e.get("entity_type") == "host": - ev = e.get("entity_value") or {} - disp = ev.get("name") or ev.get("guid") or e.get("entity_id") or "host" - host_index[e.get("entity_id")] = disp - if ev.get("guid"): - host_index[ev.get("guid")] = disp - - lines = [] - for e in entities: - etype = e.get("entity_type") - if not e.get("related_entities"): - continue - if etype == "account": - src_label = f"account {e.get('entity_value')}" - elif etype == "host": - ev = e.get("entity_value") or {} - src_label = f"host {ev.get('name') or ev.get('guid') or e.get('entity_id')}" - else: - src_label = f"{etype} {e.get('entity_id') or ''}".strip() - - for target in e.get("related_entities") or []: - tgt = host_index.get(target, target) - if tgt: - lines.append(f"- {esc(src_label)} → **{esc(str(tgt))}**") - return lines - -# -------------------- fallback (rule-mapped CustomFields) -------------------- -def get_incident_cf(): - inc = demisto.incident() or {} - return inc.get("CustomFields") or {} - -def first_nonempty(*vals): - for v in vals: - if v not in (None, "", [], {}): - return v - return "" - -def build_fallback_assets_from_cf(cf): - """ - Best-effort assets based on your rule mappings. - Returns: - hosts_rows, accounts_rows, misc_rows (list of dicts) - """ - hosts = [] - accounts = [] - misc = [] - - # Host (agent_id / agent_hostname / action_local_ip / mac) - host_guid = first_nonempty(cf.get("agent_id"), cf.get("agentid")) - host_name = first_nonempty(cf.get("agent_hostname"), cf.get("agenthostname")) - local_ip = first_nonempty(cf.get("action_local_ip"), cf.get("prenatsourceip")) - mac = first_nonempty(cf.get("mac"), cf.get("mac_address")) - - if host_guid or host_name or local_ip or mac: - hosts.append({ - "GUID": esc(host_guid), - "Name": esc(host_name), - "IPs": esc(local_ip), # single value in rule-mapped mode - "Provenance": esc("Correlation Rule"), - "Related Entities": esc(""), - }) - if mac: - misc.append({ - "Key": "MAC", - "Value": esc(mac), - }) - - # Account (actor_effective_username / userid) - uname = first_nonempty(cf.get("actor_effective_username"), cf.get("username"), cf.get("user_name")) - uid = first_nonempty(cf.get("userid"), cf.get("user_id")) - - if uname or uid: - acct_val = uname if uname else "—" - if uid: - acct_val = f"{acct_val} (id={uid})" - accounts.append({ - "Account": esc(acct_val), - "Provenance": esc("Correlation Rule"), - "Related Entities": esc(""), - }) - - # Domain (agent_device_domain) - dom = first_nonempty(cf.get("agent_device_domain"), cf.get("domain")) - if dom: - misc.append({"Key": "Domain", "Value": esc(dom)}) - - # Remote IP (action_remote_ip) - rip = first_nonempty(cf.get("action_remote_ip"), cf.get("remote_ip_str")) - if rip: - misc.append({"Key": "Remote IP", "Value": esc(rip)}) - - return hosts, accounts, misc - -# -------------------- main -------------------- -def main(): - ctx = demisto.context() or {} - alert = find_alert_in_context(ctx) - - if isinstance(alert, dict): - # ---- Old rich mode ---- - wb_id = alert.get("id") or "—" - entities = (alert.get("impact_scope", {}) or {}).get("entities", []) or [] - - if not entities: - md_out( - "### Trend Micro Vision One — Related Assets\n" - f"**Mode:** `context-alert`\n" - f"**Workbench ID:** `{esc(wb_id)}` \n" - "_No related assets present in impact scope._" - ) - return - - by_type = defaultdict(list) - for e in entities: - et = (e.get("entity_type") or "").lower() - by_type[et].append(e) - - counts = ", ".join(f"{t}: {len(v)}" for t, v in by_type.items()) - md = [] - md.append("### Trend Micro Vision One — Related Assets") - md.append("**Mode:** `context-alert` ") - md.append(f"**Workbench ID:** `{esc(wb_id)}` ") - md.append(f"**By Type:** {esc(counts)}\n") - - host_rows = [norm_host_entity(e) for e in by_type.get("host", [])] - md.append("#### Hosts") - md.append(make_table(["GUID", "Name", "IPs", "Provenance", "Related Entities"], host_rows)) - - acct_rows = [norm_account_entity(e) for e in by_type.get("account", [])] - md.append("#### Accounts") - md.append(make_table(["Account", "Provenance", "Related Entities"], acct_rows)) - - for t, items in sorted(by_type.items()): - if t in ("host", "account"): - continue - rows = [norm_generic_entity(e) for e in items] - md.append(f"#### {t.replace('_',' ').title()}") - md.append(make_table(["Entity ID", "Value", "Provenance", "Related Entities"], rows)) - - rel_lines = build_relationship_map(entities) - if rel_lines: - md.append("#### Relationships") - md.extend(rel_lines) - - md_out("\n".join(md)) - return - - # ---- Fallback rule-mapped mode ---- - cf = get_incident_cf() - wb_id = first_nonempty(cf.get("originalalertid"), "—") - - hosts, accounts, misc = build_fallback_assets_from_cf(cf) - - md = [] - md.append("### Trend Micro Vision One — Related Assets") - md.append("**Mode:** `rule-mapped` ") - md.append(f"**Workbench ID:** `{esc(wb_id)}` ") - md.append("_Note: impact_scope/entities not available; this is best-effort from correlation rule fields._\n") - - md.append("#### Hosts") - md.append(make_table(["GUID", "Name", "IPs", "Provenance", "Related Entities"], hosts)) - - md.append("#### Accounts") - md.append(make_table(["Account", "Provenance", "Related Entities"], accounts)) - - if misc: - md.append("#### Other") - md.append(make_table(["Key", "Value"], misc)) - else: - md.append("#### Other") - md.append("_none_\n") - - md_out("\n".join(md)) - -if __name__ in ("__builtin__", "builtins", "__main__"): - main() diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.yml b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.yml deleted file mode 100644 index 8c40411a..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails.yml +++ /dev/null @@ -1,21 +0,0 @@ -commonfields: - id: displayTMV1RelatedAssetsFromAlertDetails - version: -1 -dockerimage: demisto/python3:3.12.11.4819260 -enabled: true -engineinfo: {} -mainengineinfo: {} -name: displayTMV1RelatedAssetsFromAlertDetails -pswd: "" -runas: DBotWeakRole -runonce: false -script: '' -scripttarget: 0 -subtype: python3 -tags: -- dynamic-section -- SOC -- SOC_Framework -- Trend Micro Vision One -type: python -fromversion: 6.10.0 diff --git a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails_test.py b/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails_test.py deleted file mode 100644 index 664787bf..00000000 --- a/Packs/SocFrameworkTrendMicroVisionOne/Scripts/displayTMV1RelatedAssetsFromAlertDetails/displayTMV1RelatedAssetsFromAlertDetails_test.py +++ /dev/null @@ -1,339 +0,0 @@ -import sys -import types - -# ---- Mock XSOAR runtime modules before importing the script ---- - -demisto_mock = types.SimpleNamespace() -demisto_mock.context = lambda: {} -demisto_mock.incident = lambda: {} -demisto_mock.results = lambda x: x -demisto_mock.error = lambda x: None - -sys.modules["demistomock"] = demisto_mock - -common = types.ModuleType("CommonServerPython") -sys.modules["CommonServerPython"] = common - -import displayTMV1RelatedAssetsFromAlertDetails as script - - -def test_looks_like_alert_obj_with_impact_scope_entities(): - obj = { - "id": "wb-123", - "impact_scope": { - "entities": [{"entity_id": "e1"}] - } - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_with_indicators(): - obj = { - "id": "wb-123", - "indicators": [{"id": "1"}] - } - - assert script.looks_like_alert_obj(obj) is True - - -def test_looks_like_alert_obj_false_without_id(): - obj = { - "impact_scope": { - "entities": [{"entity_id": "e1"}] - } - } - - assert script.looks_like_alert_obj(obj) is False - - -def test_find_alert_in_context_finds_nested_alert(): - ctx = { - "nested": { - "alert": { - "id": "wb-123", - "impact_scope": { - "entities": [{"entity_id": "host-1", "entity_type": "host"}] - } - } - } - } - - result = script.find_alert_in_context(ctx) - - assert result["id"] == "wb-123" - assert result["impact_scope"]["entities"][0]["entity_id"] == "host-1" - - -def test_norm_host_entity(): - entity = { - "entity_type": "host", - "entity_value": { - "name": "server1", - "ips": ["10.0.0.10", "1.2.3.4"], - "guid": "guid-123" - }, - "provenance": ["vision-one"], - "related_entities": ["acct-1"] - } - - result = script.norm_host_entity(entity) - - assert result == { - "GUID": "guid-123", - "Name": "server1", - "IPs": "10.0.0.10, 1.2.3.4", - "Provenance": "vision-one", - "Related Entities": "acct-1", - } - - -def test_norm_account_entity(): - entity = { - "entity_type": "account", - "entity_value": "alice", - "provenance": ["vision-one"], - "related_entities": ["host-1"] - } - - result = script.norm_account_entity(entity) - - assert result == { - "Account": "alice", - "Provenance": "vision-one", - "Related Entities": "host-1", - } - - -def test_norm_generic_entity_with_dict_value(): - entity = { - "entity_type": "ip", - "entity_id": "ip-1", - "entity_value": { - "address": "1.2.3.4", - "namespace": "public" - }, - "provenance": ["vision-one"], - "related_entities": ["host-1"] - } - - result = script.norm_generic_entity(entity) - - assert result["Entity ID"] == "ip-1" - assert "address=1.2.3.4" in result["Value"] - assert "namespace=public" in result["Value"] - assert result["Provenance"] == "vision-one" - assert result["Related Entities"] == "host-1" - - -def test_build_relationship_map(): - entities = [ - { - "entity_id": "host-1", - "entity_type": "host", - "entity_value": { - "name": "server1", - "guid": "guid-123" - }, - "related_entities": [] - }, - { - "entity_id": "acct-1", - "entity_type": "account", - "entity_value": "alice", - "related_entities": ["host-1"] - } - ] - - result = script.build_relationship_map(entities) - - assert result == [ - "- account alice → **server1**" - ] - - -def test_build_fallback_assets_from_cf(): - cf = { - "agent_id": "agent-123", - "agent_hostname": "host1", - "action_local_ip": "10.0.0.10", - "mac": "aa:bb:cc:dd:ee:ff", - "actor_effective_username": "scott", - "userid": "u-123", - "agent_device_domain": "corp.local", - "action_remote_ip": "8.8.8.8", - } - - hosts, accounts, misc = script.build_fallback_assets_from_cf(cf) - - assert hosts == [ - { - "GUID": "agent-123", - "Name": "host1", - "IPs": "10.0.0.10", - "Provenance": "Correlation Rule", - "Related Entities": "", - } - ] - assert accounts == [ - { - "Account": "scott (id=u-123)", - "Provenance": "Correlation Rule", - "Related Entities": "", - } - ] - assert {"Key": "MAC", "Value": "aa:bb:cc:dd:ee:ff"} in misc - assert {"Key": "Domain", "Value": "corp.local"} in misc - assert {"Key": "Remote IP", "Value": "8.8.8.8"} in misc - - -def test_main_outputs_context_alert_with_entities(mocker): - ctx = { - "some_nested_key": { - "alert": { - "id": "wb-999", - "impact_scope": { - "entities": [ - { - "entity_id": "host-1", - "entity_type": "host", - "entity_value": { - "name": "server1", - "ips": ["10.0.0.10"], - "guid": "guid-123" - }, - "provenance": ["vision-one"], - "related_entities": [] - }, - { - "entity_id": "acct-1", - "entity_type": "account", - "entity_value": "alice", - "provenance": ["vision-one"], - "related_entities": ["host-1"] - }, - { - "entity_id": "ip-1", - "entity_type": "ip", - "entity_value": { - "address": "1.2.3.4" - }, - "provenance": ["vision-one"], - "related_entities": ["host-1"] - } - ] - } - } - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "### Trend Micro Vision One — Related Assets" in result["Contents"] - assert "**Mode:** `context-alert`" in result["Contents"] - assert "**Workbench ID:** `wb-999`" in result["Contents"] - assert "**By Type:** host: 1, account: 1, ip: 1" in result["Contents"] - assert "#### Hosts" in result["Contents"] - assert "server1" in result["Contents"] - assert "guid-123" in result["Contents"] - assert "#### Accounts" in result["Contents"] - assert "alice" in result["Contents"] - assert "#### Ip" in result["Contents"] - assert "address=1.2.3.4" in result["Contents"] - assert "#### Relationships" in result["Contents"] - assert "- account alice → **server1**" in result["Contents"] - - -def test_main_outputs_context_alert_no_entities(mocker): - ctx = { - "nested": { - "alert": { - "id": "wb-100", - "impact_scope": { - "entities": [] - } - } - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value={}) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - assert "**Mode:** `context-alert`" in result["Contents"] - assert "**Workbench ID:** `wb-100`" in result["Contents"] - assert "_No related assets present in impact scope._" in result["Contents"] - - -def test_main_outputs_rule_mapped_fallback(mocker): - ctx = {} - incident = { - "CustomFields": { - "originalalertid": "wb-200", - "agent_id": "agent-123", - "agent_hostname": "host2", - "action_local_ip": "10.0.0.20", - "mac": "11:22:33:44:55:66", - "actor_effective_username": "bob", - "userid": "u-456", - "agent_device_domain": "example.local", - "action_remote_ip": "9.9.9.9", - } - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - results_mock.assert_called_once() - result = results_mock.call_args[0][0] - - assert result["ContentsFormat"] == "markdown" - assert "**Mode:** `rule-mapped`" in result["Contents"] - assert "**Workbench ID:** `wb-200`" in result["Contents"] - assert "_Note: impact_scope/entities not available; this is best-effort from correlation rule fields._" in result["Contents"] - assert "#### Hosts" in result["Contents"] - assert "agent-123" in result["Contents"] - assert "host2" in result["Contents"] - assert "10.0.0.20" in result["Contents"] - assert "#### Accounts" in result["Contents"] - assert "bob (id=u-456)" in result["Contents"] - assert "#### Other" in result["Contents"] - assert "11:22:33:44:55:66" in result["Contents"] - assert "example.local" in result["Contents"] - assert "9.9.9.9" in result["Contents"] - - -def test_main_outputs_rule_mapped_empty(mocker): - ctx = {} - incident = { - "CustomFields": {} - } - - mocker.patch.object(script.demisto, "context", return_value=ctx) - mocker.patch.object(script.demisto, "incident", return_value=incident) - results_mock = mocker.patch.object(script.demisto, "results") - - script.main() - - result = results_mock.call_args[0][0] - - assert "**Mode:** `rule-mapped`" in result["Contents"] - assert "**Workbench ID:** `—`" in result["Contents"] - assert "#### Hosts" in result["Contents"] - assert "#### Accounts" in result["Contents"] - assert "#### Other" in result["Contents"] - assert "_none_" in result["Contents"] \ No newline at end of file diff --git a/Packs/soc-optimization-unified/Lists/SOCExecutionList_V3/SOCExecutionList_V3.json b/Packs/soc-optimization-unified/Lists/SOCExecutionList_V3/SOCExecutionList_V3.json index 9489b10e..6f03f9ed 100644 --- a/Packs/soc-optimization-unified/Lists/SOCExecutionList_V3/SOCExecutionList_V3.json +++ b/Packs/soc-optimization-unified/Lists/SOCExecutionList_V3/SOCExecutionList_V3.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" + "fromVersion": "6.5.0", + "display_name": "SOCExecutionList_V3" } diff --git a/Packs/soc-optimization-unified/Lists/SOCFrameworkActions_V3/SOCFrameworkActions_V3.json b/Packs/soc-optimization-unified/Lists/SOCFrameworkActions_V3/SOCFrameworkActions_V3.json index 029aebaa..f0d62957 100644 --- a/Packs/soc-optimization-unified/Lists/SOCFrameworkActions_V3/SOCFrameworkActions_V3.json +++ b/Packs/soc-optimization-unified/Lists/SOCFrameworkActions_V3/SOCFrameworkActions_V3.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" -} \ No newline at end of file + "fromVersion": "6.5.0", + "display_name": "SOCFrameworkActions_V3" +} diff --git a/Packs/soc-optimization-unified/Lists/SOCOptimizationConfig_V3/SOCOptimizationConfig_V3.json b/Packs/soc-optimization-unified/Lists/SOCOptimizationConfig_V3/SOCOptimizationConfig_V3.json index 0eeb5501..daa3d9ed 100644 --- a/Packs/soc-optimization-unified/Lists/SOCOptimizationConfig_V3/SOCOptimizationConfig_V3.json +++ b/Packs/soc-optimization-unified/Lists/SOCOptimizationConfig_V3/SOCOptimizationConfig_V3.json @@ -1,27 +1,28 @@ { - "allRead": true, - "allReadWrite": true, - "cacheVersn": 0, - "data": "-", - "definitionId": "", - "description": "", - "detached": false, - "fromServerVersion": "", - "id": "SOCOptimizationConfig_V3", - "isOverridable": false, - "itemVersion": "", - "locked": false, - "name": "SOCOptimizationConfig_V3", - "fromVersion": "6.5.0", - "nameLocked": false, - "packID": "", - "packName": "", - "previousAllRead": true, - "previousAllReadWrite": true, - "system": false, - "tags": null, - "toServerVersion": "", - "truncated": false, - "type": "json", - "version": -1 -} \ No newline at end of file + "allRead": true, + "allReadWrite": true, + "cacheVersn": 0, + "data": "-", + "definitionId": "", + "description": "", + "detached": false, + "fromServerVersion": "", + "id": "SOCOptimizationConfig_V3", + "isOverridable": false, + "itemVersion": "", + "locked": false, + "name": "SOCOptimizationConfig_V3", + "fromVersion": "6.5.0", + "nameLocked": false, + "packID": "", + "packName": "", + "previousAllRead": true, + "previousAllReadWrite": true, + "system": false, + "tags": null, + "toServerVersion": "", + "truncated": false, + "type": "json", + "version": -1, + "display_name": "SOCOptimizationConfig_V3" +} diff --git a/Packs/soc-optimization-unified/Lists/SOCProductCategoryMap_V3/SOCProductCategoryMap_V3.json b/Packs/soc-optimization-unified/Lists/SOCProductCategoryMap_V3/SOCProductCategoryMap_V3.json index 10cce951..647024c4 100644 --- a/Packs/soc-optimization-unified/Lists/SOCProductCategoryMap_V3/SOCProductCategoryMap_V3.json +++ b/Packs/soc-optimization-unified/Lists/SOCProductCategoryMap_V3/SOCProductCategoryMap_V3.json @@ -23,5 +23,6 @@ "truncated": false, "type": "json", "version": -1, - "fromVersion": "6.5.0" -} \ No newline at end of file + "fromVersion": "6.5.0", + "display_name": "SOCProductCategoryMap_V3" +} diff --git a/pack_catalog.json b/pack_catalog.json index f53bcabc..b5dbbadd 100644 --- a/pack_catalog.json +++ b/pack_catalog.json @@ -4,7 +4,7 @@ "id": "SocFrameworkCrowdstrikeFalcon", "display_name": "SOC CrowdStrike Falcon Integration Enhancement for Cortex XSIAM", "category": "End Point", - "version": "1.0.44", + "version": "1.0.45", "path": "Packs/SocFrameworkCrowdstrikeFalcon", "visible": true, "xsoar_config": "https://raw.githubusercontent.com/Palo-Cortex/secops-framework/refs/heads/main/Packs/SocFrameworkCrowdstrikeFalcon/xsoar_config.json" diff --git a/tools/fix_errors.py b/tools/fix_errors.py index 21c5d0af..9cdebaee 100644 --- a/tools/fix_errors.py +++ b/tools/fix_errors.py @@ -26,6 +26,14 @@ auto-fixed. A clear manual fix instruction is printed instead. - Pre-flight scan of all Lists/**/*.json files for missing required descriptor fields, giving specific file paths rather than a general warning. + +CHANGED (2026-03b): +- preflight_list_descriptors() now AUTO-FIXES missing fields by writing them back to the + descriptor JSON, rather than only printing a warning. The fix derives missing id/name/ + display_name from the filename stem, and defaults type to 'json' if absent. +- run_demisto_format() now sets DEMISTO_SDK_SKIP_CONTENT_GRAPH=1 to avoid crashing when + Docker is not running (Neo4j/content graph startup failure). If the format output still + contains a Docker error, a clear actionable message is printed instead of raw traceback. """ import argparse import json @@ -117,6 +125,12 @@ def de_ansi(s: str) -> str: # Required fields for List descriptor JSON files LIST_DESCRIPTOR_REQUIRED = ('id', 'name', 'display_name', 'type') +# Signals a Docker / Neo4j failure inside format output +DOCKER_ERROR_RE = re.compile( + r'docker\.errors\.DockerException|Failed to init docker client|neo4j_service', + re.IGNORECASE, +) + def parse_semver(v: str): if not v or not isinstance(v, str): @@ -197,7 +211,8 @@ def _is_script_yaml(path: str) -> bool: def preflight_pydantic_errors(full_text: str) -> int: """ Scans the full SDK output text for pydantic ValidationError blocks. - These are emitted before any per-file error lines and cannot be auto-fixed. + These are emitted before any per-file error lines and cannot be auto-fixed + (no file path is included in the error). Prints a clear manual fix instruction for each block found. Returns the count of blocks found. """ @@ -249,11 +264,15 @@ def preflight_pydantic_errors(full_text: str) -> int: def preflight_list_descriptors(repo_root: str) -> int: """ Walks all Packs/**/Lists/**/*.json files (excluding *_data.json) and checks - for the four required descriptor fields. Prints specific file paths and - missing fields so the user knows exactly what to fix. - Returns the count of files with issues. + for the four required descriptor fields. + + AUTO-FIX: If any required field is missing, writes it back to the file. + - id, name, display_name are derived from the filename stem. + - type defaults to 'json' if absent. + + Returns the count of files that were fixed. """ - issues = 0 + fixed = 0 packs_dir = os.path.join(repo_root, 'Packs') if not os.path.isdir(packs_dir): return 0 @@ -279,17 +298,32 @@ def preflight_list_descriptors(repo_root: str) -> int: if not data.get(field) ] - if missing: - issues += 1 - rel = os.path.relpath(fpath, repo_root) + if not missing: + continue + + # Derive the canonical name from the filename stem (strip .json) + list_name = os.path.splitext(fname)[0] + rel = os.path.relpath(fpath, repo_root) + + for field in missing: + if field == 'type': + data['type'] = 'json' + else: + # id, name, display_name all match the list name + data[field] = list_name + + try: + with open(fpath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + fixed += 1 print( - f"\n⚠️ List descriptor missing required fields: {rel}\n" - f" Missing: {', '.join(missing)}\n" - f" Add these fields to {fname} — values should match the list name.\n" - f" Example: \"display_name\": \"{data.get('name') or os.path.splitext(fname)[0]}\"" + f"AUTO-FIXED list descriptor: {rel}\n" + f" Added: {', '.join(f'{k}={data[k]!r}' for k in missing)}" ) + except Exception as e: + print(f"ERROR writing {rel}: {e}") - return issues + return fixed # --- Parsing Error Fixer (JSON) --------------------------------------------- @@ -604,6 +638,11 @@ def run_demisto_format(target_path: str, dry_run: bool = False): type: python). Running format on these rewrites the script block and can corrupt indentation, breaking the Python. A manual fix instruction is printed instead. + + DOCKER GUARD: Sets DEMISTO_SDK_SKIP_CONTENT_GRAPH=1 so the SDK does not + attempt to start Neo4j via Docker. If the output still contains a Docker + error (e.g., older SDK ignoring the env var), a clear actionable message is + printed rather than dumping a raw traceback. """ if not os.path.exists(target_path): return False, f"SKIP (missing): {target_path}" @@ -633,13 +672,37 @@ def run_demisto_format(target_path: str, dry_run: bool = False): env = os.environ.copy() env.setdefault("DEMISTO_SDK_IGNORE_CONTENT_WARNING", "1") + # Prevent the SDK from starting Neo4j via Docker during format. + # Without Docker running this causes a hard crash with a long traceback. + env["DEMISTO_SDK_SKIP_CONTENT_GRAPH"] = "1" try: res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) + output = res.stdout or "" + + # Check returncode first — a zero exit is always success regardless of log noise. + # (neo4j_service / docker strings can appear in verbose output even on success.) if res.returncode == 0: - tail = res.stdout.strip().splitlines()[-1] if res.stdout else "format completed" + tail = output.strip().splitlines()[-1] if output.strip() else "format completed" return True, f"FORMAT OK: {target_path} ({tail})" - return False, f"FORMAT FAILED ({res.returncode}): {target_path}\n{res.stdout}" + + # Non-zero exit — now check whether Docker/Neo4j is the root cause. + if DOCKER_ERROR_RE.search(output): + return False, ( + f"\n" + f"╔══════════════════════════════════════════════════════════════════╗\n" + f"║ FORMAT SKIPPED — Docker is not running ║\n" + f"╚══════════════════════════════════════════════════════════════════╝\n" + f" File: {target_path}\n" + f"\n" + f" demisto-sdk format requires Docker to start its Neo4j content\n" + f" graph. Docker Desktop is not running on this machine.\n" + f"\n" + f" To fix: start Docker Desktop, then re-run pack_prep.py.\n" + f" The BA102 error for this file will be addressed on the next run.\n" + ) + + return False, f"FORMAT FAILED ({res.returncode}): {target_path}\n{output}" except FileNotFoundError: return False, "ERROR: `demisto-sdk` not found in PATH. Install it or add to PATH." except Exception as e: @@ -670,17 +733,16 @@ def main(): pydantic_count = preflight_pydantic_errors(full_text) # ── Pre-flight 2: walk Lists/ descriptors for missing required fields ──── - # Gives specific file paths so the user knows exactly what to fix. - list_issue_count = preflight_list_descriptors(repo_root) + # Now AUTO-FIXES files in place; returns count of files fixed. + list_fixed_count = preflight_list_descriptors(repo_root) - if pydantic_count == 0 and list_issue_count == 0: + if pydantic_count == 0 and list_fixed_count == 0: pass # no pre-flight issues — proceed silently to per-line fixes else: print( f"\n{'─' * 68}\n" - f"Pre-flight found {pydantic_count} pydantic error block(s) and " - f"{list_issue_count} list descriptor issue(s).\n" - f"Fix these manually before re-running the SDK.\n" + f"Pre-flight: {pydantic_count} pydantic block(s) require manual fix. " + f"{list_fixed_count} list descriptor(s) auto-fixed.\n" f"{'─' * 68}\n" )