IBM Resilient Systems

Case management that enables visibility across your tools for continual IR improvement.

Case Management · IBM Security QRadar SOAR

Details

IDIBM Resilient Systems
ProviderIBM
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/resilient:2.0.0.10133006
Supported ModulesAgentix XSIAM

README

Case management that enables visibility across your tools for continual IR improvement.

Configure IBM Resilient Systems in Cortex

Parameter Description Required
Server URL (e.g. 192.168.0.1)   True
Organization name   True
API key ID   False
API key secret   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Incident type   False
Fetch incidents   False
Fetch closed incidents   False
Fetch notes    
Fetch tasks    
First fetch timestamp (YYYY-MM-DDTHH:MM:SSZ). For example: 2020-02-02T19:00:00Z   False
Maximum incidents to fetch. Maximum number of incidents per fetch. The maximum is 1000. False
Incident Mirroring Direction Cortex XSOAR only parameter. False
Close Mirrored XSOAR Incidents When selected, closing the IBM Resilient incident with a ‘Closed’ status, will close the Cortex XSOAR incident. False
Close Mirrored IBM Resilient Incidents When selected, closing the Cortex XSOAR incident will close the incident in IBM Resilient. False
Tag from IBM Qradar SOAR Add this tag to an entry to mirror it from IBM Qradar SOAR. False
Tag to IBM QRadar SOAR Add this tag to an entry to mirror it to IBM Qradar SOAR. 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.

rs-search-incidents


Query for incidents

Base Command

rs-search-incidents

Input

Argument Name Description Required
page Indicates the starting point for the first record in the dataset. Starting from 1. Optional
page_size The maximum number of records to return in the response. Null or less than 1 to return all records, up to the server-configured maximum limit. When greater than 0, an error will be thrown if the length is greater than the server-configured maximum limit. Optional
limit Maximum number of incidents to retrieve. Default is 1000. Optional
severity Comma-separated list of incident severity, e.g., Low,Medium,High. Optional
date-created-before Created date of the incident before the given date in the format YYYY-MM-DDTHH:MM:SSZ, e.g., 2018-05-07T10:59:07Z. Optional
date-created-after Created date of the incident after the given date in the format YYYY-MM-DDTHH:MM:SSZ, e.g., 2018-05-07T10:59:07Z. Optional
date-created-within-the-last Created date of the incident within the last timeframe (days/hours/minutes). Should be given a number, along with the timeframe argument. Optional
timeframe Timeframe to search within for incident. Should be given with within-the-last/due-in argument. Possible values: “days”, “hours”, “minutes”. Possible values are: days, hours, minutes. Optional
date-occurred-within-the-last Occurred date of the incident within the last timeframe (days/hours/minutes). Should be given a number, along with the timeframe argument. Optional
date-occurred-before Occurred date of the incident before the given date in the format YYYY-MM-DDTHH:MM:SSZ, e.g., 2018-05-07T10:59:07Z. Optional
date-occurred-after Occurred date of the incident after the given date in the format YYYY-MM-DDTHH:MM:SSZ, e.g., 2018-05-07T10:59:07Z. Optional
incident-type Incident type. Possible values are: CommunicationError, DenialOfService, ImproperDisposal:DigitalAsset, ImproperDisposal:documents/files, LostDocuments/files/records, LostPC/laptop/tablet, LostPDA/smartphone, LostStorageDevice/media, Malware, NotAnIssue, Other, Phishing, StolenDocuments/files/records, StolenPC/laptop/tablet, StolenPDA/Smartphone, StolenStorageDevice/media, SystemIntrusion, TBD/Unknown, Vendor/3rdPartyError. Optional
nist NIST Attack Vectors. Possible values: “Attrition”, “E-mail”, “External/RemovableMedia”, “Impersonation”, “ImproperUsage”, “Loss/TheftOfEquipment”, “Other”, “Web”. Possible values are: Attrition, E-mail, External/RemovableMedia, Impersonation, ImproperUsage, Loss/TheftOfEquipment, Other, Web. Optional
status Incident status. Possible values: “Active” and “Closed”. Possible values are: Active, Closed. Optional
due-in Due date of the incident in given timeframe (days/hours/minutes). Should be given a number, along with the timeframe argument. Optional
return_level The incident data structure returned (“partial”, “normal”, “full”).’. Possible values are: partial, normal, full. Optional

Context Output

Path Type Description
Resilient.Incidents.CreateDate string Created date of the incident.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Resolution string Incident resolution.
Resilient.Incidents.DiscoveredDate string Discovered date of the incident.
Resilient.Incidents.ResolutionSummary string Incident resolution summary.
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Phase string Incident phase.
Resilient.Incidents.PlanStatus string Incident status.
Resilient.Incidents.Severity string Incident severity.
Resilient.Incidents.Description string Incident description.
Resilient.Incidents.Confirmed boolean Incident confirmation.
Resilient.Incidents.NegativePr boolean Whether negative PR is likely.
Resilient.Incidents.DateOccurred string Date incident occurred.
Resilient.Incidents.Reporter string Name of reporting individual.
Resilient.Incidents.NistAttackVectors Unknown Incident NIST attack vectors.
Resilient.Incidents.ExposureType string Incident exposure type.
Resilient.Incidents.ResolutionSummary string Incident resolution summary.

rs-update-incident


Updates incidents.

Base Command

rs-update-incident

Input

Argument Name Description Required
incident-id Incident ID to update. Required
severity Severity to update. Possible value: “Low”, “Medium”, and “High”. Possible values are: Low, Medium, High. Optional
owner User full name to set as incident owner, e.g., Steve Jobs. Optional
incident-type Incident type (added to the current incident types list). Possible values are: CommunicationError, DenialOfService, ImproperDisposal:DigitalAsset, ImproperDisposal:documents/files, LostDocuments/files/records, LostPC/laptop/tablet, LostPDA/smartphone, LostStorageDevice/media, Malware, NotAnIssue, Other, Phishing, StolenDocuments/files/records, StolenPC/laptop/tablet, StolenPDA/Smartphone, StolenStorageDevice/media, SystemIntrusion, TBD/Unknown, Vendor/3rdPartyError. Optional
resolution Incident resolution. Possible value: “Unresolved”, “Duplicate”, “NotAnIssue”, and “Resolved”. Possible values are: Unresolved, Duplicate, NotAnIssue, Resolved. Optional
resolution-summary Incident resolution summary. Optional
description Incident description. Optional
name Incident name. Optional
nist NIST Attack Vectors (added to the current list of NIST attack vectors). Possible values: “Attrition”, “E-mail”, “External/RemovableMedia”, “Impersonation”, “ImproperUsage”, “Loss/TheftOfEquipment”, “Other”, “Web”. Possible values are: Attrition, E-mail, External/RemovableMedia, Impersonation, ImproperUsage, Loss/TheftOfEquipment, Other, Web. Optional
other-fields A JSON object of the form: {field_name: new_field_value}. For example: {"description": {"textarea": {"format": "html", "content": "The new description"}}, "name": {"text": "The new name"}}. The name should be the path to it in the incident separated by “.” For example: `{“properties.incident_summary”: {“text”: “The new name”}}”. Because of API limitations we currently support only fields of the following types: ID, list of IDS, Number, Boolean, Text, Data, Textarea. For more information, refer to https://xsoar.pan.dev/docs/reference/integrations/ibm-resilient-systems. In case of conflicts between the other-fields argument and the regular fields arguments, the other-fields value will be used. Optional

Context Output

There is no context output for this command.

Command Example

!rs-update-incident incident-id=1234 severity=High incident-type=Malware

Human Readable Output

Incident 1234 was updated successfully.

rs-incidents-get-members


Gets members of the incident.

Base Command

rs-incidents-get-members

Input

Argument Name Description Required
incident-id Incident ID to get members of. Required

Context Output

Path Type Description
Resilient.Incidents.ID string Incident ID.
Resilient.Incidents.Members.FirstName string Member’s first name.
Resilient.Incidents.Members.LastName string Member’s last name.
Resilient.Incidents.Members.ID number Member’s ID.
Resilient.Incidents.Members.Email string Member’s email address.

Command Example

!rs-incidents-get-members incident-id=1234

Context Example

{
    "Resilient": {
        "Incidents": {
            "Id": "1234",
            "Members": [
                {
                    "Email": "example@example.com",
                    "FirstName": "example",
                    "ID": 1,
                    "LastName": "example"
                }
            ]
        }
    }
}

Human Readable Output

Members of incident 1234

ID LastName FirstName Email
1 example example example@example.com

rs-get-incident


Gets an individual incident by ID.

Base Command

rs-get-incident

Input

Argument Name Description Required
incident-id ID of incident to get. Required

Context Output

Path Type Description
Resilient.Incidents.CreateDate string Created date of the incident.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Resolution string Incident resolution.
Resilient.Incidents.DiscoveredDate string Discovered date of the incident.
Resilient.Incidents.ResolutionSummary string Incident resolution summary.
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Phase string Incident phase.
Resilient.Incidents.PlanStatus string Incident status.
Resilient.Incidents.Severity string Incident severity.
Resilient.Incidents.Description string Incident description.
Resilient.Incidents.Confirmed boolean Incident confirmation.
Resilient.Incidents.NegativePr boolean Whether negative PR is likely.
Resilient.Incidents.DateOccurred string Date incident occurred.
Resilient.Incidents.Reporter string Name of reporting individual.
Resilient.Incidents.NistAttackVectors Unknown Incident NIST attack vectors.
Resilient.Incidents.ExposureType string Incident exposure type.

rs-get-incident


Gets an individual incident by ID.

Base Command

rs-get-incident

Input

Argument Name Description Required
incident-id ID of incident to get. Required

Context Output

Path Type Description
Resilient.Incidents.CreateDate string Created date of the incident.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Resolution string Incident resolution.
Resilient.Incidents.DiscoveredDate string Discovered date of the incident.
Resilient.Incidents.ResolutionSummary string Incident resolution summary.
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Phase string Incident phase.
Resilient.Incidents.PlanStatus string Incident status.
Resilient.Incidents.Severity string Incident severity.
Resilient.Incidents.Description string Incident description.
Resilient.Incidents.Confirmed boolean Incident confirmation.
Resilient.Incidents.NegativePr boolean Whether negative PR is likely.
Resilient.Incidents.DateOccurred string Date incident occurred.
Resilient.Incidents.Reporter string Name of reporting individual.
Resilient.Incidents.NistAttackVectors Unknown Incident NIST attack vectors.
Resilient.Incidents.ExposureType string Incident exposure type.
Resilient.Incidents.ResolutionSummary string Incident resolution summary.
    "Incidents": {
        "Id": "1234",
        "Members": {
            "Email": "example@exampe.com",
            "FirstName": "example",
            "ID": 2,
            "LastName": "example",
            "members": [],
            "vers": 10
        }
    }
} }

#### Human Readable Output

>### Members of incident 1234
>|Email|FirstName|ID|LastName|members|vers|
>|---|---|---|---|---|---|
>| example@example.com | example | 2 | example |  | 10 |


### rs-get-users
***
Gets a list of all users in the system.


#### Base Command

`rs-get-users`
#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example
```!rs-get-users```

#### Human Readable Output

>### IBM Resilient Systems Users
>|ID|LastName|FirstName|Email|
>|---|---|---|---|
>| 1 | example | example | example@example.com |
>| 2 | example1 | example1 | example1@example.com |


### rs-close-incident
***
Closes an incident.


#### Base Command

`rs-close-incident`
#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| incident-id | ID of the incident to close. | Required | 


#### Context Output

There is no context output for this command.

#### Command Example
```!rs-close-incident incident-id=1234```

#### Human Readable Output

>Incident 1234 was closed.

### rs-create-incident
***
Creates an incident.


#### Base Command

`rs-create-incident`
#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| name | Incident name. | Required | 


#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Resilient.Incidents.Id | string | Incident ID. | 
| Resilient.Incidents.Name | string | Incident name. | 


#### Command Example
```!rs-create-incident name=IncidentName```

#### Context Example
```json
{
    "Resilient": {
        "Incidents": {
            "Id": "1235",
            "Name": "IncidentName"
        }
    }
}

