TaegisXDRv2

For integration with the Secureworks Taegis XDR platform.

Data Enrichment & Threat Intelligence · Secureworks

Details

IDTaegisXDRv2
ProviderSophos
CategoryData Enrichment & Threat Intelligence
From Version6.2.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Configure Taegis XDR in Cortex

Parameter Description Required
Taegis Environment The environment to utilize True
Client ID Client ID as described in the Taegis Documentation True
Client Secret Client Secret as described in the Taegis Documentation True
Use system proxy settings Defines whether the system proxy is used or not False
Fetch Incident Type The type of incident to fetch from Taegis (Alerts or Investigations) True
Include Assets in Fetch When using the Investigations fetch type, should assets be included? This can cause API failures or latency and should only be enabled if necessary False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

taegis-add-evidence-to-investigation

Base Command

!taegis-add-evidence-to-investigation

Inputs

Argument Name Description Required
id The investigation id to update True
alerts A list of alert IDs to add to an investigation False
events A list of event IDs to add to an investigation False
alert_query A Taegis CQL query for alerts to add to the investigation False

At least one of the inputs alerts, events, or alert_query MUST be defined

Command Example

`!taegis-add-evidence-to-investigation` id=c207ca4c-8a78-4408-a056-49f05d6eb77d alerts="alert://priv:crowdstrike:11772:1677742145475:07e2d9cc-0a04-55ec-890a-97f39d63698e"

Context Example

{
    "TaegisXDR": {
        "InvestigationEvidenceUpdate": {
            "investigationId": "c207ca4c-8a78-4408-a056-49f05d6eb77d"
        }
    }
}

taegis-archive-investigation

Base Command

!taegis-archive-investigation

Inputs

Argument Name Description Required
id The investigation id to archive True

Command Example

!taegis-archive-investigation id=c207ca4c-8a78-4408-a056-49f05d6eb77d

Context Example

{
    "TaegisXDR": {
        "ArchivedInvestigation": {
            "id": "c207ca4c-8a78-4408-a056-49f05d6eb77d"
        }
    }
}

taegis-create-comment

Base Command

!taegis-create-comment

Inputs

Argument Name Description Required
comment The comment string to add to the investigation True
id The investigation ID to add the comment to True

Command Example

!taegis-create-comment comment="This is a test comment" id="219da0ee-8642-4363-827c-8a6fbd479082"

Context Example

{
    "TaegisXDR": {
        "CommentCreate": {
            "id": "593fa115-abad-4a52-9fc4-2ec403a8a1e4"
        }
    }
}

taegis-create-investigation

Base Command

!taegis-create-investigation

Inputs

Argument Name Description Required
title The subject or description of the investigation True
priority The priority for the investigiation [Default: 3] False
status The status for the investigation [Default: OPEN] False
alerts A list of alert IDs to add to the investigation [Default: []] False
keyFindings The Key Findings for the investigation False
type The investigation type [Default: SECURITY_INVESTIGATION] False
assigneeId The assignee for the investigation [Default: @secureworks] False
serviceDeskId A 3rd party ticket number for tracking purposes False
serviceDeskType The type of 3rd party ticket number False
tags A list of tags to add to the investigation [Default: []] False

Command Example

!taegis-create-investigation priority=1 title="XSOAR Created Investigation"

Context Example

{
    "TaegisXDR": {
        "Investigation": {
            "id": "593fa115-abad-4a52-9fc4-2ec403a8a1e4"
        }
    }
}

taegis-create-sharelink

Base Command

!taegis-create-sharelink

Inputs

Argument Name Description Required
id The ID of the Taegis element to create a sharelink to True
type The type of Taegis element to create a sharelink with True

Command Example

!taegis-create-sharelink type=investigationId id=219da0ee-8642-4363-827c-8a6fbd479082

Context Example

{
    "TaegisXDR": {
        "ShareLink": {
            "id": "593fa115-abad-4a52-9fc4-2ec403a8a1e4",
            "url": "https://ctpx.secureworks.com/share/593fa115-abad-4a52-9fc4-2ec403a8a1e4"
        }
    }
}

taegis-execute-playbook

Base Command

!taegis-execute-playbook

Inputs

Argument Name Description Required
id Playbook instance ID to execute True
inputs JSON object of inputs to pass into the playbook execution False

Command Example

!taegis-execute-playbook id=UGxheWJvb2tJbnN0YW5jZTphZDNmNzBlZi1mN2U0LTQ0OWYtODJiMi1hYWQwMjQzZTA2NTg=
!taegis-execute-playbook id=UGxheWJvb2tJbnN0YW5jZTphZDNmNzBlZi1mN2U0LTQ0OWYtODJiMi1hYWQwMjQzZTA2NTg= inputs=`{'myvar': 'myval'}`

Context Example

{
    "id": "UGxheWJvb2tFeGVjdXRpb246NGYwZDZiNGQtNWNiZS00NDkxLTg3YzYtMDZkNjkxYzMwMTg4"
}

taegis-fetch-alerts

Base Command

!taegis-fetch-alerts

Input

Argument Name Description Default Required
ids A list of alerts by IDs 936c1cc1-db8f-430c-837c-1c914fcca35a False
limit Number of results to when ids is not defined 10 False
offset The result to start from when ids is not defined 0 False
cql_query The query to utilize when searching for Alerts from alert severity >= 0.6 and status='OPEN' False

Command Examples

!taegis-fetch-alerts ids=`["6594e97f-a898-5b28-82b2-ea03293cdaa1"]`

Context Example

{
    "TaegisXDR": {
        "Alerts": [
            {
                "id": "c4f33b53-eaba-47ac-8272-199af0f7935b",
                "metadata": {
                    "title": "Test Alert",
                    "description": "This is a test alert",
                    "severity": 0.5,
                },
                "url": "https://ctpx.secureworks.com/alerts/c4f33b53-eaba-47ac-8272-199af0f7935b"
            }
        ]
    }
}

taegis-fetch-assets

Base Command

!taegis-fetch-assets

Input

Argument Name Description Default Required
page   0 False
page_size   10 False
endpoint_type     False
host_id ID of the asset to fetch e43b545a-580a-4047-b489-4338c1cc4ba1 False
hostname     False
investigation_id     False
ip_address     False
mac_address     False
os_family     False
os_version     False
sensor_version     False
username     False

Command Examples

!taegis-fetch-assets
!taegis-fetch-assets page=1 page_size=5
!taegis-fetch-assets hostname=MyHostname01
!taegis-fetch-assets host_id=e43b545a-580a-4047-b489-4338c1cc4ba1

Context Example