Human Readable Output

Incident IncidentName was created

ID Name
1235 IncidentName

rs-incident-artifacts


Gets incident artifacts.

Base Command

rs-incident-artifacts

Input

Argument Name Description Required
incident-id Incident ID to get artifacts of. Required

Context Output

Path Type Description
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Artifacts.CreatedDate string Artifact created date.
Resilient.Incidents.Artifacts.Creator string Artifact creator.
Resilient.Incidents.Artifacts.Description string Artifact description.
Resilient.Incidents.Artifacts.ID number Artifact ID.
Resilient.Incidents.Artifacts.Type string Artifact type.
Resilient.Incidents.Artifacts.Value string Artifact value.
Resilient.Incidents.Artifacts.Attachments.ContentType string Attachment content type.
Resilient.Incidents.Artifacts.Attachments.CreatedDate string Attachment created date.
Resilient.Incidents.Artifacts.Attachments.Creator string Attachment creator.
Resilient.Incidents.Artifacts.Attachments.ID number Attachment ID.
Resilient.Incidents.Artifacts.Attachments.Name string Attachment name.
Resilient.Incidents.Artifacts.Attachments.Size number Attachment size.

Command Example

!rs-incident-artifacts incident-id=1234

Context Example

{
    "Resilient": {
        "Incidents": {
            "Artifacts": [
                {
                    "CreatedDate": "2000-00-00T00:00:00Z",
                    "Creator": "example example",
                    "Description": "example",
                    "ID": 1,
                    "Type": "IP Address",
                    "Value": "1.1.1.1"
                },
                {
                    "CreatedDate": "2000-00-00T00:00:00Z",
                    "Creator": "example example",
                    "Description": "example",
                    "ID": 2,
                    "Type": "IP Address",
                    "Value": "2.2.2.2"
                }
             ],
            "Id": "1234",
            "Name": "example"
        }
    }
}

Human Readable Output

Incident 1234 artifacts

ID Value Description CreatedDate Creator
1 1.1.1.1 example 2000-00-00T00:00:00Z example example
2 2.2.2.2 example 2000-00-00T00:00:00Z example example

rs-incident-attachments


Gets incident attachments.

Base Command

rs-incident-attachments

Input

Argument Name Description Required
incident-id Incident ID to get attachments from. Required

Context Output

Path Type Description
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Owner string Incident owner.
Resilient.Incidents.Attachments.ContentType string Attachment content type.
Resilient.Incidents.Attachments.CreatedDate string Attachment created date.
Resilient.Incidents.Attachments.Creator string Attachment creator.
Resilient.Incidents.Attachments.ID number Attachment ID.
Resilient.Incidents.Attachments.Name string Attachment name.
Resilient.Incidents.Attachments.Size number Attachment size.

Command Example

!rs-incident-attachments incident-id=1234

Context Example

{
    "Resilient": {
        "Incidents": {
            "Attachments": [
                {
                    "ContentType": "example",
                    "CreatedDate": "2000-00-00T00:00:00Z",
                    "Creator": "example example",
                    "ID": 1,
                    "Name": "example",
                    "Size": 10
                }
            ],
            "Id": "1234",
            "Name": "example",
            "Owner": "example example"
        }
    }
}

Human Readable Output

Incident 1234 attachments

ContentType CreatedDate Creator ID Name Size
example 2000-00-00T00:00:00Z example example 1 example 10

rs-related-incidents


Gets related incidents.

Base Command

rs-related-incidents

Input

Argument Name Description Required
incident-id Incident ID to get related incidents of. Required

Context Output

Path Type Description
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Related.CreatedDate string Created date of the related incident.
Resilient.Incidents.Related.Name string Name of the related incident.
Resilient.Incidents.Related.ID number ID of the related incident.
Resilient.Incidents.Related.Status string Status (Active/Closed) of the related incident.
Resilient.Incidents.Related.Artifacts.CreatedDate string Created date of the artifact.
Resilient.Incidents.Related.Artifacts.ID number ID of the artifact.
Resilient.Incidents.Related.Artifacts.Creator string Creator of the artifact.

Command Example

!rs-related-incidents incident-id=1234

Context Example

{
    "Resilient": {
        "Incidents": {
            "Id": "1234",
            "Related": [
                {
                    "Artifacts": [
                        {
                            "CreatedDate": "2000-00-00T00:00:00Z",
                            "Creator": "example example",
                            "ID": 1
                        },
                        {
                            "CreatedDate": "2000-00-00T00:00:00Z",
                            "Creator": "example example",
                            "Description": "example",
                            "ID": 2
                        }
                    ],
                    "CreatedDate": "2000-00-00T00:00:00Z",
                    "ID": 1235,
                    "Name": "example",
                    "Status": "Closed"
                }
            ]
        }
    }
}

Human Readable Output

Incident 1234 related incidents

Artifacts CreatedDate ID Name Status
ID: 1
Created Date: 2000-00-00T00:00:00Z
Creator: example example
ID: 2
Created Date: 2000-00-00T00:00:00Z
Description: example

Creator: example example
2000-00-00T00:00:00Z 1234 example Closed

rs-incidents-get-tasks


Gets tasks of incidents.

Base Command

rs-incidents-get-tasks

Input

Argument Name Description Required
incident-id Incident ID to get tasks of. Required

Context Output

Path Type Description
Resilient.Incidents.Id string Incident ID.
Resilient.Incidents.Name string Incident name.
Resilient.Incidents.Tasks.Category string Task category.
Resilient.Incidents.Tasks.Creator string Task creator.
Resilient.Incidents.Tasks.DueDate string Task due date.
Resilient.Incidents.Tasks.Form string Task form.
Resilient.Incidents.Tasks.ID string Task ID.
Resilient.Incidents.Tasks.Name string Task name.
Resilient.Incidents.Tasks.Required boolean Whether the task is required.
Resilient.Incidents.Tasks.Status string Task status (Open/Closed).

Command Example

!rs-incidents-get-tasks incident-id=1234

Context Example

{
    "Resilient": {
        "Incidents": {
            "Id": "1234",
            "Name": "example",
            "Tasks": [
                {
                    "Category": "Respond",
                    "Creator": "example example",
                    "Form": "data_compromised, determined_date",
                    "ID": 1,
                    "Name": "example",
                    "Required": true,
                    "Status": "Open"
                }
            ]
        }
    }
}

Human Readable Output

Incident 1234 tasks

ID Name Category Form Status DueDate Instructions UserNotes Required Creator
1 example Respond data_compromised, determined_date Open       true example example

rs-add-note


Add a note to an incident.

Base Command

rs-add-note

Input

Argument Name Description Required
incident-id The ID of the incident. Required
note The text of the note. Required

Context Output

Path Type Description
Resilient.IncidentNote.type String The type of the note (incident or task).
Resilient.IncidentNote.id Number The note’s ID.
Resilient.IncidentNote.parent_id Number The ID of the parent note (null for top-level note)..
Resilient.IncidentNote.user_id Number The ID of the user who created the note.
Resilient.IncidentNote.user_fname String The user’s first name.
Resilient.IncidentNote.user_lname String The user’s last name.
Resilient.IncidentNote.text String The note text.
Resilient.IncidentNote.create_date Date The date the note was created.
Resilient.IncidentNote.modify_date Date The date the note was modified.
Resilient.IncidentNote.is_deleted Boolean The flag indicating if the note is deleted. Generally, note objects are removed from the database when the user deletes them. However, if the user deletes a parent note, the parent is just marked as deleted (and its text is cleared).
Resilient.IncidentNote.modify_user.id Number The ID of the user who last modified the note.
Resilient.IncidentNote.modify_user.first_name String The first name of the user who last modified the note.
Resilient.IncidentNote.modify_user.last_name String The last name of the user who last modified the note.
Resilient.IncidentNote.inc_id Number The ID of the incident to which this note belongs.
Resilient.IncidentNote.inc_name String The name of the incident to which this note belongs.
Resilient.IncidentNote.task_id Number The ID of the task to which this note belongs. Will be null on incident notes.
Resilient.IncidentNote.task_name String The name of the task to which this note belongs. Will be null on incident notes.
Resilient.IncidentNote.task_custom Booolean For a task note, whether that task is a custom task. Null for incident notes.
Resilient.IncidentNote.task_members Unknown For a task note, the list of that task’s members, if any. Null for incident notes.
Resilient.IncidentNote.task_at_id Unknown For a task note, whether that task is an automatic task. Null for incident notes and task notes that are not automatically generated.
Resilient.IncidentNote.inc_owner Number The owner of the incident to which this note belongs.
Resilient.IncidentNote.user_name String The name of the owner of the incident to which this note belongs.
Resilient.IncidentNote.modify_principal.id Number The ID of the principal.
Resilient.IncidentNote.modify_principal.type String The type of the principal. Currently only user or group.
Resilient.IncidentNote.modify_principal.name String The name of the principal.
Resilient.IncidentNote.modify_principal.display_name String The display name of the principal.
Resilient.IncidentNote.comment_perms.update Boolean Whether the current user has permission to update this note.
Resilient.IncidentNote.comment_perms.delete Boolean Whether the current user has permission to delete this note.

Command Example

!rs-add-note incident-id=1234 note="This is a note"

Context Example

{
    "Resilient": {
        "incidentNote": {
            "actions": [],
            "children": [],
            "comment_perms": {
                "delete": true,
                "update": true
            },
            "create_date": 1600000000000,
            "id": 10,
            "inc_id": 1234,
            "inc_name": "example",
            "inc_owner": 1,
            "is_deleted": false,
            "mentioned_users": [],
            "modify_date": 1600000000000,
            "modify_principal": {
                "display_name": "example example",
                "id": 1,
                "name": "example@example.com",
                "type": "user"
            },
            "modify_user": {
                "first_name": "example",
                "id": 1,
                "last_name": "example"
            },
            "parent_id": null,
            "task_at_id": null,
            "task_custom": null,
            "task_id": null,
            "task_members": null,
            "task_name": null,
            "text": "<div>This is a note</div>",
            "type": "incident",
            "user_fname": "example",
            "user_id": 1,
            "user_lname": "example",
            "user_name": "example example"
        }
    }
}

Human Readable Output

The note was added successfully to incident 1234

rs-add-artifact


Add an artifact to an incident.

Base Command

rs-add-artifact

Input

Argument Name Description Required
incident-id The ID of the incident. Required
artifact-type The type of the artifact. Possible values are: DNS Name, Email Attachment, Email Attachment Name, Email Body, Email Recipient, Email Sender, Email Sender Name, Email Subject, File Name, File Path, HTTP Request Header, HTTP Response Header, IP Address, Log File, MAC Address, Malware Family/Variant, Malware MD5 Hash, Malware Sample, Malware Sample Fuzzy Hash, Malware SHA-1 Hash, Malware SHA-256 Hash, Mutex, Network CIDR Range, Observed Data, Other File, Password, Port, Process Name, Registry Key, RFC 822 Email Message File, Service, String, System Name, Threat CVE ID, URI Path, URL, URL Referer, User Account, User Agent, X509 Certificate File. Required
artifact-value The value of the artifact. Required
artifact-description The description of the artifact. Optional

Context Output

Path Type Description
Resilient.IncidentArtifact.id Number The ID of the artifact.
Resilient.IncidentArtifact.type Number The type of the artifact.
Resilient.IncidentArtifact.value String The value of the artifact. For example, the IP address for an IP address artifact.
Resilient.IncidentArtifact.description String The description of the artifact.
Resilient.IncidentArtifact.attachment Unknown The files attached to the artifact.
Resilient.IncidentArtifact.parent_id Number The parent artifact ID.
Resilient.IncidentArtifact.creator.id Number The ID of the artifact creator.
Resilient.IncidentArtifact.creator.fname String The first name of the artifact creator.
Resilient.IncidentArtifact.creator.lname String The last name of the artifact creator.
Resilient.IncidentArtifact.creator.display_name String The display name of the artifact creator.
Resilient.IncidentArtifact.creator.status String The status of the artifact creator.
Resilient.IncidentArtifact.creator.email String The email of the artifact creator.
Resilient.IncidentArtifact.creator.phone String The phone number of the artifact creator.
Resilient.IncidentArtifact.creator.cell String The cellphone number of the artifact creator.
Resilient.IncidentArtifact.creator.title String The user’s job title (e.g., Incident Response Manager).
Resilient.IncidentArtifact.creator.locked Boolean The status of the creator’s account. (True if locked. false otherwise).
Resilient.IncidentArtifact.creator.password_changed Boolean Whether the user’s password has changed. (True if changed, false otherwise).
Resilient.IncidentArtifact.creator.is_external Boolean Whether the user’s account is authenticated externally.
Resilient.IncidentArtifact.creator.ui_theme String The UI theme the user has selected. The Resilient UI recognizes the following values (darkmode, lightmode, verydarkmode).
Resilient.IncidentArtifact.inc_id Number The incident ID.
Resilient.IncidentArtifact.inc_name String The incident name.
Resilient.IncidentArtifact.inc_owner Number The incident owner.
Resilient.IncidentArtifact.created Date The date when the artifact is created.
Resilient.IncidentArtifact.last_modified_time Date The last date on which the artifact changed.
Resilient.IncidentArtifact.last_modified_by.id Number The ID of the user who last changed the artifact.
Resilient.IncidentArtifact.last_modified_by.type String The type of user who last changed the artifact.
Resilient.IncidentArtifact.last_modified_by.name String The name of the user who last changed the artifact.
Resilient.IncidentArtifact.last_modified_by.display_name String The display name of the user who last changed the artifact.
Resilient.IncidentArtifact.perms.read Boolean Whether the current user has permission to read this artifact.
Resilient.IncidentArtifact.perms.write Boolean Whether the current user has permission to write to this artifact.
Resilient.IncidentArtifact.perms.delete Boolean Whether the current user has permission to delete this artifact.
Resilient.IncidentArtifact.properties Unknown The additional artifact properties.
Resilient.IncidentArtifact.hash String The hash of the incident.
Resilient.IncidentArtifact.relating Boolean Whether this artifact should be used for relating to other incidents. Null means use the default specified by the type. True means to always relate. False means to never relate.
Resilient.IncidentArtifact.creator_principal.id Number The ID of the principal.
Resilient.IncidentArtifact.creator_principal.type String The type of the principal. Currently only user or group.
Resilient.IncidentArtifact.creator_principal.name String The API name of the principal.
Resilient.IncidentArtifact.creator_principal.display_name String The display name of the principal.
Resilient.IncidentArtifact.ip.source Boolean Whether the IP address is a source.
Resilient.IncidentArtifact.ip.destination Boolean Whether the IP address is a destination.

Command Example

!rs-add-artifact artifact-type="IP Address" artifact-value=1.1.1.1 incident-id=1234 artifact-description="This is a description"

Context Example

{
    "Resilient": {
        "incidentArtifact": {
            "actions": [],
            "attachment": null,
            "created": 1600000000000,
            "creator": {
                "cell": "",
                "display_name": "example example",
                "email": "example@example.com",
                "fname": "example",
                "id": 9,
                "is_external": false,
                "lname": "example",
                "locked": false,
                "password_changed": false,
                "phone": "",
                "status": "A",
                "title": "",
                "ui_theme": "darkmode"
            },
            "creator_principal": {
                "display_name": "example example",
                "id": 1,
                "name": "example@example.com",
                "type": "user"
            },
            "description": "example",
            "hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "hits": [],
            "id": 1,
            "inc_id": 1234,
            "inc_name": "example",
            "inc_owner": 1,
            "ip": {
                "destination": null,
                "source": null
            },
            "last_modified_by": {
                "display_name": "example example",
                "id": 1,
                "name": "example@example.com",
                "type": "user"
            },
            "last_modified_time": 1600000000000,
            "parent_id": null,
            "pending_sources": [],
            "perms": {
                "delete": true,
                "read": true,
                "write": true
            },
            "properties": null,
            "relating": null,
            "type": 1,
            "value": "1.1.1.1"
        }
    }
}

Human Readable Output

The artifact was added successfully to incident 1234

rs-delete-incidents


Delete multiple incidents.

Base Command

rs-delete-incidents

Input

Argument Name Description Required
incident_ids A comma-separated list of incident IDs to be deleted. Required

Context Output

There is no context output for this command.

rs-list-incident-notes


Gets all of the top-level comments for an incident.

Base Command

rs-list-incident-notes

Input

Argument Name Description Required
incident_id Incident ID to update. Required

Context Output

Path Type Description
Resilient.IncidentNote Dictionary Top-level comments for incident.

rs-update-task


Update an incident’s task fields.

Base Command

rs-update-task

Input

Argument Name Description Required
task_id ID of task to update. Required
name Task name. Technically required, copy original task name if no changes are desired. Required
owner_id User ID of the new owner. Optional
due_date Task due date in ISO format e.g,. “2020-02-02T19:00:00Z. Empty date indicates that the task has no assigned due date. Optional
phase The phase to which this task belongs. Possible values are: Initial, Engage, Detect/Analyze, Respond, Post-Incident, Custom, Complete. Optional
status Changing the status field, completes or re-openes the task. Possible values are: Open, Completed. Optional

Context Output

There is no context output for this command.

rs-add-custom-task


Adds a custom task to the specified incident.

Base Command

rs-add-custom-task

Input

Argument Name Description Required
incident_id ID of incident to add a task to. Required
name Task name. Required
description Task description. Required
instructions Textual instructions for the task. This will override the default instructions for the task. Required
due_date Task due date in ISO format e.g., “2020-02-02T19:00:00Z. Empty date indicates that the task has no assigned due date. Required
owner_id The owner of the task (ID or name as appears in IBM QRadar SOAR). Leave empty if the task has no owner. Optional
phase Task to be added to the IBM QRadar incident. Possible values are: Initial, Engage, Detect/Analyze, Respond, Post-Incident, Custom, Complete. Default is task. Required

Context Output

There is no context output for this command.

rs-delete-task-members


Delete a task’s member. This effectively changes the task from a “private” task to a non-private task (to one where any incident member can operate on it).

Base Command

rs-delete-task-members

Input

Argument Name Description Required
task_id ID of the task to delete its members. Required

Context Output

There is no context output for this command.

rs-list-tasks


Gets an array of open tasks to which the current user is assigned.

Base Command

rs-list-tasks

Input

There are no input arguments for this command.

Context Output

Path Type Description
Resilient.Tasks Dictionary List of open tasks.

rs-list-scripts


Retrieves the specified script’s information or a list of all organization’s scripts.

Base Command

rs-list-scripts

Input

Argument Name Description Required
script_id Internal ID/name of the script. Optional

Context Output

Path Type Description
Resilient.Scripts Dictionary Retrieved script or list of scripts with metadata.

rs-delete-tasks


Deletes a specified list of tasks. Note that only custom tasks can be deleted.

Base Command

rs-delete-tasks

Input

Argument Name Description Required
task_ids A comma-separated list of task IDs to be deleted. Required

Context Output

There is no context output for this command.

rs-list-task-instructions


Lists the task’s instructions.

Base Command

rs-list-task-instructions

Input

Argument Name Description Required
task_id ID of the task to list its instructions. Required

Context Output

Path Type Description
Resilient.Task Dictionary Task instructions.

rs-update-incident-note


Updates an incident’s note.

Base Command

rs-update-incident-note

Input