{
    "TaegisXDR": {
        "Assets": [
            {
              "id": "",
              "ingestTime": "",
              "createdAt": "",
              "updatedAt": "",
              "deletedAt": "",
              "biosSerial": "",
              "firstDiskSerial": "",
              "systemVolumeSerial": "",
              "sensorVersion": "",
              "endpointPlatform": "",
              "hostnames": [{"id": ", "hostname": ""],
              "architecture": "",
              "osFamily": "",
              "osVersion": "",
              "osDistributor": "",
              "osRelease": "",
              "systemType": "",
              "osCodename": "",
              "kernelRelease": "",
              "kernelVersion": "",
              "tags": [ "key": "", "tag": ""],
              "endpointType": "",
              "hostId": "",
              "sensorId": "",
            }
        ]
    }
}

taegis-fetch-comment

Base Command

!taegis-fetch-comment

Inputs

Argument Name Description Required
id The ID of the comment to fetch True

Command Example

!taegis-fetch-comment id=ff9ca818-4749-4ccb-883a-2ccc6f6c9e0f

Context Example

{
    "TaegisXDR": {
        "Comment": {
            "author_user": {
                "email_normalized": "myuser@email.com",
                "given_name": "John",
                "family_name": "Smith",
                "id": "auth0|000000000000000000000001",
            },
            "id": "ff9ca818-4749-4ccb-883a-2ccc6f6c9e0f",
            "comment": "This is a comment in an investigation",
            "created_at": "2022-01-01T13:04:57.17234Z",
            "deleted_at": None,
            "modified_at": None,
            "parent_id": "c2e09554-833e-41a1-bc9d-8160aec0d70d",
            "parent_type": "investigation",
        }
    }
}

taegis-fetch-comments

Base Command

!taegis-create-comments

Inputs

Argument Name Description Required
id The investigation ID to fetch comments for True
page Search page number [Default: 0] False
page_size Number of results per page [Default: 10] False
order_direction The order direction [Default: DESCENDING] False

Command Example

!taegis-fetch-comments id=c2e09554-833e-41a1-bc9d-8160aec0d70d

Context Example

{
    "TaegisXDR": {
        "Comments": [
            {
                "author_user": {
                    "email_normalized": "myuser@email.com",
                    "given_name": "John",
                    "family_name": "Smith",
                    "id": "auth0|000000000000000000000001",
                },
                "id": "ff9ca818-4749-4ccb-883a-2ccc6f6c9e0f",
                "comment": "This is a comment in an investigation",
                "created_at": "2022-01-01T13:04:57.17234Z",
                "deleted_at": None,
                "modified_at": None,
                "parent_id": "c2e09554-833e-41a1-bc9d-8160aec0d70d",
                "parent_type": "investigation",
            },
            {
                "author_user": {
                    "email_normalized": "myuser@email.com",
                    "given_name": "John",
                    "family_name": "Smith",
                    "id": "auth0|000000000000000000000001",
                },
                "id": "ff9ca818-4749-4ccb-883a-2ccc6f6c1234",
                "comment": "This is another comment",
                "created_at": "2022-01-02T13:04:57.17234Z",
                "deleted_at": None,
                "modified_at": None,
                "parent_id": "c2e09554-833e-41a1-bc9d-8160aec0d70d",
                "parent_type": "investigation",
            }
        ]
    }
}

taegis-fetch-endpoint

Base Command

!taegis-fetch-endpoint

Inputs

Argument Name Description Required
id Endpoint ID to fetch True

Command Example

!taegis-fetch-endpoint id=ff9ca818-4749-4ccb-883a-2ccc6f6c9e0f

Context Example

{
    "TaegisXDR": {
        "assetEndpointInfo": {
            "hostId": "",
            "hostName": "",
            "actualIsolationStatus": "",
            "allowedDomain": "",
            "desiredIsolationStatus": "",
            "firstConnectTime": "",
            "moduleHealth": {
                "enabled": ""
                "lastRunningTime": "",
                "moduleDisplayName": "",
            }
            "lastConnectAddress": "",
            "lastConnectTime": "",
            "sensorVersion": ""
        }
    }
}

taegis-fetch-investigation

Base Command

!taegis-fetch-investigation

Inputs

Argument Name Description Required
id Investigation ID to lookup False
query If not using ID, the query to utilize when searching investigations [Default: deleted_at is null] False
page Search page number [Default: 0] False
page_size Number of results per page [Default: 10] False
order_by The field to order results by [Default: created_at] False
order_direction The order direction [Default: DESCENDING] False

Command Example

!taegis-fetch-investigation id=936c1cc1-db8f-430c-837c-1c914fcca35a

Context Example

{
    "TaegisXDR": {
        "Investigations": [
            {
                "archived_at": None,
                "created_at": "2022-02-02T13:53:35Z",
                "description": "Test Investigation",
                "id": "c2e09554-833e-41a1-bc9d-8160aec0d70d",
                "key_findings": "",
                "priority": 2,
                "service_desk_id": "",
                "service_desk_type": "",
                "status": "Open",
                "alerts2": [],
                "url": "https://ctpx.secureworks.com/investigations/c2e09554-833e-41a1-bc9d-8160aec0d70d",
            }
        ]
    }
}

taegis-fetch-investigation-alerts

Base Command

!taegis-fetch-investigation-alerts

Inputs

Argument Name Description Required
id Investigation ID to lookup True
page Search page number [Default: 0] False
page_size Number of results per page [Default: 10] False

Command Example

!taegis-fetch-investigation-alerts id=936c1cc1-db8f-430c-837c-1c914fcca35a

Context Example

{
    "TaegisXDR": {
        "InvestigationAlerts": [
            {
                "id": "c4f33b53-eaba-47ac-8272-199af0f7935b",
                "description": "Test Alert",
                "message": "This is a test alert",
                "severity": 0.5,
            }
        ]
    }
}

taegis-fetch-playbook-execution

Base Command

!taegis-fetch-playbook-execution

Inputs

Argument Name Description Required
id Playbook execution ID to fetch True

Command Example

!taegis-fetch-playbook-execution id=UGxheWJvb2tFeGVjdXRpb246NGYwZDZiNGQtNWNiZS00NDkxLTg3YzYtMDZkNjkxYzMwMTg4

Context Example

{
    "TaegisXDR": {
        "PlaybookExecution": {
            "createdAt": "2022-01-01T13:51:24Z",
            "executionTime": 1442,
            "id": "UGxheWJvb2tFeGVjdXRpb246NGYwZDZiNGQtNWNiZS00NDkxLTg3YzYtMDZkNjkxYzMwMTg4",
            "inputs": {
                "alert": {
                    "message": "Test Alert",
                }
            },
            "instance": {
                "name": "Test Alert Instance",
                "playbook": {
                    "name": "Taegis.PagerDutyAlertEvent"
                }
            },
            "outputs": "d6b65662-c1da-4109-8553-c5664918c952",
            "state": "Completed",
            "updatedAt": "2022-01-01T13:51:31Z"
        }
    }
}

taegis-fetch-users

Base Command

!taegis-fetch-users

Inputs

Argument Name Description Required
id The id of the user, in auth0 format False
email The email of the user False
status The users to find based on status False
page   False
page_size   False

Command Example

!taegis-fetch-users id="auth0|123456"

Context Example

{
    "TaegisXDR": {
        "Users": [
            {
                "email": "myuser@email.com",
                "family_name": "Smith",
                "given_name": "John",
                "status": "Registered",
                "user_id": "auth0|123456"
            }
        ]
    }
}

taegis-isolate-asset

Base Command

!taegis-isolate-asset

Input

Argument Name Description Default Required
id ID of the asset to isolate e43b545a-580a-4047-b489-4338c1cc4ba1 True
reason The reason for the isolation See ticket 12345 True

Command Examples

!taegis-isolate-asset id="e43b545a-580a-4047-b489-4338c1cc4ba1" reason="See ticket 12345"

Context Example

{
    "TaegisXDR": {
        "AssetIsolation": {
            "id": "e43b545a-580a-4047-b489-4338c1cc4ba1"
        }
    }
}

taegis-fetch-events

Beta Command: This is a beta command, which lets you implement and test pre-release software. Since the command is beta, it might contain bugs. Updates to the command during the beta phase might include non-backward compatible features. We appreciate your feedback on the quality and usability of the command to help us identify issues, fix them, and continually improve.

Base Command

taegis-fetch-events

Input

Argument Name Description Required
ids A comma-separated list of event IDs to return. False
cql_query The Taegis CQL query string to use for searching events (e.g. FROM process EARLIEST=-1d \| head 10). If not defined, defaults to FROM * EARLIEST=-1m \| head 50. False
limit The maximum number of events to return. For standard CQL searches, the limit is passed as a GraphQL variable. For user-provided queries, embed the limit directly in the CQL string (e.g., \| head 100). Default is 50. False
offset The number of events to skip before returning results. Default is 0. False
next The pagination cursor token returned from a previous taegis-fetch-events call. Use this to retrieve the next page of results. False
fields The fields to return from the query. False
tenant_id The tenant to run against if using an MSP. If no tenant is provided, the tenant of the generated credentials is used. False

CQL Query Time Field Reference

Scenario Use This Field Why?
Incident Reconstruction event_time You need to see the exact sequence of the attacker’s steps.
Real-time Monitoring EARLIEST=-1m You want to see everything that hits the platform in the last 60 seconds.
Compliance/Audit ingest_time You need to prove when Secureworks actually received the record.
Offline Host Sync ingest_time You want to find data from a laptop that was just turned back on after a weekend.

Command example

!taegis-fetch-events
!taegis-fetch-events cql_query="FROM process EARLIEST=-1d | head 10"
!taegis-fetch-events cql_query="FROM dnsquery WHERE query_name MATCHES ('*.xyz', '*.top') EARLIEST=-24h" limit=100
!taegis-fetch-events ids="event-12345-67890,event-12345-67891"
!taegis-fetch-events next="eyJvZmZzZXQiOiAxMH0="

Context Example

[
    {
        "TaegisXDR": {
            "Events": [
                {
                    "id": "event-12345-67890",
                    "metadata": {
                        "event_type": "process",
                        "event_time": "2024-05-20T14:30:05.123Z",
                        "tenant_id": "999-000-111",
                        "sensor_id": "win-endpoint-01"
                    },
                    "parent_process_id": "456",
                    "image_path": "C:\\Windows\\System32\\cmd.exe",
                    "commandline": "cmd.exe /c \"whoami\"",
                    "username": "admin_user",
                    "next": "CursorToken_Batch01_Seq99"
                },
                {
                    "id": "event-12345-67891",
                    "metadata": {
                        "event_type": "netflow",
                        "event_time": "2024-05-20T14:30:10.456Z",
                        "tenant_id": "999-000-111",
                        "sensor_id": "fw-edge-02"
                    },
                    "source_ip": "1.1.1.1",
                    "destination_ip": "8.8.8.8",
                    "destination_port": 53,
                    "protocol": "UDP",
                    "next": "CursorToken_Batch01_Seq99"
                }
            ]
        }
    }
]

Context Output

Path Type Description
TaegisXDR.Events.id String The unique identifier of the event.
TaegisXDR.Events.metadata.event_type String The type of event (e.g., process, netflow, dnsquery).
TaegisXDR.Events.metadata.event_time String The timestamp when the event occurred.
TaegisXDR.Events.metadata.tenant_id String The tenant ID associated with the event.
TaegisXDR.Events.metadata.sensor_id String The sensor ID that generated the event.
TaegisXDR.Events.parent_process_id String The parent process ID (process events).
TaegisXDR.Events.image_path String The image/executable path (process events).
TaegisXDR.Events.commandline String The command line string (process events).
TaegisXDR.Events.username String The username associated with the event.
TaegisXDR.Events.source_ip String The source IP address (netflow events).
TaegisXDR.Events.destination_ip String The destination IP address (netflow events).
TaegisXDR.Events.destination_port Number The destination port number (netflow events).
TaegisXDR.Events.protocol String The network protocol (netflow events).
TaegisXDR.Events.next String Pagination cursor token for retrieving the next page of results.

taegis-update-alert-status

Base Command

!taegis-update-alert-status

Input

Argument Name Description Default Required
ids A comma-separated list of alerts by IDs alert://priv:crowdstrike:11772:1666269058114:59284e28-4ec8-542b-a4a1-452c3688bc1a True
status The status to update the alert(s) with FALSE_POSITIVE True
reason A comment/reason for the alert status update See ticket 13245 False
Permitted Status Values
  • FALSE_POSITIVE
  • NOT_ACTIONABLE
  • OPEN
  • TRUE_POSITIVE_BENIGN
  • TRUE_POSITIVE_MALICIOUS
  • OTHER

Command Examples

!taegis-update-alert-status ids="alert://priv:crowdstrike:11772:1677742145475:07e2d9cc-0a04-55ec-890a-97f39d63698e" status=NOT_ACTIONABLE reason="Test Reason"

Context Example

{
    "TaegisXDR": {
        "AlertStatusUpdate": {
            "reason": "feedback updates successfully applied",
            "resolution_status": "SUCCESS"
        }
    }
}

taegis-update-comment

Base Command

!taegis-update-comment

Inputs

Argument Name Description Required
comment The comment string to add to the investigation True
id The comment ID to update True

Command Example

!taegis-update-comment id="ff9ca818-4749-4ccb-883a-2ccc6f6c9e0f" comment="Newly updated comment"

Context Example

{
    "TaegisXDR": {
        "CommentUpdate": {
            "id": "593fa115-abad-4a52-9fc4-2ec403a8a1e4"
        }
    }
}

taegis-update-investigation

Base Command

!taegis-update-investigation

Inputs

Argument Name Description Required
id Investigation ID to update True
title The title of the investigation False
keyFindings The investigation Key Findings False
prioirity The priority of the Investigation (1-5) False
status The current status of the Investigation False
assigneeId The id of a user to assign, in auth0|12345 format False
serviceDeskId A 3rd party ticket number for tracking purposes False
serviceDeskType The type of 3rd party ticket number False
tags A list of tags to add to the investigation [Default: []] False

Note: At least 1 of the above inputs (in addition to id) must be defined

Permitted Status Values
  • Active
  • Awaiting Action
  • Closed: Authorized Activity
  • Closed: Confirmed Security Incident
  • Closed: False Positive Alert
  • Closed: Inconclusive
  • Closed: Informational
  • Closed: Not Vulnerable
  • Closed: Threat Mitigated
  • Open
  • Suspended

Command Example

!taegis-update-investigation id="936c1cc1-db8f-430c-837c-1c914fcca35a" priority=3 status="OPEN"

Context Example

{
    "TaegisXDR": {
        "InvestigationUpdate": {
            "id": "c2e09554-833e-41a1-bc9d-8160aec0d70d"
        }
    }
}

taegis-unarchive-investigation

Base Command

!taegis-unarchive-investigation

Inputs

Argument Name Description Required
id The investigation id to unarchive True

Command Example

!taegis-unarchive-investigation id=c207ca4c-8a78-4408-a056-49f05d6eb77d

Context Example

{
    "TaegisXDR": {
        "UnarchivedInvestigation": {
            "id": "c207ca4c-8a78-4408-a056-49f05d6eb77d"
        }
    }
}

Configuration parameters

  • environment — Taegis Environment (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • client_id — Client ID (required)
  • client_secret — Client Secret (required)
  • isFetch — Fetch incidents
  • max_fetch — Maximum number of incidents per fetch
  • incidentFetchInterval — Incidents Fetch Interval
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • incidentType — Incident type
  • include_assets — Include Assets in Fetch
  • fetch_type — Fetch Incident Type

Commands (21)

  • taegis-add-evidence-to-investigation

    Add alerts and events to an existing investigation.

  • taegis-archive-investigation

    Archive an investigation.

  • taegis-create-comment

    Create a comment on an investigation.

  • taegis-create-investigation

    Create an Investigation within Taegis.

  • taegis-create-sharelink

    Creates a ShareLink to a Taegis element, such as an alert or investigation.

  • taegis-execute-playbook

    Executes a Taegis playbook instance.

  • taegis-fetch-alerts

    List Taegis alerts by ID.

  • taegis-fetch-assets

    Fetch assets based on search criteria.

  • taegis-fetch-comment

    Fetch comment by comment ID.

  • taegis-fetch-comments

    Fetch comments by Investigation ID.

  • taegis-fetch-endpoint

    Fetch endpoint information.

  • taegis-fetch-events

    This is a beta command, which lets you implement and test pre-release software. Since the command is beta, it might contain bugs. Updates to the command during the beta phase might include non-backward compatible features. We appreciate your feedback on the quality and usability of the command to help us identify issues, fix them, and continually improve. Fetches Taegis events using a CQL query string or a list of event IDs. Supports pagination via the next cursor token returned in results. If no cql_query is provided, defaults to "FROM * EARLIEST=-1m | head 50".

  • taegis-fetch-investigation

    Fetch all investigations or a specific investigation.

  • taegis-fetch-investigation-alerts

    Fetch Alert IDs related to a specific investigation.

  • taegis-fetch-playbook-execution

    Fetch the results of a Taegis playbook instance execution.

  • taegis-fetch-users

    Fetch a user by ID or email address.

  • taegis-isolate-asset

    Isolate a specific asset.

  • taegis-unarchive-investigation

    Unarchive an investigation.

  • taegis-update-alert-status

    Update the status of an alert.

  • taegis-update-comment

    Update an existing comment.

  • taegis-update-investigation

    Update an existing investigation.

from typing import Any

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

""" CONSTANTS """


AUTH_ENDPOINT = "/auth/api/v2/auth/token"
GRAPHQL_ENDPOINT = "/graphql"

ENV_URLS = {
    "us1 (charlie)": {"api": "https://api.ctpx.secureworks.com", "xdr": "https://ctpx.secureworks.com"},
    "us2 (delta)": {"api": "https://api.delta.taegis.secureworks.com", "xdr": "https://delta.taegis.secureworks.com"},
    "us3 (foxtrot)": {"api": "https://api.foxtrot.taegis.secureworks.com", "xdr": "https://foxtrot.taegis.secureworks.com"},
    "eu (echo)": {"api": "https://api.echo.taegis.secureworks.com", "xdr": "https://echo.taegis.secureworks.com"},
}

ALERT_STATUSES = {
    "FALSE_POSITIVE",
    "NOT_ACTIONABLE",
    "OPEN",
    "TRUE_POSITIVE_BENIGN",
    "TRUE_POSITIVE_MALICIOUS",
    "OTHER",
    "SUPPRESSED",
}
ASSET_SEARCH_FIELDS = (
    "endpoint_type",
    "host_id",
    "hostname",
    "investigation_id",
    "ip_address",
    "mac_address",
    "os_family",
    "os_version",
    "sensor_version",
    "username",
)
COMMENT_TYPES = {
    "investigation",
}
INVESTIGATION_STATUSES = {
    "OPEN",
    "ACTIVE",
    "AWAITING_ACTION",
    "SUSPENDED",
    "CLOSED_AUTHORIZED_ACTIVITY",
    "CLOSED_CONFIRMED_SECURITY_INCIDENT",
    "CLOSED_FALSE_POSITIVE_ALERT",
    "CLOSED_INCONCLUSIVE",
    "CLOSED_INFORMATIONAL",
    "CLOSED_NOT_VULNERABLE",
    "CLOSED_THREAT_MITIGATED",
}
INVESTIGATION_TYPES = {
    "SECURITY_INVESTIGATION",
    "INCIDENT_RESPONSE",
    "THREAT_HUNT",
    "MANAGED_XDR_THREAT_HUNT",
    "CTU_THREAT_HUNT",
    "MANAGED_XDR_ELITE_THREAT_HUNT",
    "SECUREWORKS_INCIDENT_RESPONSE",
}
INVESTIGATION_UPDATE_FIELDS = {
    "keyFindings",
    "priority",
    "status",
    "assigneeId",
    "title",
    "type",
    "serviceDeskId",
    "serviceDeskType",
    "tags",
}
SHARELINK_TYPES = {
    "alertId",
    "connectorId",
    "connectionId",
    "endpointDetails",
    "eventId",
    "investigationId",
    "queryId",
    "playbookTemplateId",
    "playbookInstanceId",
    "playbookExecutionId",
}
DEFAULT_EVENT_FIELDS = """
    id
    metadata {
        event_type
        event_time
        tenant_id
        sensor_id
    }
    parent_process_id
    image_path
    commandline
    username
    source_ip
    destination_ip
    destination_port
    protocol
    next
"""

DEFAULT_FIRST_FETCH_INTERVAL = "1 day"


""" CLIENT """


class Client(BaseClient):
    """
    Secureworks Taegis XDR Client class for implementing API logic with Taegis
    """

    _auth_header = {"access_token": "None"}

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        base_url: str,
        proxy: bool = False,
        verify: bool = True,
        tenant_id: str = "",
    ) -> None:
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.base_url = base_url
        self._client_id = client_id
        self._client_secret = client_secret
        self.verify = verify
        self.tenant_id = tenant_id

    def auth(self) -> None:
        """Authenticate to the Taegis API using client_id and client_secret

        See the documentation for obtaining the client ID and secret: https://docs.ctpx.secureworks.com/apis/api_authenticate/
        """
        response = self._http_request(
            "POST",
            AUTH_ENDPOINT,
            headers={"Content-Type": "application/json"},
            json_data={"grant_type": "client_credentials"},
            auth=(self._client_id, self._client_secret),
        )

        token = response.get("access_token", None)
        self._auth_header = {
            "Authorization": f"Bearer {token}",
            "x-tenant-context": self.tenant_id,
        }

    def graphql_run(self, query: str, variables: dict[str, Any] = None):
        """Perform a GraphQL query

        :type query: ``str``
        :param query: The GraphQL query

        :type variables: ``Dict[str, Any]``
        :param variables: The variables to utilize with the query
        """
        json_data: dict[str, Any] = {"query": query}

        if variables:
            json_data["variables"] = variables

        response = self._http_request(
            method="POST",
            url_suffix=GRAPHQL_ENDPOINT,
            json_data=json_data,
            headers=self._auth_header,
        )
        return response

    def test(self) -> dict[str, Any]:
        """
        Get the current API/asset version for testing auth and connectivity
        """
        response = self._http_request(
            method="GET",
            url_suffix="/assets/version",
            headers=self._auth_header,
        )
        return response


""" COMMANDS """


def add_evidence_to_investigation_command(client: Client, env: str, args=None):
    """
    Add events or alert evidence to an investigation
    """

    if not args.get("id"):
        raise ValueError("Cannot add evidence to investigation, id cannot be empty")
    if not args.get("alerts") and not args.get("events") and not args.get("alert_query"):
        raise ValueError("Cannot add evidence to investigation. alerts, events, or alert_query must be defined")

    variables: dict = {
        "input": {
            "investigationId": args.get("id"),
            "alerts": argToList(args.get("alerts")),
            "events": argToList(args.get("events")),
            "alertsSearchQuery": args.get("alert_query", ""),
        }
    }

    fields: str = args.get("fields") or "investigationId"

    query = f"""
    mutation addEvidenceToInvestigation($input: AddEvidenceToInvestigationInput!) {{
      addEvidenceToInvestigation(input: $input) {{
        {fields}
      }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)
    try:
        investigation = result["data"]["addEvidenceToInvestigation"]
        investigation["url"] = generate_id_url(env, "investigations", investigation["investigationId"])
    except (KeyError, TypeError):
        raise ValueError(f"Failed to create investigation: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.InvestigationEvidenceUpdate",
        outputs_key_field="investigationId",
        outputs=investigation,
        readable_output=tableToMarkdown(
            "Taegis Investigation Evidence",
            investigation,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def create_comment_command(client: Client, env: str, args=None):
    if not args.get("comment"):
        raise ValueError("Cannot create comment, comment cannot be empty")

    if not args.get("id"):
        raise ValueError("Cannot create comment, id cannot be empty")

    fields: str = args.get("fields") or "id"

    query = f"""
    mutation addCommentToInvestigation($input: AddCommentToInvestigationInput!) {{
        addCommentToInvestigation(input: $input) {{
            {fields}
        }}
    }}
    """

    variables = {
        "input": {
            "comment": args.get("comment"),
            "investigationId": args.get("id"),
        }
    }

    result = client.graphql_run(query=query, variables=variables)

    try:
        comment = result["data"]["addCommentToInvestigation"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to create comment: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.CommentCreate",
        outputs_key_field="id",
        outputs=comment,
        readable_output=tableToMarkdown(
            "Taegis Comment",
            comment,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def create_investigation_command(client: Client, env: str, args=None):
    fields: str = args.get("fields") or "id shortId"
    query = f"""
    mutation ($input: CreateInvestigationInput!) {{
    createInvestigationV2(input: $input) {{
            {fields}
        }}
    }}
    """

    variables = {
        "input": {
            "title": args.get("title"),
            "priority": arg_to_number(args.get("priority", 3)),
            "status": args.get("status", "OPEN"),
            "alerts": argToList(args.get("alerts")),
            "keyFindings": args.get("key_findings", ""),
            "type": args.get("type", "SECURITY_INVESTIGATION"),
            "assigneeId": args.get("assignee_id", "@secureworks"),
            "tags": argToList(args.get("tags")),
        }
    }

    if not variables["input"]["assigneeId"].startswith("auth0") and variables["input"]["assigneeId"] != "@secureworks":
        raise ValueError("assigneeId MUST be in 'auth0|12345' format or '@secureworks'")
    if variables["input"]["priority"] and not 0 < variables["input"]["priority"] < 5:
        raise ValueError("Priority must be between 1-4")
    if variables["input"]["status"] not in INVESTIGATION_STATUSES:
        raise ValueError(
            f"The provided status, {variables['input']['status']}, is not valid for updating an investigation. "
            f"Supported Status Values: {INVESTIGATION_STATUSES}"
        )
    if variables["input"]["type"] not in INVESTIGATION_TYPES:
        raise ValueError(
            f"The provided type, {variables['input']['type']}, is not valid for updating an investigation. "
            f"Supported Type Values: {INVESTIGATION_TYPES}"
        )
    if not variables["input"]["title"]:
        raise ValueError("Title must be defined")

    result = client.graphql_run(query=query, variables=variables)

    try:
        investigation = result["data"]["createInvestigationV2"]
        investigation["url"] = generate_id_url(env, "investigations", investigation["id"])
    except (KeyError, TypeError):
        raise ValueError(f"Failed to create investigation: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.Investigation",
        outputs_key_field="id",
        outputs=investigation,
        readable_output=tableToMarkdown(
            "Taegis Investigation",
            investigation,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def create_sharelink_command(client: Client, env: str, args=None):
    """
    Create a ShareLink to an investigation or alert
    """

    if not args.get("id"):
        raise ValueError("Cannot create ShareLink, id cannot be empty")
    if not args.get("type"):
        raise ValueError("Cannot create ShareLink, type cannot be empty")
    if args["type"] not in SHARELINK_TYPES:
        raise ValueError(
            f"The provided ShareLink type, {args['type']}, is not valid for creating a ShareLink. "
            f"Supported Type Values: {SHARELINK_TYPES}"
        )

    variables: dict = {
        "sharelink": {
            "linkRef": args["id"],
            "linkType": args["type"],
        }
    }

    if args.get("tenant_id"):
        variables["tenant_id"] = args["tenant_id"]

    fields: str = args.get("fields") or "id createdTime"

    query = f"""
    mutation ($sharelink: ShareLinkCreateInput!) {{
        createShareLink (input: $sharelink) {{
            {fields}
        }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)
    try:
        link_result = result["data"]["createShareLink"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to create ShareLink: {result['errors'][0]['message']}")

    link_result.update({"url": generate_id_url(env, "share", link_result["id"])})

    results = CommandResults(
        outputs_prefix="TaegisXDR.ShareLink",
        outputs_key_field="id",
        outputs=link_result,
        readable_output=tableToMarkdown(
            "Taegis ShareLink",
            link_result,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def execute_playbook_command(client: Client, env: str, args=None):
    playbook_id = args.get("id")
    if not playbook_id:
        raise ValueError("Cannot execute playbook, missing playbook_id")

    fields: str = args.get("fields") or "id"
    query = f"""
    mutation executePlaybookInstance(
        $playbookInstanceId: ID!
        $parameters: JSONObject
    ) {{
        executePlaybookInstance(
            playbookInstanceId: $playbookInstanceId
            parameters: $parameters
        ) {{
            {fields}
        }}
    }}
    """

    playbook_inputs = args.get("inputs", {})

    variables = {
        "playbookInstanceId": playbook_id,
        "parameters": playbook_inputs,
    }

    result = client.graphql_run(query=query, variables=variables)

    if not result.get("data"):
        raise ValueError(f"Failed to execute playbook: {result['errors'][0]['message']}")

    execution = result["data"]["executePlaybookInstance"]
    execution["url"] = generate_id_url(env, "automations/playbook-executions", execution["id"])

    results = CommandResults(
        outputs_prefix="TaegisXDR.Execution",
        outputs_key_field="id",
        outputs=execution,
        readable_output=tableToMarkdown(
            "Taegis Playbook Execution",
            execution,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_alerts_command(client: Client, env: str, args=None):
    """
    Fetch a specific alert or a list of alerts based on a CQL Taegis query
    """
    variables: dict = {
        "cql_query": args.get("cql_query", "from alert severity >= 0.4 and status='OPEN'"),
        "limit": arg_to_number(args.get("limit", 10)),
        "offset": arg_to_number(args.get("offset", 0)),
        "ids": args.get("ids", []),  # ["alert://id1", "alert://id2"]
    }
    fields: str = (
        args.get("fields")
        or """
        status
        reason
        alerts {
            total_results
            list {
                id
                tenant_id
                status
                suppressed
                suppression_rules {
                  id
                  version
                }
                resolution_reason
                attack_technique_ids
                entities{
                  entities
                  relationships{
                    from_entity
                    relationship
                    to_entity
                  }
                }
                metadata {
                  engine {
                    name
                  }
                  creator {
                    detector {
                      version
                      detector_id
                    }
                    rule {
                      rule_id
                      version
                    }
                  }
                  title
                  description
                  confidence
                  severity
                  created_at {
                    seconds
                  }
                }
                investigation_ids {
                  id
                }
                event_ids {
                  id
                  event_data
                }
                sensor_types
            }
        }
    """
    )

    if args.get("ids"):
        field = "alertsServiceRetrieveAlertsById"
        query = f"""
        query alertsServiceRetrieveAlertsById($ids: [String!]) {{
            alertsServiceRetrieveAlertsById(
                in: {{
                    iDs: $ids
                }}
            ) {{
                {fields}
            }}
        }}
        """

        variables["ids"] = argToList(variables["ids"])
    else:
        field = "alertsServiceSearch"
        query = f"""
        query alertsServiceSearch($cql_query: String, $limit: Int, $offset: Int) {{
            alertsServiceSearch(
                in: {{
                    cql_query: $cql_query,
                    offset: $offset,
                    limit: $limit
                }}
            ) {{
                {fields}
            }}
        }}
        """

    result = client.graphql_run(query=query, variables=variables)
    alerts = result["data"][field]["alerts"]["list"]

    for alert in alerts:
        alert.update({"url": generate_id_url(env, "alerts", alert["id"])})

    results = CommandResults(
        outputs_prefix="TaegisXDR.Alerts",
        outputs_key_field="id",
        outputs=alerts,
        readable_output=tableToMarkdown(
            "Taegis Alerts",
            alerts,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_assets_command(client: Client, env: str, args=None):
    page = arg_to_number(args.get("page")) or 0
    page_size = arg_to_number(args.get("page_size")) or 10

    variables: dict[str, Any] = {
        "input": {},
        "pagination_input": {
            "limit": page_size,
            "offset": page_size * page,
        },
    }

    # Loop over allowed search fields and add valid search options to the query variables
    for field in ASSET_SEARCH_FIELDS:
        if args.get(field):
            variables["input"][field] = args.get(field).strip()

    fields: str = (
        args.get("fields")
        or """
        id
        ingestTime
        createdAt
        updatedAt
        deletedAt
        biosSerial
        firstDiskSerial
        systemVolumeSerial
        sensorVersion
        endpointPlatform
        architecture
        osFamily
        osVersion
        osDistributor
        osRelease
        systemType
        osCodename
        kernelRelease
        kernelVersion
        hostnames {
            id
            hostname
        },
        tags {
            key
            tag
        }
        endpointType
        hostId
        sensorId
        """
    )
    query = f"""
    query searchAssetsV2($input: SearchAssetsInput!, $pagination_input: SearchAssetsPaginationInput!) {{
        searchAssetsV2(input: $input, paginationInput: $pagination_input) {{
            assets {{
                {fields}
            }}
        }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)
    try:
        assets = result["data"]["searchAssetsV2"]["assets"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to fetch assets: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.Assets",
        outputs_key_field="id",
        outputs=assets,
        readable_output=tableToMarkdown(
            "Taegis Assets",
            assets,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def fetch_comment_command(client: Client, env: str, args=None):
    comment_id = args.get("id")
    if not comment_id:
        raise ValueError("Cannot fetch comment, missing comment_id")

    fields: str = (
        args.get("fields")
        or """
        author_user {
            id
            family_name
            given_name
            email_normalized
        }
        id
        comment
        modified_at
        deleted_at
        created_at
        parent_id
        parent_type
        """
    )

    query = f"""
    query comment ($comment_id: ID!) {{
        comment(comment_id: $comment_id) {{
            {fields}
        }}
    }}
    """

    variables = {"comment_id": comment_id}

    result = client.graphql_run(query=query, variables=variables)

    try:
        comment = result["data"]["comment"]
    except (KeyError, TypeError):
        raise ValueError("Could not locate comment by provided ID")

    results = CommandResults(
        outputs_prefix="TaegisXDR.Comment",
        outputs_key_field="id",
        outputs=comment,
        readable_output=tableToMarkdown(
            "Taegis Comment",
            comment,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def fetch_comments_command(client: Client, env: str, args=None):
    if not args.get("id"):
        raise ValueError("Cannot fetch comments, missing id")

    fields: str = (
        args.get("fields")
        or """
        author {
            id
            family_name
            given_name
            email_normalized
        }
        authorId
        id
        comment
        createdAt
        updatedAt
        """
    )

    query = f"""
    query commentsV2 ($arguments: CommentsV2Arguments!) {{
        commentsV2(arguments: $arguments) {{
            comments {{
                {fields}
            }}
        }}
    }}
    """

    variables = {
        "arguments": {
            "investigationId": args.get("id"),
            "page": arg_to_number(args.get("page", 0)),
            "perPage": arg_to_number(args.get("page_size", 10)),
            "orderBy": args.get("order_direction", "DESCENDING"),
        }
    }

    result = client.graphql_run(query=query, variables=variables)

    try:
        comments = result["data"]["commentsV2"]["comments"]
    except (KeyError, TypeError):
        comments = []

    results = CommandResults(
        outputs_prefix="TaegisXDR.Comments",
        outputs_key_field="id",
        outputs=comments,
        readable_output=tableToMarkdown(
            "Taegis Comments",
            comments,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def fetch_endpoint_command(client: Client, env: str, args=None):
    if not args.get("id"):
        raise ValueError("Cannot fetch endpoint information, missing id")

    variables: dict[str, Any] = {"id": args.get("id")}

    fields: str = (
        args.get("fields")
        or """
        hostId
        hostName
        actualIsolationStatus
        allowedDomain
        desiredIsolationStatus
        firstConnectTime
        moduleHealth {
            enabled
            lastRunningTime
            moduleDisplayName
        }
        lastConnectAddress
        lastConnectTime
        sensorVersion
        """
    )

    query = f"""
    query assetEndpointInfo($id: ID!) {{
      assetEndpointInfo(id: $id) {{
        {fields}
      }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)
    try:
        endpoint = result["data"]["assetEndpointInfo"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to fetch endpoint information: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.Endpoint",
        outputs_key_field="hostId",
        outputs=endpoint,
        readable_output=tableToMarkdown(
            "Taegis Endpoint",
            endpoint,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def fetch_incidents(
    client: Client,
    env: str,
    fetch_type: str = "investigations",
    max_fetch: int = 15,
    include_assets: bool = True,
    first_fetch_interval: str = DEFAULT_FIRST_FETCH_INTERVAL,
):
    """
    Fetch Taegis Investigations or Alerts for the use with "Fetch Incidents"
    """
    if not 0 < int(max_fetch) < 201:
        raise ValueError("Max Fetch must be between 1 and 200")

    if fetch_type not in ["alerts", "investigations"]:
        raise ValueError("Incident Type is invalid. Supported types: ['alerts', 'investigations']")

    last_run = demisto.getLastRun()
    demisto.debug(f"Last Fetch Incident Run: {last_run}")
    now = datetime.now()
    start_time = str(dateparser.parse(first_fetch_interval))  # Default start if first ever run
    if last_run and "start_time" in last_run:
        start_time = last_run.get("start_time")

    if fetch_type == "alerts":
        query = """
          query alertsServiceSearch($cql_query: String, $limit: Int) {
            alertsServiceSearch(
              in: {
                cql_query:$cql_query,
                limit:$limit
              }
            ) {
                  status
              reason
              alerts {
                list {
                  id
                  status
                  tenant_id
                  suppressed
                  resolution_reason
                  attack_technique_ids
                  entities{
                    entities
                    relationships{
                      from_entity
                      relationship
                      to_entity
                    }
                  }
                  metadata {
                    engine {
                      name
                    }
                    creator {
                      detector {
                        version
                        detector_id
                        detector_name
                      }
                      rule {
                        rule_id
                        version
                      }
                    }
                    title
                    description
                    confidence
                    severity
                    created_at {
                      seconds
                    }
                    began_at {
                      seconds
                    }
                    ended_at {
                      seconds
                    }
                  }
                  event_ids {
                      id
                      event_data
                  }
                  investigation_ids {
                    id
                  }
                  sensor_types
                }
                total_results
              }
            }
          }
        """

        variables = {
            "limit": arg_to_number(max_fetch),
            # We only support Medium, High, Critical
            "cql_query": f"from alert where severity >=0.4 AND earliest = '{start_time}'",
        }
    elif fetch_type == "investigations":
        asset_query = ""
        if include_assets:
            demisto.debug("include_assets=True, fetching assets with investigation")
            # Assets to be deprecated in the future
            asset_query = "assets {id hostnames {id hostname} tags {tag}} assetsEvidence {id assetId}"

        query = """
        query investigationsSearch(
            $page: Int,
            $perPage: Int,
            $orderByField: OrderFieldInput,
            $orderDirection: OrderDirectionInput,
            $query: String
        ) {
          investigationsSearch(
            page: $page,
            perPage: $perPage,
            orderByField: $orderByField,
            orderDirection: $orderDirection,
            query: $query
          ) {
            totalCount
            investigations {
                id
                tenant_id
                description
                key_findings
                assignee {
                    name
                    id
                    email
                }
                assignee_user {
                    family_name
                    given_name
                    id
                    email
                }
                alerts2 {
                    id
                    suppressed
                    status
                    priority {
                        value
                    }
                    entities {
                        entities
                    }
                    metadata {
                        title
                        description
                        created_at {
                            seconds
                        }
                        severity
                        confidence
                    }
                }
                created_by
                created_by_scwx
                service_desk_id
                service_desk_type
                latest_activity
                priority
                status
                created_at
                archived_at
                alertsEvidence {id alertId}
                tags
                %s
            }
          }
        }
        """ % (asset_query)  # noqa: UP031

        variables = {
            "orderByField": "created_at",
            "orderDirection": "asc",
            "page": 0,
            "perPage": arg_to_number(max_fetch),
            "query": f"status in ('Open', 'Active', 'Awaiting Action') AND earliest = '{start_time}'",
        }
    else:
        query = ""
        variables = {}
        demisto.debug(f"No condition was met -> {query=} {variables=}")

    result = client.graphql_run(query=query, variables=variables)
    if result.get("errors") and result["errors"]:
        raise DemistoException(f"Error when fetching incidents: {result['errors'][0]['message']}")

    try:
        results = (
            result["data"]["investigationsSearch"]["investigations"]
            if fetch_type == "investigations"
            else result["data"]["alertsServiceSearch"]["alerts"]["list"]
        )
    except (TypeError, KeyError):
        results = []

    incidents = []
    for incident in results:
        # createdAfter really means createdAtOrAfter so skip the duplicate
        created_date = (
            incident["created_at"]
            if fetch_type == "investigations"
            else datetime.fromtimestamp(int(incident["metadata"]["created_at"]["seconds"])).strftime("%Y-%m-%d %H:%M:%S.%f")
        )
        if start_time == created_date:
            continue

        # Skip archived, if necessary
        if fetch_type == "investigations" and incident["archived_at"]:
            demisto.debug(f"Skipping Archived Investigation: {incident['description']} ({incident['id']})")
            continue

        incident_name: str = incident["description"] if fetch_type == "investigations" else incident["metadata"]["title"]
        demisto.debug(f"Found New Incident: [{incident['id']}] {incident_name}")

        incident.update({"url": generate_id_url(env, fetch_type, incident["id"])})
        incidents.append(
            {
                "name": incident_name,
                "occured": created_date,
                "dbotMirrorId": incident["id"],
                "rawJSON": json.dumps(incident),
            }
        )

    demisto.debug(f"Located {len(incidents)} Incidents")

    last_run = str(now) if not incidents else incidents[-1]["occured"]
    demisto.debug(f"Setting New Last Run Time: {last_run}")
    demisto.setLastRun({"start_time": last_run})

    demisto.incidents(incidents)

    return incidents


def fetch_investigation_alerts_command(client: Client, env: str, args=None):
    investigation_id = args.get("id")
    page = arg_to_number(args.get("page", 0))
    page_size = arg_to_number(args.get("page_size", 10))
    if not investigation_id:
        raise ValueError("Cannot fetch investigation, missing investigation_id")

    fields: str = (
        args.get("fields")
        or """
        alerts {
            id
        }
        alerts2 {
            id
        }
        totalCount
        """
    )

    query = f"""
    query investigationAlerts($investigation_id: ID!, $page: Int, $perPage: Int) {{
        investigationAlerts(investigation_id: $investigation_id, page: $page, perPage: $perPage) {{
            {fields}
        }}
    }}
    """

    variables = {"page": page, "perPage": page_size, "investigation_id": investigation_id}
    result = client.graphql_run(query=query, variables=variables)

    try:
        alerts = result["data"]["investigationAlerts"]["alerts"]
    except (KeyError, TypeError):
        alerts = []

    for alert in alerts:
        alert.update({"url": generate_id_url(env, "alerts", alert["id"])})

    results = CommandResults(
        outputs_prefix="TaegisXDR.InvestigationAlerts",
        outputs_key_field="id",
        outputs=alerts,
        readable_output=tableToMarkdown(
            "Taegis Investigation Alerts",
            alerts,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_investigation_command(client: Client, env: str, args=None):
    fields: str = ""
    variables: Dict[str, Any] = {}
    if args.get("id"):
        # alerts, assets, and assignee to be deprecated in the future
        fields = (
            args.get("fields")
            or """
            id
            shortId
            title
            keyFindings
            alerts
            assets
            alertsEvidence {id alertId}
            assetsEvidence {id assetId}
            status
            assignee {
                id
                family_name
                given_name
            }
            priority
            type
            processingStatus {
                assets
                events
                alerts
            }
            archivedAt
            tags
            """
        )

        query = f"""
        query investigationV2($arguments: InvestigationV2Arguments!) {{
            investigationV2(arguments: $arguments) {{
                {fields}
            }}
        }}
        """

        variables = {"arguments": {"id": args.get("id")}}
        result = client.graphql_run(query=query, variables=variables)
    else:
        # assignee {} to be deprecated in the future
        fields = (
            args.get("fields")
            or """
            id
            tenant_id
            description
            key_findings
            alerts2 {
                id
                suppressed
                status
                priority {
                    value
                }
                metadata {
                    title
                    description
                    created_at {
                        seconds
                    }
                    severity
                    confidence
                }
            }
            genesis_alerts2 {
                id
                suppressed
                status
                priority {
                    value
                }
                metadata {
                    title
                    description
                    created_at {
                        seconds
                    }
                    severity
                    confidence
                }
            }
            assignee {
                name
                id
                email
            }
            assignee_user {
                family_name
                given_name
                email
                id
            }
            archived_at
            created_at
            updated_at
            service_desk_id
            service_desk_type
            latest_activity
            priority
            status
            type
            processing_status {
                assets
                events
                alerts
            }
            assets {
                id
                hostnames {
                    id
                    hostname
                }
                tags {
                    tag
                }
            }
            alertsEvidence {id alertId}
            assetsEvidence {id assetId}
            tags
            """
        )

        query = f"""
        query investigationsSearch(
            $page: Int,
            $perPage: Int,
            $orderByField: OrderFieldInput,
            $orderDirection: OrderDirectionInput,
            $query: String
        ) {{
            investigationsSearch(
                page: $page
                perPage: $perPage
                orderByField: $orderByField
                orderDirection: $orderDirection
                query: $query
            ) {{
                totalCount
                investigations {{
                    {fields}
                }}
            }}
        }}
        """
        variables = {
            "page": arg_to_number(args.get("page", 0)),
            "perPage": arg_to_number(args.get("page_size", 10)),
            "query": args.get("query", "deleted_at is null"),
            "orderByField": args.get("order_by", "created_at"),
            "orderDirection": args.get("order_direction", "desc"),
        }
        result = client.graphql_run(query=query, variables=variables)

    try:
        investigations = (
            [result["data"]["investigationV2"]] if args.get("id") else result["data"]["investigationsSearch"]["investigations"]
        )
    except (KeyError, TypeError):
        investigations = []

    # If no investigation found, no error status is returned but investigation will be null
    if len(investigations) == 1 and investigations[0] is None:
        investigations = []

    for investigation in investigations:
        investigation.update({"url": generate_id_url(env, "investigations", investigation["id"])})

    results = CommandResults(
        outputs_prefix="TaegisXDR.Investigations",
        outputs_key_field="id",
        outputs=investigations,
        readable_output=tableToMarkdown(
            "Taegis Investigations",
            investigations,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_playbook_execution_command(client: Client, env: str, args=None):
    execution_id = args.get("id")
    if not execution_id:
        raise ValueError("Cannot fetch playbook execution, missing execution id")

    fields: str = (
        args.get("fields")
        or """
        id
        state
        instance {
          name
          playbook {
              name
          }
        }
        inputs
        createdAt
        updatedAt
        executionTime
        outputs
        """
    )

    query = f"""
    query playbookExecution($playbookExecutionId: ID!) {{
      playbookExecution(playbookExecutionId: $playbookExecutionId) {{
        {fields}
      }}
    }}
    """

    variables = {"playbookExecutionId": execution_id}

    result = client.graphql_run(query=query, variables=variables)

    try:
        execution = result["data"]["playbookExecution"]
        execution["url"] = generate_id_url(env, "automations/playbook-executions", execution["id"])
    except (KeyError, TypeError):
        raise ValueError(f"Failed to fetch playbook execution: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.PlaybookExecution",
        outputs_key_field="id",
        outputs=execution,
        readable_output=tableToMarkdown(
            "Taegis Playbook Execution",
            execution,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_users_command(client: Client, env: str, args=None):
    page = arg_to_number(args.get("page")) or 0
    page_size = arg_to_number(args.get("page_size")) or 10

    variables: dict[str, Any] = {
        "filters": {
            "status": args.get("status", ""),
            "perPage": page_size,
            "pageOffset": page_size * page,
        }
    }
    fields: str = args.get("fields") or "user_id email family_name given_name status"
    if args.get("id"):
        if not args["id"].startswith("auth0"):
            raise ValueError("id MUST be in 'auth0|12345' format")

        query = f"""
        query ($ids: [String!]) {{
            tdrusersByIDs (userIDs: $ids) {{
                {fields}
            }}
        }}
        """
        variables = {"ids": [args["id"]]}
    else:
        query = f"""
        query ($filters: TDRUsersSearchInput) {{
            tdrUsersSearch (filters: $filters) {{
                results {{
                    {fields}
                }}
            }}
        }}
        """

    if args.get("email"):
        variables["filters"]["emails"] = args["email"]

    result = client.graphql_run(query=query, variables=variables)

    try:
        if args.get("id"):
            user = result["data"]["tdrusersByIDs"]
        else:
            user = result["data"]["tdrUsersSearch"]["results"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to fetch user information: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.Users",
        outputs_key_field="user_id",
        outputs=user,
        readable_output=tableToMarkdown(
            "Taegis Users",
            user,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def isolate_asset_command(client: Client, env: str, args=None):
    if not args.get("id"):
        raise ValueError("Cannot isolate asset, missing id")
    if not args.get("reason"):
        raise ValueError("Cannot isolate asset, missing reason")

    variables: dict[str, Any] = {"id": args.get("id"), "reason": args.get("reason")}

    fields: str = args.get("fields") or "id"

    query = f"""
    mutation isolateAsset ($id: ID!, $reason: String!) {{
      isolateAsset (id: $id, reason: $reason) {{
        {fields}
      }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)

    try:
        isolation = result["data"]["isolateAsset"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to isolate asset: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.AssetIsolation",
        outputs_key_field="id",
        outputs=isolation,
        readable_output=tableToMarkdown(
            "Taegis Asset Isolation",
            isolation,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def update_alert_status_command(client: Client, env: str, args=None):
    if not args.get("ids"):
        raise ValueError("Alert IDs must be defined")
    if not args.get("status"):
        raise ValueError("Alert status must be defined")

    if args.get("status").upper() not in ALERT_STATUSES:
        raise ValueError(
            f"The provided status, {args['status']}, is not valid for updating an alert. "
            f"Supported Status Values: {ALERT_STATUSES}"
        )

    variables = {
        "alert_ids": argToList(args.get("ids")),
        "reason": args.get("reason", ""),
        "resolution_status": args.get("status"),
    }

    fields: str = args.get("fields") or "resolution_status reason"

    query = f"""
    mutation alertsServiceUpdateResolutionInfo($alert_ids: [String!], $reason: String, $resolution_status: ResolutionStatus) {{
      alertsServiceUpdateResolutionInfo(
        in: {{
          alert_ids: $alert_ids,
          reason: $reason,
          resolution_status: $resolution_status
        }}
      ) {{
        {fields}
      }}
    }}
    """

    result = client.graphql_run(query=query, variables=variables)

    try:
        update_result = result["data"]["alertsServiceUpdateResolutionInfo"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to locate/update alert: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.AlertStatusUpdate",
        outputs_key_field="status",
        outputs=update_result,
        readable_output=tableToMarkdown(
            "Taegis Alert Update",
            update_result,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def update_comment_command(client: Client, env: str, args=None):
    if not args.get("id"):
        raise ValueError("Cannot update comment, comment id cannot be empty")

    if not args.get("comment"):
        raise ValueError("Cannot update comment, comment cannot be empty")

    fields: str = args.get("fields") or "id"

    query = f"""
    mutation updateInvestigationComment($input: UpdateInvestigationCommentInput!) {{
        updateInvestigationComment(input: $input) {{
            {fields}
        }}
    }}
    """
    variables = {
        "input": {
            "commentId": args.get("id"),
            "comment": args.get("comment"),
        }
    }

    result = client.graphql_run(query=query, variables=variables)

    try:
        comment = result["data"]["updateInvestigationComment"]
    except (KeyError, TypeError):
        raise ValueError(f"Failed to locate/update comment: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.CommentUpdate",
        outputs_key_field="id",
        outputs=comment,
        readable_output=tableToMarkdown(
            "Taegis Comment",
            comment,
            removeNull=True,
        ),
        raw_response=result,
    )

    return results


def update_investigation_command(client: Client, env: str, args=None):
    if not args.get("id"):
        raise ValueError("Cannot fetch investigation without id defined")

    fields: str = args.get("fields") or "id shortId"

    query = f"""
    mutation updateInvestigationV2($input: UpdateInvestigationV2Input!) {{
        updateInvestigationV2(input: $input) {{
            {fields}
        }}
    }}
    """

    variables = {"input": {"id": args.get("id")}}

    for field in INVESTIGATION_UPDATE_FIELDS:
        if not args.get(field):
            continue

        if field == "assigneeId" and not args["assigneeId"].startswith("auth0") and args["assigneeId"] != "@secureworks":
            raise ValueError("assigneeId MUST be in 'auth0|12345' format or '@secureworks'")
        if field == "priority" and not 0 < int(args.get("priority", 0)) < 5:
            raise ValueError("Priority must be between 1-4")
        if field == "status" and args.get("status") not in INVESTIGATION_STATUSES:
            raise ValueError(
                f"The provided status, {args['status']}, is not valid for updating an investigation. "
                f"Supported Status Values: {INVESTIGATION_STATUSES}"
            )
        if field == "type" and args.get("type") not in INVESTIGATION_TYPES:
            raise ValueError(
                f"The provided type, {args['type']}, is not valid for updating an investigation. "
                f"Supported Type Values: {INVESTIGATION_TYPES}"
            )

        if field == "tags":
            variables["input"]["tags"] = argToList(args["tags"])
        else:
            variables["input"][field] = args.get(field)

    if len(variables["input"]) < 2:
        raise ValueError(f"No valid investigation fields provided. Supported Update Fields: {INVESTIGATION_UPDATE_FIELDS}")

    result = client.graphql_run(query=query, variables=variables)

    try:
        investigation = result["data"]["updateInvestigationV2"]
        investigation["url"] = generate_id_url(env, "investigations", investigation["id"])
    except (KeyError, TypeError):
        raise ValueError(f"Failed to locate/update investigation: {result['errors'][0]['message']}")

    results = CommandResults(
        outputs_prefix="TaegisXDR.InvestigationUpdate",
        outputs_key_field="id",
        outputs=investigation,
        readable_output=tableToMarkdown(
            "Taegis Investigation",
            investigation,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def archive_investigation_command(client: Client, env: str, args=None):
    investigation_id = args.get("id")
    if not investigation_id:
        raise ValueError("Cannot archive investigation, missing investigation id")

    fields: str = args.get("fields") or "id"

    query = f"""
    mutation ($investigation_id: ID!) {{
      archiveInvestigation(investigation_id: $investigation_id) {{
        {fields}
      }}
    }}
    """

    variables = {"investigation_id": investigation_id}
    result = client.graphql_run(query=query, variables=variables)
    try:
        investigation = result["data"]["archiveInvestigation"]
        status = "Successfully Archived Investigation"
    except (KeyError, TypeError):
        raise ValueError(f"Could not locate investigation with id: {investigation_id}")

    archive_results = {
        "id": investigation_id,
        "result": investigation,
        "status": status,
        "url": generate_id_url(env, "investigations", investigation_id),
    }

    results = CommandResults(
        outputs_prefix="TaegisXDR.ArchivedInvestigation",
        outputs_key_field="id",
        outputs=archive_results,
        readable_output=tableToMarkdown(
            "Taegis Investigation Archiving",
            archive_results,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def unarchive_investigation_command(client: Client, env: str, args=None):
    investigation_id = args.get("id")
    if not investigation_id:
        raise ValueError("Cannot unarchive investigation, missing investigation id")

    fields: str = args.get("fields") or "id"

    query = f"""
    mutation ($investigation_id: ID!) {{
      unArchiveInvestigation(investigation_id: $investigation_id) {{
        {fields}
      }}
    }}
    """

    variables = {"investigation_id": investigation_id}
    result = client.graphql_run(query=query, variables=variables)
    try:
        investigation = result["data"]["unArchiveInvestigation"]
        status = "Successfully Unarchived Investigation"
    except (KeyError, TypeError):
        if result["errors"][0].get("message"):
            investigation = {}
            status = "Investigation is not currently archived"
        else:
            raise ValueError(f"Could not locate investigation with id: {investigation_id}")

    archive_results = {
        "id": investigation_id,
        "result": investigation,
        "status": status,
        "url": generate_id_url(env, "investigations", investigation_id),
    }

    results = CommandResults(
        outputs_prefix="TaegisXDR.UnarchivedInvestigation",
        outputs_key_field="id",
        outputs=archive_results,
        readable_output=tableToMarkdown(
            "Taegis Investigation Unarchiving",
            archive_results,
            removeNull=True,
            url_keys=("url"),
        ),
        raw_response=result,
    )

    return results


def fetch_events_command(client: Client, env: str, args=None):
    """
    Fetch Taegis events using a CQL query string with optional pagination support.
    Mirrors the Taegis SDK event_query / event_page functionality.

    For standard CQL searches, the limit is passed as a GraphQL variable ($limit).
    For user-provided queries, the limit should be embedded directly in the CQL string.
    Pagination is handled via the 'next' cursor token returned in each event object.
    """
    limit = arg_to_number(args.get("limit", 50))
    offset = arg_to_number(args.get("offset", 0))
    cql_query: str | None = args.get("cql_query")

    fields: str = args.get("fields") or DEFAULT_EVENT_FIELDS

    if args.get("ids"):
        # Fetch events by IDs — no CQL query involved
        variables: dict[str, Any] = {
            "ids": argToList(args.get("ids")),
        }
        query = f"""
        query eventsServiceRetrieveEventsById($ids: [String!]) {{
            eventsServiceRetrieveEventsById(
                in: {{
                    iDs: $ids
                }}
            ) {{
                {fields}
            }}
        }}
        """
        result = client.graphql_run(query=query, variables=variables)
        try:
            events = result["data"]["eventsServiceRetrieveEventsById"]
        except (KeyError, TypeError) as e:
            demisto.debug(f"fetch_events_command (ids) exception: {e}\nFull result: {result}")
            raise ValueError(f"Failed to fetch events by ID: {result.get('errors', [{}])[0].get('message', 'Unknown error')}")

    elif args.get("next"):
        # Fetch next page using pagination cursor — no CQL query involved
        variables = {"next": args.get("next")}
        query = f"""
        query eventsServiceEventPage($next: String!) {{
            eventsServiceEventPage(next: $next) {{
                {fields}
            }}
        }}
        """
        result = client.graphql_run(query=query, variables=variables)
        try:
            events = result["data"]["eventsServiceEventPage"]
        except (KeyError, TypeError) as e:
            demisto.debug(f"fetch_events_command (next page) exception: {e}\nFull result: {result}")
            raise ValueError(f"Failed to fetch events page: {result.get('errors', [{}])[0].get('message', 'Unknown error')}")

    else:
        # Standard CQL query — assign default only here
        if not cql_query:
            cql_query = f"FROM * EARLIEST=-1m | head {limit}"
        variables = {
            "cql_query": cql_query,
            "limit": limit,
            "offset": offset,
        }
        query = f"""
        query eventsServiceSearch($cql_query: String, $limit: Int, $offset: Int) {{
            eventsServiceSearch(
                in: {{
                    cql_query: $cql_query,
                    limit: $limit,
                    offset: $offset
                }}
            ) {{
                {fields}
            }}
        }}
        """
        result = client.graphql_run(query=query, variables=variables)
        try:
            events = result["data"]["eventsServiceSearch"]
        except (KeyError, TypeError) as e:
            demisto.debug(f"fetch_events_command (cql_query) exception: {e}\nFull result: {result}")
            raise ValueError(f"Failed to fetch events: {result.get('errors', [{}])[0].get('message', 'Unknown error')}")

    if not events:
        events = []
        readable_output = "No events found."
    else:
        readable_events = [
            {
                "ID": e.get("id"),
                "Event Type": (e.get("metadata") or {}).get("event_type"),
                "Event Time": (e.get("metadata") or {}).get("event_time"),
                "Tenant ID": (e.get("metadata") or {}).get("tenant_id"),
                "Sensor ID": (e.get("metadata") or {}).get("sensor_id"),
                "Parent Process ID": e.get("parent_process_id"),
                "Image Path": e.get("image_path"),
                "Command Line": e.get("commandline"),
                "Username": e.get("username"),
            }
            for e in events
        ]

        readable_output = tableToMarkdown(
            "Taegis Events",
            readable_events,
            headers=[
                "ID",
                "Event Type",
                "Event Time",
                "Tenant ID",
                "Sensor ID",
                "Parent Process ID",
                "Image Path",
                "Command Line",
                "Username",
            ],
            removeNull=True,
        )

    results = CommandResults(
        outputs_prefix="TaegisXDR.Events",
        outputs_key_field="id",
        outputs=events,
        readable_output=readable_output,
        raw_response=result,
    )

    return results


def test_module(client: Client) -> str:
    """
    Returns success if authentication was successful
    """
    try:
        client.test()
        return "ok"
    except DemistoException as exception:
        raise DemistoException(exception)


""" UTILITIES """


def generate_id_url(env: str, endpoint: str, element_id: str):
    element_id: str = element_id.replace("/", "%2F")
    return f"{ENV_URLS[env]['xdr']}/{endpoint}/{element_id}"


""" MAIN """


def main():
    command = demisto.command()
    demisto.debug(f"Running Taegis Command: {command}")

    commands: dict[str, Any] = {
        "fetch-incidents": fetch_incidents,
        "taegis-add-evidence-to-investigation": add_evidence_to_investigation_command,
        "taegis-create-comment": create_comment_command,
        "taegis-create-investigation": create_investigation_command,
        "taegis-create-sharelink": create_sharelink_command,
        "taegis-execute-playbook": execute_playbook_command,
        "taegis-fetch-alerts": fetch_alerts_command,
        "taegis-fetch-assets": fetch_assets_command,
        "taegis-fetch-comment": fetch_comment_command,
        "taegis-fetch-comments": fetch_comments_command,
        "taegis-fetch-endpoint": fetch_endpoint_command,
        "taegis-fetch-investigation": fetch_investigation_command,
        "taegis-fetch-investigation-alerts": fetch_investigation_alerts_command,
        "taegis-fetch-playbook-execution": fetch_playbook_execution_command,
        "taegis-fetch-users": fetch_users_command,
        "taegis-isolate-asset": isolate_asset_command,
        "taegis-fetch-events": fetch_events_command,
        "taegis-update-alert-status": update_alert_status_command,
        "taegis-update-comment": update_comment_command,
        "taegis-update-investigation": update_investigation_command,
        "taegis-archive-investigation": archive_investigation_command,
        "taegis-unarchive-investigation": unarchive_investigation_command,
        "test-module": test_module,
    }

    ARGS = demisto.args()
    PARAMS = demisto.params()
    try:
        if command not in commands:
            raise NotImplementedError(f'The "{command}" command has not been implemented.')

        environment = PARAMS.get("environment", "us1").lower()
        if not ENV_URLS.get(environment):
            raise ValueError(f"Unknown Environment Provided: {environment}")

        verify_cert = not PARAMS.get("insecure", False)

        client = Client(
            client_id=PARAMS.get("client_id"),
            client_secret=PARAMS.get("client_secret"),
            base_url=ENV_URLS[environment]["api"],
            proxy=PARAMS.get("proxy", False),
            verify=verify_cert,
            tenant_id=ARGS.get("tenant_id"),
        )
        client.auth()

        if command == "test-module":
            result = commands[command](client=client)
            return_results(result)

        elif command == "fetch-incidents":
            commands[command](
                client=client,
                env=environment,
                fetch_type=PARAMS.get("fetch_type"),
                max_fetch=PARAMS.get("max_fetch"),
                include_assets=PARAMS.get("include_assets"),
                first_fetch_interval=PARAMS.get("first_fetch", DEFAULT_FIRST_FETCH_INTERVAL),
            )
        else:
            return_results(commands[command](client=client, env=environment, args=ARGS))
    except Exception as e:
        error_string = str(e)
        demisto.error(f"Error running command: {e}")

        if "Unauthorized" in error_string:
            error_string = "Invalid credentials (Client ID or Client Secret)"
        return_error(f"Failed to execute {command} command. Error: {error_string}")


""" ENTRY POINT """
if __name__ in ("__main__", "__builtin__", "builtins"):
    main()