Argument Name Description Required
incident_id Incident ID to update its note. Required
note_id Note ID to update. Required
note Text of the note. Required

Context Output

There is no context output for this command.

rs-upload-incident-attachment


Upload an attachment for an incident.

Base Command

rs-upload-incident-attachment

Input

Argument Name Description Required
incident_id Incident ID to update. Required
entry_id EntryID of the file to upload. Required

Context Output

There is no context output for this command.

rs-get-task-members


Get the members of a task. Private tasks will have the returned “members” property set. Non-private tasks will have a null “members” property.

Base Command

rs-get-task-members

Input

Argument Name Description Required
task_id ID of task to get its members. Required

Context Output

Path Type Description
Resilient.Task Dictionary Task members.

rs-get-attachment


Gets incident attachment’s name and contents as a file by its ID.

Base Command

rs-get-attachment

Input

Argument Name Description Required
incident_id Incident ID to get attachments from. Required
attachment_id Attachment ID to get. Required

Context Output

There is no context output for this command.

rs-incidents-update-member


Updates incident’s members.

Base Command

rs-incidents-update-member

Input

Argument Name Description Required
incident-id ID of the incident for which to update its members. Required
members A comma-separated list of members to add, e.g., 1,2,3. Required

Context Output

There is no context output for this command.

Configuration parameters

  • server — Server URL (e.g. 192.168.0.1) (required)
  • org — Organization name (required)
  • api_key_id — API key ID
  • api_key_secret — API key secret
  • credentials_api_key — API key ID
  • credentials — Credentials (Deprecated - use API Key authentication instead)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents
  • fetch_closed — Fetch closed incidents
  • fetch_notes — Fetch notes
  • fetch_tasks — Fetch tasks
  • fetch_time — First fetch timestamp (YYYY-MM-DDTHH:MM:SSZ). For example: 2020-02-02T19:00:00Z
  • max_fetch — Maximum incidents to fetch.
  • mirror_direction — Incident Mirroring Direction
  • close_xsoar_incident — Close Mirrored XSOAR Incidents
  • close_ibm_incident — Close Mirrored IBM Resilient Incidents
  • tag_from_ibm — Tag from IBM Qradar SOAR
  • tag_to_ibm — Tag to IBM QRadar SOAR

Commands (31)

  • get-mapping-fields

    Returns the list of fields to map in outgoing mirroring. This command is only used for debugging purposes.

  • get-modified-remote-data

    Returns the list of incidents IDs that were modified since the last update time. Note that this method is for debugging purposes. The get-modified-remote-data command is used as part of the mirroring feature, which is available from version 6.1.

  • get-remote-data

    Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.

  • rs-add-artifact

    Add an artifact to an incident.

  • rs-add-custom-task

    Adds a custom task to the specified incident.

  • rs-add-note

    Add a note to an incident.

  • rs-close-incident

    Closes an incident.

  • rs-create-incident

    Creates an incident.

  • rs-delete-incidents

    Delete multiple incidents.

  • rs-delete-task-members

    Delete a task's member. This effectively changes the task from a "private" task to a non-private task (to one where any incident member can operate on it).

  • rs-delete-tasks

    Deletes a specified list of tasks. Note that only custom tasks can be deleted.

  • rs-get-attachment

    Gets incident attachment's name and contents as a file by its ID.

  • rs-get-incident

    Gets an individual incident by ID.

  • rs-get-task-members

    Get the members of a task. Private tasks will have the returned "members" property set. Non-private tasks will have a null "members" property.

  • rs-get-users

    Gets a list of all users in the system.

  • rs-incident-artifacts

    Gets incident artifacts.

  • rs-incident-attachments

    Gets incident attachments.

  • rs-incidents-get-members

    Gets members of the incident.

  • rs-incidents-get-tasks

    Gets tasks of incidents.

  • rs-incidents-update-member

    Updates the incident's members.

  • rs-list-incident-notes

    Gets all of the top-level comments for an incident.

  • rs-list-scripts

    Retrieves the specified script's information or a list of all organization's scripts.

  • rs-list-task-instructions

    Get the task's instructions.

  • rs-list-tasks

    Gets an array of open tasks to which the current user is assigned.

  • rs-related-incidents

    Gets related incidents.

  • rs-search-incidents

    Query for incidents.

  • rs-update-incident

    Updates incidents.

  • rs-update-incident-note

    Updates an incident's note.

  • rs-update-task

    Update an incident's task fields.

  • rs-upload-incident-attachment

    Upload an attachment for an incident.

  • update-remote-system

    Updates the remote incident with local incident changes. This method is only used for debugging purposes and will not update the current incident.

import json
from io import BytesIO

import demistomock as demisto
import pytest
import requests
from CommonServerPython import DemistoException
from requests import Session

DEFAULT_MAX_FETCH = 1000
TAG_TO_IBM = "FROM XSOAR"


def dict_to_response(data, status=200):
    response = requests.Response()
    response.status_code = status
    # Convert dictionary to bytes and set as content
    response.raw = BytesIO(json.dumps(data).encode("utf-8"))

    response.headers["Content-Type"] = "application/json"
    return response


def load_test_data(json_path):
    with open(json_path) as f:
        return json.load(f)


class MockClient:
    @staticmethod
    def get(incident_id):
        return {
            "name": "The old name",
            "description": {"format": "html", "content": "The old description"},
            "owner_id": 1,
            "discovered_date": 1624782898000,
            "confirmed": "true",
        }

    @staticmethod
    def post(url, body):
        return url, body

    @staticmethod
    def patch(url, body):
        return url, body


@pytest.fixture
def _mocker(mocker):
    mocker.patch.object(
        demisto, "params", return_value={"server": "example.com:80", "org": "example", "proxy": True, "close_ibm_incident": True}
    )
    return mocker


def test_update_incident_command_with_invalid_json(_mocker):
    """
    Given:
     - An incident should be updated.

    When:
     - Running update_incident_command function with other-fields argument, the other-field is an invalid json.

    Then:
     - Ensure the parsing before the request fails and returns a JSONDecodeError.
    """
    args = {"incident-id": "1234", "other-fields": "Invalid json"}
    from IBMResilientSystems import update_incident_command

    with pytest.raises(Exception) as exception:
        update_incident_command(MockClient, args)
    assert "The other_fields argument is not a valid json." in exception.value.args[0]


def test_add_note(_mocker):
    """
    Given:
     - An incident that should be updated with a note.

    When:
     - Running add_note_command function.

    Then:
     - Ensure the function runs as expected.
    """
    mock_result = _mocker.patch.object(MockClient, "post")
    expected_result = ("/incidents/1234/comments", {"text": {"format": "text", "content": f"This is a new note\n{TAG_TO_IBM}"}})
    from IBMResilientSystems import add_note_command

    output = add_note_command(MockClient, "1234", "This is a new note", tag_to_ibm=TAG_TO_IBM)

    assert mock_result.call_args.args == expected_result
    assert "1234" in output.readable_output


def test_add_incident_artifact(_mocker):
    """
    Given:
     - An incident should be updated with an artifact.

    When:
     - Running add_artifact_command function.

    Then:
     - Ensure the function runs as expected.
    """
    mock_result = _mocker.patch.object(MockClient, "post")
    expected_result = (
        "/incidents/1234/artifacts",
        {
            "type": "IP Address",
            "value": "1.1.1.1",
            "description": {"format": "text", "content": "This is the artifact description"},
        },
    )
    from IBMResilientSystems import add_artifact_command

    output = add_artifact_command(MockClient, "1234", "IP Address", "1.1.1.1", "This is the artifact description")

    assert mock_result.call_args.args == expected_result
    assert "1234" in output.get("HumanReadable")


def test_test_module(_mocker):
    """
    Tests whether the test module returns expected result for default http response.
    """

    from IBMResilientSystems import SimpleClient, test_module

    client = SimpleClient()
    _mocker.patch.object(client, "get", return_value={})
    assert test_module(client, "2024-01-01T00:00:00Z") == "ok"


@pytest.mark.parametrize(
    "fetch_time, expected_result",
    [
        ("2024-01-01T00:00:00Z", "ok"),
        ("2024-01-01T00:00:00", "ok"),
        ("", "ok"),
        ("2024/01/01 00:00:00", "fail"),
        ("invalid-date", "fail"),
    ],
)
def test_test_module_fetch_time(fetch_time, expected_result, _mocker):
    """
    Tests whether the test module returns expected result for valid and invalid responses.
    """

    from IBMResilientSystems import SimpleClient, test_module, validate_iso_time_format

    client = SimpleClient()
    _mocker.patch.object(client, "get", return_value={})
    fetch_time = validate_iso_time_format(fetch_time)
    if expected_result == "fail":
        with pytest.raises(DemistoException):
            test_module(client, fetch_time)
    else:
        assert test_module(client, fetch_time) == expected_result


@pytest.mark.parametrize(
    "args, expected",
    [
        (
            {},
            {
                "filters": [{"conditions": []}],
                "sorts": [{"field_name": "create_date", "type": "asc"}],
                "length": DEFAULT_MAX_FETCH,
            },
        ),  # Test without any filters or pagination params
        (
            {"severity": "Low"},
            {
                "filters": [{"conditions": [{"field_name": "severity_code", "method": "in", "value": [50]}]}],
                "sorts": [{"field_name": "create_date", "type": "asc"}],
                "length": DEFAULT_MAX_FETCH,
            },
        ),
        (
            {"date-created-before": "2022-01-01T10:00:00Z"},
            {
                "filters": [{"conditions": [{"field_name": "create_date", "method": "lte", "value": 1641031200000}]}],
                "sorts": [{"field_name": "create_date", "type": "asc"}],
                "length": DEFAULT_MAX_FETCH,
            },
        ),
        (
            {"page": 1, "page_size": 10, "last-modified-after": "2022-01-01T10:00:00Z"},
            {
                "filters": [{"conditions": [{"field_name": "inc_last_modified_date", "method": "gte", "value": 1641031200000}]}],
                "sorts": [{"field_name": "create_date", "type": "asc"}],
                "start": 0,
                "length": 10,
            },
        ),
    ],
    ids=["no-filters-query", "args-1-query", "args-2-query", "pagination-params-query"],
)
def test_prepare_search_query_data(_mocker, args, expected):
    from IBMResilientSystems import prepare_search_query_data

    assert prepare_search_query_data(args) == expected


@pytest.mark.parametrize(
    "input_notes, expected_output",
    [
        (
            [
                {
                    "type": "incident",
                    "id": 0,
                    "parent_id": None,
                    "user_id": 0,
                    "user_fname": "Demisto",
                    "user_lname": "Resilient",
                    "text": "insecure?",
                    "create_date": 1722424268280,
                    "modify_date": 1722424268280,
                    "children": [],
                    "mentioned_users": [],
                    "is_deleted": False,
                    "modify_user": {"id": 0, "first_name": "Demisto", "last_name": "Resilient"},
                    "actions": [],
                    "inc_id": 2222,
                    "inc_name": "inci-11",
                    "task_id": None,
                    "task_name": None,
                    "task_custom": None,
                    "task_members": None,
                    "task_at_id": None,
                    "inc_owner": 0,
                    "user_name": "Demisto Resilient",
                    "modify_principal": {"id": 0, "type": "user", "name": "demist", "display_name": "Demisto Resilient"},
                    "comment_perms": {"update": True, "delete": True},
                }
            ],
            [
                {
                    "create_date": "2024-07-31T11:11:08Z",
                    "created_by": "Demisto Resilient",
                    "id": 0,
                    "modify_date": 1722424268280,
                    "text": "insecure?",
                }
            ],
        ),
        (
            [{"id": 2, "text": " ", "create_date": 1722424253387}],
            [{"create_date": "2024-07-31T11:10:53Z", "created_by": " ", "id": 2, "modify_date": None, "text": " "}],
        ),
    ],
)
def test_prettify_incident_notes(_mocker, input_notes, expected_output):
    from IBMResilientSystems import prettify_incident_notes

    assert prettify_incident_notes(input_notes) == expected_output


@pytest.mark.parametrize(
    "incidents, expected_output",
    [
        ([], "No results found."),
    ],
)
def test_search_incidents_command(_mocker, incidents, expected_output):
    from IBMResilientSystems import SimpleClient, search_incidents_command

    client = SimpleClient()
    _mocker.patch("IBMResilientSystems.search_incidents", return_value=incidents)

    assert search_incidents_command(client=client, args={}) == expected_output


@pytest.mark.parametrize(
    "args",
    [
        ({"date-created-after": 1577865600000, "limit": "1000", "page": "1", "page_size": "10"}),
    ],
)
def test_search_incidents(_mocker, args):
    from IBMResilientSystems import DEFAULT_RETURN_LEVEL, SimpleClient, search_incidents

    test_dict_response = load_test_data("./test_data/test_search_incidents_response.json")
    test_response = dict_to_response(test_dict_response)
    request = _mocker.patch.object(Session, "post", return_value=test_response)
    client = SimpleClient()
    client.org_id = 0
    search_incidents(client=client, args=args)

    request_url = request.call_args.args[0]
    request_headers = request.call_args.kwargs["headers"]
    request_data = request.call_args.kwargs["data"]

    assert request_url.endswith(
        f"/rest/orgs/0/incidents/query_paged?text_content_output_format=objects_convert_text&return_level="
        f"{args.get('return_level', DEFAULT_RETURN_LEVEL)}"
    )
    assert request_headers["content-type"] == "application/json"
    assert request_data == (
        '{"filters": [{"conditions": [{"field_name": "create_date", "method": "gte", "value": 1577865600000}]}],'
        ' "sorts": [{"field_name": "create_date", "type": "asc"}], "length": 10, "start": 0}'
    )


@pytest.mark.parametrize(
    "args, processed_payload",
    [
        (
            {
                "incident-id": 0000,
                "severity": "Low",
                "incident-type": "Malware",
                "nist": "Attrition",
                "resolution": "NotAnIssue",
                "resolution-summary": "This is a test incident.",
                "description": "Test incident",
                "name": "incident-0000",
            },
            {
                "changes": [
                    {"field": "severity_code", "old_value": {"id": 6}, "new_value": {"id": 4}},
                    {
                        "field": "incident_type_ids",
                        "old_value": {"ids": [21, 19, 17, 6]},
                        "new_value": {"ids": [21, 19, 17, 6, 19]},
                    },
                    {"field": "nist_attack_vectors", "old_value": {"ids": [4, 2]}, "new_value": {"ids": [4, 2, 2]}},
                    {"field": "resolution_id", "old_value": {"id": 9}, "new_value": {"id": 9}},
                    {
                        "field": "resolution_summary",
                        "old_value": {"textarea": {"format": "html", "content": "This is a test incident."}},
                        "new_value": {"textarea": {"format": "html", "content": "This is a test incident."}},
                    },
                    {
                        "field": "description",
                        "old_value": {"textarea": {"format": "html", "content": "1111 2222 3333"}},
                        "new_value": {"textarea": {"format": "html", "content": "Test incident"}},
                    },
                    {"field": "name", "old_value": {"text": "incident_name"}, "new_value": {"text": "incident-0000"}},
                ]
            },
        ),
    ],
)
def test_update_incident_command(_mocker, args, processed_payload):
    from IBMResilientSystems import SimpleClient, update_incident_command

    client = SimpleClient()
    client.org_id = 0
    _mocker.patch.object(
        Session, "get", return_value=dict_to_response(load_test_data("./test_data/test_get_incident_response.json"))
    )

    request = _mocker.patch.object(
        Session, "patch", return_value=dict_to_response({"success": True, "title": None, "message": None, "hints": []})
    )
    update_incident_command(client, args)
    assert request.call_args.args[0].endswith(f"/rest/orgs/{client.org_id}/incidents/{args['incident-id']}")
    assert json.loads(request.call_args[1]["data"]) == processed_payload


def test_update_incident(_mocker):
    from IBMResilientSystems import SimpleClient, update_incident

    request = _mocker.patch.object(
        Session, "patch", return_value=dict_to_response({"success": True, "title": None, "message": None, "hints": []})
    )

    client = SimpleClient()
    client.org_id = 0

    update_incident(client, incident_id="0000", data={})
    assert request.call_args.args[0].endswith("/rest/orgs/0/incidents/0000")
    assert request.call_args[1]["data"] == "{}"


@pytest.mark.parametrize(
    "incident_id, expected_human_readable",
    [
        (
            "1000",
            "### IBM QRadar SOAR incident ID 1000\n|Id|Name|Description|NistAttackVectors|Phase|Resolution|ResolutionSummary|Owner"  # noqa: E501
            "|CreatedDate|DateOccurred|DiscoveredDate|DueDate|NegativePr|Confirmed|ExposureType|Severity|Reporter|\n"
            "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n| 1000 | incident_name | 1111 2222 3333 | "
            "E-mail<br>Attrition<br> |  | Not an Issue | This is a test incident. |  | 2024-07-29T11:32:36Z |  | 2024-07-29T11:31:57Z "  # noqa: E501
            "|  | true | true | ExternalParty | 6 |  |",
        ),
    ],
)
def test_get_incident_command(_mocker, incident_id, expected_human_readable):
    from IBMResilientSystems import SimpleClient, get_incident_command

    client = SimpleClient()
    client.org_id = 0
    _mocker.patch("IBMResilientSystems.get_users", return_value=[])
    _mocker.patch("IBMResilientSystems.get_phases", return_value={})
    _mocker.patch.object(
        Session, "get", return_value=dict_to_response(load_test_data("./test_data/test_get_incident_response.json"))
    )

    context_entry = get_incident_command(client, incident_id)
    assert context_entry["HumanReadable"].strip() == expected_human_readable


@pytest.mark.parametrize(
    "script_id, expected_outputs, expected_readable_output",
    [
        (
            "1",
            [
                {
                    "id": 1,
                    "name": "Sample script: process inbound email (v35)",
                    "description": "This script processes inbound emails.",
                    "language": "python",
                    "object_type": 13,
                    "uuid": "0000-0000-0000-0000-0000",
                    "actions": [],
                    "tags": [],
                    "script_text": "...",
                    "creator_id": 0,
                    "last_modified_by": 0,
                    "last_modified_time": 1600698818789,
                }
            ],
            """### example Scripts
|id|name|description|language|
|---|---|---|---|
| 1 | Sample script: process inbound email (v35) | This script processes inbound emails. | python |
""",
        ),
        (
            "",
            [
                {
                    "id": 1,
                    "name": "Sample script: process inbound email (v35)",
                    "description": "This script processes inbound emails.",
                    "language": "python",
                    "object_type": 13,
                    "uuid": "0000-0000-0000-0000-0000",
                    "actions": [],
                    "tags": [],
                },
                {
                    "id": 3,
                    "name": "test-script",
                    "description": "Testing",
                    "language": "python",
                    "object_type": 0,
                    "uuid": "0000-0000-0000-0000-0000",
                    "actions": [],
                    "tags": [],
                },
                {
                    "id": 4,
                    "name": "test-script-2",
                    "description": "Testing 2",
                    "language": "python",
                    "object_type": 13,
                    "uuid": "0000-0000-0000-0000-0000",
                    "actions": [],
                    "tags": [],
                },
            ],
            """### example Scripts
|id|name|description|language|
|---|---|---|---|
| 1 | Sample script: process inbound email (v35) | This script processes inbound emails. | python |
| 3 | test-script | Testing | python |
| 4 | test-script-2 | Testing 2 | python |
""",
        ),
        (
            "2",
            [
                {
                    "error_code": "generic",
                    "hints": [],
                    "message": "Unable to find object with ID 2",
                    "success": False,
                    "title": None,
                }
            ],
            """### example Scripts
|id|name|description|language|
|---|---|---|---|
|  |  |  |  |
""",
        ),
    ],
)
def test_list_scripts_command(_mocker, script_id: str, expected_outputs: list, expected_readable_output: str):
    from os import path

    from IBMResilientSystems import SimpleClient, list_scripts_command

    def side_effect(url: str):
        _script_id = url.split("/")[-1]
        if _script_id:  # Return enriched script data for a specific script ID
            response_path = f"./test_data/test_get_script_{_script_id}_response.json"
            if not path.exists(response_path):
                response_path = "./test_data/test_get_script_fail_response.json"
        else:
            response_path = "./test_data/test_get_all_scripts_response.json"

        return load_test_data(response_path)

    client = SimpleClient()
    client.org_id = 0
    args = {"script_id": script_id}

    _mocker.patch.object(SimpleClient, "get", side_effect=side_effect)

    command_result = list_scripts_command(client, args)
    assert command_result.readable_output == expected_readable_output
    assert command_result.outputs == expected_outputs


@pytest.mark.parametrize("file_entry_id", ["ENTRY_ID"])
def test_upload_incident_attachment(_mocker, file_entry_id: str):
    from IBMResilientSystems import SimpleClient, upload_incident_attachment_command

    client = SimpleClient()
    client.org_id = 0
    response = {"status_code": 200}
    expected_output = "File was uploaded successfully to 1000."

    def mock_get_file_path(entry_id):  # noqa: F811
        if entry_id == "ENTRY_ID":
            return {"path": "/path/to/file", "name": "filename.txt"}
        return None

    _mocker.patch.object(demisto, "getFilePath", side_effect=mock_get_file_path)
    post_attachment_request = _mocker.patch.object(SimpleClient, "post_attachment", return_value=response)

    args = {"entry_id": file_entry_id, "incident_id": 1000}
    result = upload_incident_attachment_command(SimpleClient(), args, tag_to_ibm="FROM XSOAR")

    assert result.readable_output == expected_output
    post_attachment_request.assert_called_once_with(
        uri=f"/incidents/{args['incident_id']}/attachments", filepath="/path/to/file", filename=f"filename_{TAG_TO_IBM}.txt"
    )


def test_delete_incidents_command(_mocker):
    from IBMResilientSystems import SimpleClient, delete_incidents_command

    client = SimpleClient()
    client.org_id = 0

    delete_incident_request = _mocker.patch.object(
        SimpleClient, "put", return_value={"success": True, "title": None, "message": None, "hints": []}
    )

    incident_ids = ["1001", "1002"]
    delete_incidents_command(client, args={"incident_ids": ",".join(incident_ids)})

    delete_incident_request.assert_called_once_with("/incidents/delete", payload=incident_ids)


def test_list_incident_notes_command(_mocker):
    from IBMResilientSystems import SimpleClient, list_incident_notes_command

    client = SimpleClient()
    client.org_id = 0

    get_incident_notes_request = _mocker.patch.object(
        SimpleClient, "get", return_value=load_test_data("./test_data/test_get_incident_notes_reponse.json")
    )
    list_incident_notes_command(client, {"incident_id": "2000"})

    get_incident_notes_request.assert_called_once_with("/incidents/2000/comments?text_content_output_format=objects_convert_text")


def test_update_incident_note(_mocker):
    from IBMResilientSystems import SimpleClient, update_incident_note_command

    client = SimpleClient()
    client.org_id = 0

    update_incident_note_request = _mocker.patch.object(SimpleClient, "put", return_value={})
    update_incident_note_command(client, args={"incident_id": 2000, "note_id": 1, "note": "NOTE_BODY"})

    update_incident_note_request.assert_called_once_with(
        "/incidents/2000/comments/1", payload={"text": {"format": "text", "content": "NOTE_BODY"}}
    )


@pytest.mark.parametrize(
    "args, expected_task_dto",
    [
        (
            {
                "incident_id": "2000",
                "name": "TASK-1",
                "owner_id": "0",
                "description": "TASK",
                "instructions": "INSTRUCTIONS",
                "phase": "Initial",
                "due_date": "2023-04-01T12:00:00.000Z",
            },
            {
                "name": "TASK-1",
                "phase_id": {"name": "Initial"},
                "description": "TASK",
                "due_date": 1680350400000,
                "instructions": "INSTRUCTIONS",
                "owner_id": 0,
            },
        ),
        (  # Task without Instructions
            {
                "incident_id": "2001",
                "name": "TASK-2",
                "owner_id": "1",
                "description": "TASK 2",
                "instructions": "",
                "phase": "Custom",
                "due_date": "2023-05-01T12:00:00.000Z",
            },
            {"name": "TASK-2", "phase_id": {"name": "Custom"}, "description": "TASK 2", "due_date": 1682942400000, "owner_id": 1},
        ),
        (  # Invalid Owner ID
            {
                "incident_id": "2003",
                "name": "TASK-3",
                "owner_id": "abcd",
                "description": "TASK 3",
                "instructions": "TASK 3 Instructions",
                "phase": "Initial",
                "due_date": "2023-07-01T12:00:00.000Z",
            },
            DemistoException("Owner ID must be an integer number."),
        ),
        (  # Task without Due Date
            {
                "incident_id": "2004",
                "name": "TASK-4",
                "owner_id": "3",
                "description": "TASK 4",
                "instructions": "TASK 4 Instructions",
                "phase": "Initial",
                "due_date": "",
            },
            {
                "name": "TASK-4",
                "phase_id": {"name": "Initial"},
                "description": "TASK 4",
                "instructions": "TASK 4 Instructions",
                "owner_id": 3,
            },
        ),
    ],
)
def test_add_custom_task_command(_mocker, args, expected_task_dto):
    from IBMResilientSystems import SimpleClient, add_custom_task_command

    client = SimpleClient()
    client.org_id = 0

    def post_side_effect(uri, payload):
        if isinstance(expected_task_dto, Exception):
            raise expected_task_dto
        assert uri == f"/incidents/{args['incident_id']}/tasks"
        assert payload == expected_task_dto
        return {"id": "1234"}

    add_custom_task_request = _mocker.patch.object(SimpleClient, "post", side_effect=post_side_effect)

    if isinstance(expected_task_dto, Exception):
        with pytest.raises(DemistoException, match="Owner ID must be an integer number."):
            add_custom_task_command(client, args=args)
    else:
        result = add_custom_task_command(client, args=args)
        add_custom_task_request.assert_called_once_with(uri=f"/incidents/{args['incident_id']}/tasks", payload=expected_task_dto)
        assert (
            result.readable_output == f"Successfully created new task for incident with ID {args['incident_id']}. Task ID: 1234"
        )


def test_list_tasks_command(_mocker):
    from IBMResilientSystems import SimpleClient, list_tasks_command

    client = SimpleClient()
    client.org_id = 0

    get_tasks_request = _mocker.patch.object(SimpleClient, "get", return_value={})
    list_tasks_command(client)
    get_tasks_request.assert_called_with("/tasks")


def test_get_task_members_command(_mocker):
    from IBMResilientSystems import SimpleClient, get_task_members_command

    client = SimpleClient()
    client.org_id = 0
    task_id = "1234"
    get_task_members_request = _mocker.patch.object(SimpleClient, "get", return_value={})
    get_task_members_command(client, args={"task_id": task_id})

    get_task_members_request.assert_called_with(f"/tasks/{task_id}/members")


@pytest.mark.parametrize("task_ids, should_raise_exception", [("1000", False), ("2000,3000", False), ("", True)])
def test_delete_tasks_command(_mocker, task_ids, should_raise_exception):
    from IBMResilientSystems import SimpleClient, delete_tasks_command

    client = SimpleClient()
    client.org_id = 0

    delete_tasks_request = _mocker.patch.object(
        SimpleClient, "put", return_value={"success": True, "title": None, "message": None, "hints": []}
    )

    if should_raise_exception:
        with pytest.raises(DemistoException):
            delete_tasks_command(client, args={"task_ids": task_ids})
    else:
        result = delete_tasks_command(client, args={"task_ids": task_ids})
        task_id_list = task_ids.split(",")
        delete_tasks_request.assert_called_once_with("/tasks/delete", payload=task_id_list)
        assert result.readable_output == f"Tasks with IDs {task_id_list} were deleted successfully."


def test_delete_task_members_command(_mocker):
    from IBMResilientSystems import SimpleClient, delete_task_members_command

    client = SimpleClient()
    client.org_id = 0

    task_id = "1234"
    mock_response = {"content": "Members deleted successfully"}

    delete_task_members_request = _mocker.patch.object(SimpleClient, "delete", return_value=mock_response)
    delete_task_members_command(client, args={"task_id": task_id})

    delete_task_members_request.assert_called_once_with(f"/tasks/{task_id}/members")


def test_list_task_instructions_command(_mocker):
    from IBMResilientSystems import SimpleClient, list_task_instructions_command

    client = SimpleClient()
    client.org_id = 0

    task_id = "5678"
    mock_response = {"text": {"content": "These are the instructions for the task.", "format": "text"}}

    get_task_instructions_request = _mocker.patch.object(SimpleClient, "get", return_value=mock_response)

    list_task_instructions_command(client, args={"task_id": task_id})

    get_task_instructions_request.assert_called_once_with(
        f"/tasks/{task_id}/instructions_ex?text_content_output_format=objects_convert_text"
    )


def test_get_attachment_command(_mocker):
    from IBMResilientSystems import SimpleClient, get_attachment_command
    from requests import Response

    client = SimpleClient()
    client.org_id = 0

    def side_effect(url: str, get_response_object=False):
        if url.endswith("/contents"):  # File content request
            response = Response()
            response.__setattr__("_content", b"test file content")  # Note: Bytes for content
            response.status_code = 200
            return response
        # File metadata response
        else:
            return {
                "type": "incident",
                "id": 1,
                "uuid": "0000-0000-0000-0000",
                "name": "test-test-test.txt",
                "content_type": "text/plain",
                "created": 1725880565507,
                "creator_id": 1,
                "size": 6,
                "actions": [],
                "playbooks": [],
                "task_id": None,
                "task_name": None,
                "task_custom": None,
                "task_members": None,
                "task_at_id": None,
                "reconciliation_status": "matched",
                "vers": 8,
                "inc_id": 2000,
                "inc_name": "INCIDENT-1",
                "inc_owner": 0,
            }

    get_attachment_request = _mocker.patch.object(SimpleClient, "get", side_effect=side_effect)

    args = {"incident_id": "1000", "attachment_id": "1"}
    get_attachment_command(client, args)

    get_attachment_endpoint = f'/incidents/{args.get("incident_id")}/attachments/{args.get("attachment_id")}'
    get_attachment_contents_endpoint = get_attachment_endpoint + "/contents"

    # Check the calls made to the mock
    get_attachment_request.assert_has_calls(
        [_mocker.call(get_attachment_endpoint), _mocker.call(get_attachment_contents_endpoint, get_response_object=True)]
    )


def test_get_modified_remote_data_command(_mocker):
    from CommonServerPython import GetModifiedRemoteDataResponse
    from IBMResilientSystems import SimpleClient, get_modified_remote_data_command

    client = SimpleClient()
    client.org_id = 0

    mock_search_incidents = _mocker.patch(
        "IBMResilientSystems.search_incidents",
        return_value=[
            {"id": 1000, "last_modified_time": "2023-09-01T12:01:00Z"},
            {"id": 1001, "last_modified_time": "2023-09-01T12:02:00Z"},
        ],
    )
    expected_output = GetModifiedRemoteDataResponse(["1000", "1001"])

    last_update = "2023-09-01T12:00:00Z"
    args = {"lastUpdate": last_update}
    result = get_modified_remote_data_command(client, args)

    mock_search_incidents.assert_called_once_with(client, args={"last-modified-after": last_update})
    assert result.modified_incident_ids == expected_output.modified_incident_ids


def test_get_remote_data_command(_mocker):
    from IBMResilientSystems import SimpleClient, get_remote_data_command

    # Mock client and its methods
    client = SimpleClient()
    client.org_id = 0

    incident_id = "1000"
    last_update = "2024-01-01T00:00:00Z"

    # Mock incoming arguments
    args = {"id": incident_id, "lastUpdate": last_update}

    # Mock the get_incident and process_raw_incident function behavior
    mock_incident_data = {
        "plan_status": "A",  # 'A' stands for Active
        "end_date": None,
        "notes": [{"modify_date": 1725880565507, "text": {"content": "Note content"}, "created_by": "User 1"}],
        "attachments": [{"ID": "1", "Create Time": 1725880565507, "Name": "Attachment1"}],
    }

    _mocker.patch("IBMResilientSystems.get_incident", return_value=mock_incident_data)
    _mocker.patch("IBMResilientSystems.process_raw_incident", return_value=mock_incident_data)

    # Mock get_attachment and handle_incoming_incident_resolution
    _mocker.patch("IBMResilientSystems.get_attachment", return_value=("filename.txt", b"file content"))
    _mocker.patch("IBMResilientSystems.handle_incoming_incident_resolution", return_value={"Contents": "Incident resolved"})
    # Call the command and capture the result
    result = get_remote_data_command(client, args, tag_to_ibm="FROM ", tag_from_ibm="TO ")

    # Check if the result contains the expected mirrored data and entries
    assert len(result.entries) == 3  # A note, a file, and a reopen entry.
    assert "Note content" in result.entries[0].get("Contents")
    assert "filename.txt" in result.entries[1].get("File")
    assert result.mirrored_object


def test_update_remote_system_command_no_changes(_mocker):
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {"remoteId": "1000", "incidentChanged": False, "entries": [], "delta": None, "data": {}, "incStatus": "Active"}

    debug_mock = _mocker.patch.object(demisto, "debug")
    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1000"
    debug_mock.assert_called_with("Skipping updating remote incident fields [1000] as it is not new nor changed")


def test_update_remote_system_command_with_changes(_mocker):
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {
        "remoteId": "1001",
        "incidentChanged": True,
        "entries": [],
        "delta": {"name": "Updated Incident Name"},
        "data": {},
        "incStatus": "Active",
    }

    prepare_mock = _mocker.patch(
        "IBMResilientSystems.prepare_incident_update_dto_for_mirror", return_value={"name": "Updated Incident Name"}
    )
    update_mock = _mocker.patch("IBMResilientSystems.update_incident")

    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1001"
    prepare_mock.assert_called_once_with(client, "1001", {"name": "Updated Incident Name"})
    update_mock.assert_called_once_with(client, "1001", {"name": "Updated Incident Name"})


def test_update_remote_system_command_with_note(_mocker):
    from CommonServerPython import EntryType
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {
        "remoteId": "1002",
        "incidentChanged": False,
        "entries": [{"id": "1", "type": EntryType.NOTE, "tags": ["FROM XSOAR"], "Contents": "Test note"}],
        "delta": None,
        "data": {},
        "incStatus": "Active",
    }

    add_note_mock = _mocker.patch("IBMResilientSystems.add_note")

    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1002"
    add_note_mock.assert_called_once_with(client, "1002", "Test note")


def test_update_remote_system_command_with_file(_mocker):
    from CommonServerPython import EntryType
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {
        "remoteId": "1003",
        "incidentChanged": False,
        "entries": [{"id": "2", "type": EntryType.FILE, "tags": ["FROM XSOAR"], "Contents": "file content"}],
        "delta": None,
        "data": {},
        "incStatus": "Active",
    }

    upload_mock = _mocker.patch("IBMResilientSystems.upload_incident_attachment")

    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1003"
    upload_mock.assert_called_once_with(client, "1003", "2", "FROM XSOAR")


def test_update_remote_system_command_with_multiple_entries(_mocker):
    from CommonServerPython import EntryType
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {
        "remoteId": "1004",
        "incidentChanged": True,
        "entries": [
            {"id": "3", "type": EntryType.NOTE, "tags": ["FROM XSOAR"], "Contents": "Test note 1"},
            {"id": "4", "type": EntryType.FILE, "tags": ["FROM XSOAR"], "Contents": "file content"},
            {"id": "5", "type": EntryType.NOTE, "tags": ["FROM XSOAR"], "Contents": "Test note 2"},
        ],
        "delta": {"description": "Updated description"},
        "data": {},
        "incStatus": "Active",
    }

    prepare_mock = _mocker.patch(
        "IBMResilientSystems.prepare_incident_update_dto_for_mirror", return_value={"description": "Updated description"}
    )
    update_mock = _mocker.patch("IBMResilientSystems.update_incident")
    add_note_mock = _mocker.patch("IBMResilientSystems.add_note")
    upload_mock = _mocker.patch("IBMResilientSystems.upload_incident_attachment")

    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1004"
    prepare_mock.assert_called_once_with(client, "1004", {"description": "Updated description"})
    update_mock.assert_called_once_with(client, "1004", {"description": "Updated description"})
    assert add_note_mock.call_count == 2
    add_note_mock.assert_any_call(client, "1004", "Test note 1")
    add_note_mock.assert_any_call(client, "1004", "Test note 2")
    upload_mock.assert_called_once_with(client, "1004", "4", "FROM XSOAR")


def test_update_remote_system_command_with_untagged_entries(_mocker):
    from CommonServerPython import EntryType
    from IBMResilientSystems import SimpleClient, update_remote_system_command

    client = SimpleClient()
    args = {
        "remoteId": "1005",
        "incidentChanged": False,
        "entries": [
            {"id": "6", "type": EntryType.NOTE, "tags": [], "Contents": "Untagged note"},
            {"id": "7", "type": EntryType.FILE, "tags": [], "Contents": "Untagged file"},
        ],
        "delta": None,
        "data": {},
        "incStatus": "Active",
    }

    add_note_mock = _mocker.patch("IBMResilientSystems.add_note")
    upload_mock = _mocker.patch("IBMResilientSystems.upload_incident_attachment")

    result = update_remote_system_command(client, args, tag_to_ibm="FROM XSOAR")

    assert result == "1005"
    add_note_mock.assert_not_called()
    upload_mock.assert_not_called()


def test_get_mapping_fields_command(_mocker):
    from CommonServerPython import GetMappingFieldsResponse
    from IBMResilientSystems import IBM_QRADAR_INCIDENT_FIELDS, IBM_QRADAR_SOAR_INCIDENT_SCHEMA_NAME, get_mapping_fields_command

    response = get_mapping_fields_command()
    assert isinstance(response, GetMappingFieldsResponse)

    # Get the mapping scheme from the response
    scheme = response.scheme_types_mappings[0]
    # Assert that the scheme has the correct incident schema name
    assert scheme.type_name == IBM_QRADAR_SOAR_INCIDENT_SCHEMA_NAME

    # Assert that the scheme contains the correct fields
    for field_name, _field_data in IBM_QRADAR_INCIDENT_FIELDS.items():
        assert field_name in scheme.fields


@pytest.mark.parametrize(
    "last_run, first_fetch_time, expected_args, expected_last_run",
    [
        (None, "2023-01-01T00:00:00Z", {"date-created-after": 1672531200000}, 1672531200001),
        ({"time": 1672531200000}, "2023-01-01T00:00:00Z", {"date-created-after": 1672531200000}, 1672531200001),
    ],
)
def test_fetch_incidents(_mocker, last_run, first_fetch_time, expected_args, expected_last_run):
    from IBMResilientSystems import SimpleClient, fetch_incidents

    mock_search_incidents = _mocker.patch("IBMResilientSystems.search_incidents", return_value=[])
    mock_set_last_run = _mocker.patch.object(demisto, "setLastRun", return_value=None)

    client = SimpleClient()
    client.org_id = 0
    fetch_incidents(client, first_fetch_time, fetch_closed=True)

    mock_search_incidents.assert_called_once_with(client, {"date-created-after": expected_args["date-created-after"]})
    mock_set_last_run.assert_called_once_with({"time": expected_last_run})


def test_to_timestamp_with_integer(_mocker):
    from IBMResilientSystems import to_timestamp

    assert to_timestamp(1641024000000) == 1641024000000


def test_to_timestamp_with_string_timestamp(_mocker):
    from IBMResilientSystems import to_timestamp

    assert to_timestamp("1641024000000") == 1641024000000


def test_to_timestamp_with_string_date(_mocker):
    from IBMResilientSystems import to_timestamp

    assert to_timestamp("2022-01-01T12:00:00Z") == 1641038400000


def test_to_timestamp_with_invalid_string(_mocker):
    import pytest
    from IBMResilientSystems import to_timestamp

    with pytest.raises(ValueError):
        to_timestamp("INVALID_DATE_STRING")


def test_validate_iso_time_format_with_milliseconds(_mocker):
    from IBMResilientSystems import validate_iso_time_format

    input_time = "2023-01-01T12:30:45.123456Z"
    expected_output = "2023-01-01T12:30:45Z"
    assert validate_iso_time_format(input_time) == expected_output


def test_validate_iso_time_format_without_z(_mocker):
    from IBMResilientSystems import validate_iso_time_format

    input_time = "2023-01-01T12:30:45"
    expected_output = "2023-01-01T12:30:45Z"
    assert validate_iso_time_format(input_time) == expected_output


def test_validate_iso_time_format_with_z(_mocker):
    from IBMResilientSystems import validate_iso_time_format

    input_time = "2023-01-01T12:30:45Z"
    expected_output = "2023-01-01T12:30:45Z"
    assert validate_iso_time_format(input_time) == expected_output


def test_update_task_command_multiple_fields(_mocker):
    from IBMResilientSystems import SimpleClient, update_task_command

    client = SimpleClient()
    args = {
        "task_id": "5678",
        "name": "Complex Task",
        "owner_id": "10",
        "due_date": "2023-12-31T23:59:59Z",
        "phase": "Engage",
        "instructions": "Investigate thoroughly",
        "status": "Open",
    }

    update_task_mock = _mocker.patch("IBMResilientSystems.update_task")
    _mocker.patch("IBMResilientSystems.to_timestamp", return_value=1704067199000)

    result = update_task_command(client, args)

    expected_dto = {
        "name": "Complex Task",
        "inc_owner_id": 10,
        "due_date": 1704067199000,
        "phase_id": "Engage",
        "instructions": "Investigate thoroughly",
        "status": "O",
    }
    update_task_mock.assert_called_once_with(client, "5678", expected_dto)
    assert result.readable_output == "Task 5678 updated successfully."


def test_update_task_command_completed_status(_mocker):
    from IBMResilientSystems import SimpleClient, update_task_command

    client = SimpleClient()
    args = {"task_id": "9012", "status": "Completed"}

    update_task_mock = _mocker.patch("IBMResilientSystems.update_task")

    result = update_task_command(client, args)

    update_task_mock.assert_called_once_with(client, "9012", {"status": "C"})
    assert result.readable_output == "Task 9012 updated successfully."


def test_update_task_command_invalid_status(_mocker):
    from IBMResilientSystems import SimpleClient, update_task_command

    client = SimpleClient()
    args = {"task_id": "3456", "status": "Invalid"}

    update_task_mock = _mocker.patch("IBMResilientSystems.update_task")

    result = update_task_command(client, args)

    update_task_mock.assert_called_once_with(client, "3456", {})
    assert result.readable_output == "Task 3456 updated successfully."


def test_update_task_command_empty_args(_mocker):
    from IBMResilientSystems import SimpleClient, update_task_command

    client = SimpleClient()
    args = {"task_id": "7890"}

    update_task_mock = _mocker.patch("IBMResilientSystems.update_task")

    result = update_task_command(client, args)

    update_task_mock.assert_called_once_with(client, "7890", {})
    assert result.readable_output == "Task 7890 updated successfully."


def test_process_raw_incident(_mocker):
    _mocker.patch.object(
        demisto,
        "params",
        return_value={"server": "example.com:80", "org": "example", "proxy": True, "fetch_tasks": True, "fetch_notes": True},
    )

    from IBMResilientSystems import SimpleClient, process_raw_incident

    client = SimpleClient()

    _mocker.patch("IBMResilientSystems.get_tasks", return_value=[])
    _mocker.patch("IBMResilientSystems.get_incident_notes", return_value=[])
    _mocker.patch("IBMResilientSystems.incident_attachments", return_value=[])
    _mocker.patch("IBMResilientSystems.incident_artifacts", return_value=[])
    _mocker.patch("IBMResilientSystems.get_phase_name", return_value="Detect/Analyze")

    result = process_raw_incident(client, load_test_data("./test_data/test_get_incident_response.json"))

    assert result["description"] == "1111 2222 3333"
    assert result["discovered_date"] == "2024-07-29T11:31:57Z"
    assert result["create_date"] == "2024-07-29T11:32:36Z"


@pytest.mark.parametrize(
    "incident_id, delta, expected_dto",
    [
        (
            "1000",
            {"ibmsecurityqradarsoarname": "Updated Incident Name", "description": "New description"},
            {
                "changes": [
                    {"field": "name", "new_value": {"text": "Updated Incident Name"}, "old_value": {"text": "incident_name"}},
                    {
                        "field": "description",
                        "new_value": {"textarea": {"content": "New description", "format": "html"}},
                        "old_value": {"textarea": {"content": "1111 2222 3333", "format": "html"}},
                    },
                ]
            },
        ),
        (
            "1001",
            {"resolution_id": ""},
            {
                "changes": [
                    {"field": "plan_status", "new_value": {"text": "C"}, "old_value": {"text": "A"}},
                    {"field": "resolution_id", "new_value": {"textarea": None}, "old_value": {"id": 9}},
                ]
            },
        ),
    ],
)
def test_prepare_incident_update_dto_for_mirror(_mocker, incident_id, delta, expected_dto):
    from IBMResilientSystems import SimpleClient, prepare_incident_update_dto_for_mirror

    client = SimpleClient()
    client.org_id = 0

    mock_get_incident = _mocker.patch(
        "IBMResilientSystems.get_incident", return_value=load_test_data("./test_data/test_get_incident_response.json")
    )

    _mocker.patch.object(demisto, "params", return_value={"close_ibm_incident": True})

    result = prepare_incident_update_dto_for_mirror(client, incident_id, delta)

    mock_get_incident.assert_called_once_with(client, incident_id)
    assert result == expected_dto


def test_prettify_incident_tasks_multiple_tasks(_mocker):
    from IBMResilientSystems import SimpleClient, prettify_incident_tasks

    client = SimpleClient()
    tasks = [
        {
            "id": 1,
            "name": "Task 1",
            "description": "Description 1",
            "due_date": 1641024000000,
            "status": "O",
            "required": True,
            "owner_fname": "John",
            "owner_lname": "Doe",
            "phase_id": 1,
            "creator_principal": {"display_name": "Admin User"},
            "instructions": {"content": "Instructions 1"},
        },
        {
            "id": 2,
            "name": "Task 2",
            "description": "Description 2",
            "due_date": None,
            "status": "C",
            "required": False,
            "owner_fname": "Jane",
            "owner_lname": "Smith",
            "phase_id": 2,
            "creator_principal": None,
            "instructions": None,
        },
    ]
    _mocker.patch("IBMResilientSystems.get_phase_name", side_effect=["Initial", "Analysis"])
    _mocker.patch("IBMResilientSystems.normalize_timestamp", return_value="2022-01-01T12:00:00Z")

    result = prettify_incident_tasks(client, tasks)

    assert len(result) == 2
    assert result[0]["ID"] == 1
    assert result[0]["Name"] == "Task 1"
    assert result[0]["Status"] == "Open"
    assert result[0]["DueDate"] == "2022-01-01T12:00:00Z"
    assert result[0]["Phase"] == "Initial"
    assert result[0]["Creator"] == "Admin User"
    assert result[0]["Instructions"] == "Instructions 1"

    assert result[1]["ID"] == 2
    assert result[1]["Name"] == "Task 2"
    assert result[1]["Status"] == "Closed"
    assert result[1]["DueDate"] == "No due date"
    assert result[1]["Phase"] == "Analysis"
    assert result[1]["Creator"] == ""
    assert result[1]["Instructions"] == ""


def test_prettify_incident_tasks_missing_fields(_mocker):
    from IBMResilientSystems import SimpleClient, prettify_incident_tasks

    client = SimpleClient()
    tasks = [
        {"id": 1, "name": "Minimal Task", "description": "", "due_date": None, "status": "O", "required": False, "phase_id": 1}
    ]
    _mocker.patch("IBMResilientSystems.get_phase_name", return_value="Initial")

    result = prettify_incident_tasks(client, tasks)

    assert len(result) == 1
    assert result[0]["ID"] == 1
    assert result[0]["Name"] == "Minimal Task"
    assert result[0]["Description"] == ""
    assert result[0]["DueDate"] == "No due date"
    assert result[0]["Status"] == "Open"
    assert result[0]["Required"] is False
    assert result[0]["Owner"] == " "
    assert result[0]["Phase"] == "Initial"
    assert result[0]["Creator"] == ""
    assert result[0]["Instructions"] == ""


def test_list_open_incidents(_mocker):
    from IBMResilientSystems import SimpleClient, list_open_incidents

    client = SimpleClient()
    get_incidents_request = _mocker.patch.object(SimpleClient, "get", return_value=[])

    list_open_incidents(client)
    get_incidents_request.assert_called_once_with("/incidents/open")


def test_get_users(_mocker):
    from IBMResilientSystems import SimpleClient, get_users

    client = SimpleClient()
    _mocker.patch.object(SimpleClient, "get", return_value=[])
    get_users(client)
    client.get.assert_called_once_with("/users")


def test_get_phase_name(_mocker):
    from IBMResilientSystems import SimpleClient, get_phase_name

    client = SimpleClient()
    _mocker.patch.object(
        SimpleClient,
        "get",
        return_value={
            "id": 1004,
            "name": "Engage",
            "enabled": True,
            "perms": {"deleteable": True, "reorderable": True},
            "uuid": "0000-0000-0000-00000",
            "order": 1,
            "tags": [],
        },
    )

    get_phase_name(client, "1004")

    client.get.assert_called_once_with("/phases/1004")


def test_get_phases(_mocker):
    from IBMResilientSystems import SimpleClient, get_phases

    client = SimpleClient()
    _mocker.patch.object(
        SimpleClient,
        "get",
        return_value={
            "entities": [
                {
                    "id": 1003,
                    "name": "Initial",
                    "enabled": True,
                    "perms": {"deleteable": False, "reorderable": False},
                    "uuid": "0000-0000-0000-0000",
                    "order": 0,
                    "tags": [],
                }
            ]
        },
    )
    get_phases(client)

    client.get.assert_called_once_with("/phases")


def test_get_tasks(_mocker):
    from IBMResilientSystems import SimpleClient, get_tasks

    client = SimpleClient()
    _mocker.patch.object(SimpleClient, "get", return_value=[])

    get_tasks(client, "1000")

    client.get.assert_called_once_with("/incidents/1000/tasks?text_content_output_format=objects_convert_text")


@pytest.mark.parametrize(
    "resolution_id, resolution_summary, expected_close_reason, expected_close_notes",
    [
        (8, "Duplicate issue", "Duplicate", "Duplicate issue"),
        (0, "Hardware failure", "Resolved", "Hardware failure"),
        (None, "User error", "Resolved", "User error"),
    ],
)
def test_handle_incoming_incident_resolution(
    _mocker, resolution_id, resolution_summary, expected_close_reason, expected_close_notes
):
    from IBMResilientSystems import EntryFormat, EntryType, handle_incoming_incident_resolution

    incident_id = "1234"
    result = handle_incoming_incident_resolution(incident_id, resolution_id, resolution_summary)

    assert result["Type"] == EntryType.NOTE
    assert result["ContentsFormat"] == EntryFormat.JSON
    assert result["Contents"]["dbotIncidentClose"] is True
    assert result["Contents"]["closeReason"] == expected_close_reason
    assert result["Contents"]["closeNotes"] == f"{expected_close_notes}\nClosed on IBM QRadar SOAR"


def test_handle_incoming_incident_resolution_unknown_resolution(_mocker):
    from IBMResilientSystems import EntryFormat, EntryType, handle_incoming_incident_resolution

    incident_id = "5678"
    resolution_id = 999  # Unknown resolution ID
    resolution_summary = "Unknown resolution"
    result = handle_incoming_incident_resolution(incident_id, resolution_id, resolution_summary)

    assert result["Type"] == EntryType.NOTE
    assert result["ContentsFormat"] == EntryFormat.JSON
    assert result["Contents"]["dbotIncidentClose"] is True
    assert result["Contents"]["closeReason"] == "Resolved"
    assert result["Contents"]["closeNotes"] == "Unknown resolution\nClosed on IBM QRadar SOAR"