RSA Archer v2

The RSA Archer GRC platform provides a common foundation for managing policies, controls, risks, assessments, and deficiencies across lines of business.

Case Management · RSA Archer

Details

IDRSA Archer v2
ProviderRSAsecurity
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/python3-deb:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

The RSA Archer GRC platform provides a common foundation for managing policies, controls, risks, assessments and deficiencies across lines of business.

Configure RSA Archer v2 in Cortex

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for RSA Archer v2.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Server URL For example: https://192.168.0.1/rsaarcher, https://192.168.0.1/, or https://192.168.0.1/archer. True
    API Endpoint Change only if using another API endpoint. True
    Username   True
    Fetch incidents   False
    Incident type   False
    Trust any certificate (not secure)   False
    Use system proxy settings   False
    Timeout Request timeout value in seconds. Default is 400. False
    Instance name   True
    User domain   False
    Application ID for fetch   True
    Application date field for fetch The value should be the field name. Default is Date/Time Occurred. True
    Maximum number of incidents to pull per fetch Default is 10. False
    First fetch timestamp Time from which to begin fetching incidents in the <number> <time unit> format. For example: 12 hours, 7 days, 3 months, 1 year. False
    List of fields from the application to get into the incident A comma-separated list of application field names. For example: Date/Time Occurred,Days Open. False
    XML for fetch filtering Additional XML condition element(s) to use when fetching. Using a “DateComparisonFilterCondition” element is not supported. For more information, check the “Limitations” section of the documentation. False
  4. Click Test to validate the URLs, token, and connection.

Limitations

  • The “XML for fetch filtering” configuration parameter cannot contain “DateComparisonFilterCondition” XML element since it would interfere with the existing fetch date filter. Other types of filtering conditions, such as “TextFilterCondition”, are allowed.

  • Archer customers might know there is an Archer REST API that supports token based authentication. Not all functionality of this integration can be achieved using Archer’s REST API, which is why this integration requires credential based authentication.

Commands

You can execute these commands from the Cortex XSOAR 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.

archer-search-applications


Gets application details or list of all applications.

Base Command

archer-search-applications

Input

Argument Name Description Required
applicationId The application ID to get details for. Leave empty to get a list of all applications. Optional

Context Output

Path Type Description
Archer.Application.Guid String The application GUID.
Archer.Application.Id Number The unique ID of the application.
Archer.Application.Status Number The application Status.
Archer.Application.Type Number The application type.
Archer.Application.Name String The application name.

Command Example

!archer-search-applications applicationId=75

Context Example

{
    "Archer": {
        "Application": {
            "Guid": "982fc3be-7c43-4d79-89a1-858ed262b930",
            "Id": 75,
            "LanguageId": 1,
            "Name": "Incidents",
            "Status": 1,
            "Type": 2
        }
    }
}

Human Readable Output

Search applications results

Guid Id LanguageId Name Status Type
982fc3be-7c43-4d79-89a1-858ed262b930 75 1 Incidents 1 2

archer-get-application-fields


Gets all application fields by application ID.

Base Command

archer-get-application-fields

Input

Argument Name Description Required
applicationId The application ID to get the application fields for. Required

Context Output

Path Type Description
Archer.ApplicationField.FieldId Number The unique ID of the field.
Archer.ApplicationField.FieldName String The field name.
Archer.ApplicationField.FieldType String The field type.
Archer.ApplicationField.LevelID Number The field level ID.

Command Example

!archer-get-application-fields applicationId=75

Context Example

{
    "Archer": {
        "ApplicationField": [
            {
                "FieldId": 296,
                "FieldName": "Incident ID",
                "FieldType": "TrackingID",
                "LevelID": 67
            },
            {
                "FieldId": 297,
                "FieldName": "Date Created",
                "FieldType": "First Published",
                "LevelID": 67
            },
            {
                "FieldId": 298,
                "FieldName": "Last Updated",
                "FieldType": "Last Updated Field",
                "LevelID": 67
            },
            {
                "FieldId": 302,
                "FieldName": "Status",
                "FieldType": "Values List",
                "LevelID": 67
            },
            {
                "FieldId": 303,
                "FieldName": "Date/Time Occurred",
                "FieldType": "Date",
                "LevelID": 67
            },
            {
                "FieldId": 304,
                "FieldName": "Priority",
                "FieldType": "Values List",
                "LevelID": 67
            }
        ]
    }
}

Human Readable Output

Application fields

FieldId FieldName FieldType LevelID
296 Incident ID TrackingID 67
297 Date Created First Published 67
298 Last Updated Last Updated Field 67
302 Status Values List 67
303 Date/Time Occurred Date 67
304 Priority Values List 67

archer-get-field


Returns a mapping from list value name to list value ID.

Base Command

archer-get-field

Input

Argument Name Description Required
fieldID The ID of the field. Required

Context Output

Path Type Description
Archer.ApplicationField.FieldId Number The unique ID of the field.
Archer.ApplicationField.FieldName String The field name.
Archer.ApplicationField.FieldType String The field type.
Archer.ApplicationField.LevelID Number The field level ID.

Command Example

!archer-get-field fieldID=350

Context Example

{
    "Archer": {
        "ApplicationField": {
            "FieldId": 350,
            "FieldName": "Reported to Police",
            "FieldType": "Values List",
            "LevelID": 67
        }
    }
}

Human Readable Output

Application field

FieldId FieldName FieldType LevelID
350 Reported to Police Values List 67

archer-get-mapping-by-level


Returns a mapping of fields by level ID.

Base Command

archer-get-mapping-by-level

Input

Argument Name Description Required
level The ID of the level. Required

Context Output

Path Type Description
Archer.LevelMapping.Id Number The unique ID of the field.
Archer.LevelMapping.Name String The field name.
Archer.LevelMapping.Type String The field type.
Archer.LevelMapping.LevelId Number The field level ID.

Command Example

!archer-get-mapping-by-level level=67

Context Example

{
    "Archer": {
        "LevelMapping": [
            {
                "Id": 296,
                "LevelId": 67,
                "Name": "Incident ID",
                "Type": "TrackingID"
            },
            {
                "Id": 297,
                "LevelId": 67,
                "Name": "Date Created",
                "Type": "First Published"
            },
            {
                "Id": 298,
                "LevelId": 67,
                "Name": "Last Updated",
                "Type": "Last Updated Field"
            },
            {
                "Id": 302,
                "LevelId": 67,
                "Name": "Status",
                "Type": "Values List"
            }
        ]
    }
}

Human Readable Output

Level mapping for level 67

Id LevelId Name Type
296 67 Incident ID TrackingID
297 67 Date Created First Published
298 67 Last Updated Last Updated Field
302 67 Status Values List

archer-get-record


Gets information about a content record in the given application.

Base Command

archer-get-record

Input

Argument Name Description Required
contentId The content record ID. Required
applicationId The application ID. Required

Context Output

Path Type Description
Archer.Record.Id Number The unique ID of the content record.

Command Example

!archer-get-record applicationId=75 contentId=227602

Context Example

{
    "Archer": {
        "Record": {
            "Current Status": {
                "OtherText": null,
                "ValuesListIds": [
                    6412
                ]
            },
            "Date/Time Occurred": "2018-03-23T07:00:00",
            "Date/Time Reported": "2018-03-26T10:03:32.243",
            "Days Open": 805,
            "Default Record Permissions": {
                "GroupList": [
                    {
                        "HasDelete": true,
                        "HasRead": true,
                        "HasUpdate": true,
                        "Id": 50
                    },
                    {
                        "HasDelete": false,
                        "HasRead": true,
                        "HasUpdate": false,
                        "Id": 51
                    }
                ],
                "UserList": []
            },
            "Google Map": "<a target='_new' href='http://maps.google.com/maps?f=q&ie=UTF8&om=1&hl=en&q=, , , '>Google Map</a>",
            "Id": 227602,
            "Incident Details": "Incident Details",
            "Incident Result": {
                "OtherText": null,
                "ValuesListIds": [
                    531
                ]
            },
            "Incident Summary": "Summary...",
            "Is BSA (Bank Secrecy Act) reporting required in the US?": {
                "OtherText": null,
                "ValuesListIds": [
                    835
                ]
            },
            "Notify Incident Owner": {
                "OtherText": null,
                "ValuesListIds": [
                    6422
                ]
            },
            "Override Rejected Submission": {
                "OtherText": null,
                "ValuesListIds": [
                    9565
                ]
            },
            "Status": {
                "OtherText": null,
                "ValuesListIds": [
                    466
                ]
            },
            "Status Change": {
                "OtherText": null,
                "ValuesListIds": [
                    156
                ]
            },
            "Supporting Documentation": [
                125
            ]
        }
    }
}

Human Readable Output

Record details

Current Status Date/Time Occurred Date/Time Reported Days Open Default Record Permissions Google Map Id Incident Details Incident Result Incident Summary Is BSA (Bank Secrecy Act) reporting required in the US? Notify Incident Owner Override Rejected Submission Status Status Change Supporting Documentation
ValuesListIds: 6412
OtherText: null
2018-03-23T07:00:00 2018-03-26T10:03:32.243 805.0 UserList:
GroupList: {‘Id’: 50, ‘HasRead’: True, ‘HasUpdate’: True, ‘HasDelete’: True},
{‘Id’: 51, ‘HasRead’: True, ‘HasUpdate’: False, ‘HasDelete’: False}
Google Map 227602 Incident Details ValuesListIds: 531
OtherText: null
Summary… ValuesListIds: 835
OtherText: null
ValuesListIds: 6422
OtherText: null
ValuesListIds: 9565
OtherText: null
ValuesListIds: 466
OtherText: null
ValuesListIds: 156
OtherText: null
125

archer-create-record


Creates a new content record in the given application.

Note: When creating a new record, make sure the values are sent through the fieldsToValues argument properly.

  • Example for the Values List field type: {“Type”: [“Switch”], fieldname: [value1, value2]}
  • Example for the Values List field type with OtherText property: {“Patch Type”: {“ValuesList”: [“Custom Type”], “OtherText”: “actuall text”}, field_name_without_other: [value1, value2]}
  • Example for the External Links field type: {“Patch URL”: [{“value”:”github”, “link”: “https://github.com”}]}
  • Example for the Users/Groups List field type: {“Policy Owner”:{“users”: [20],”groups”: [30]}}
  • Example for the Cross- Reference field type: {“Area Reference(s)”: [20]}

In other cases the value can be sent as-is.

To determine the appropriate field type value, use the archer-get-application-fields command with the applicationId to get the list of all FieldType by FieldName.

Base Command

archer-create-record

Input

Argument Name Description Required
applicationId The application ID. Required
fieldsToValues Record fields in JSON format: { “Name1”: Value1, “Name2”: Value2 }. Field names are case sensitive. Required
levelId The Level ID to use to update the record. If empty, the command by default takes the first level ID. Optional

Context Output

Path Type Description
Archer.Record.Id Number The unique ID of the content record.

Command Example

!archer-create-record applicationId=75 fieldsToValues={"Incident Summary":"This is the incident summary","Priority":["High"]}

Context Example

{
    "Archer": {
        "Record": {
            "Id": 239643
        }
    }
}

Human Readable Output

Record created successfully, record id: 239643

archer-delete-record


Deletes an existing content record in the given application.

Base Command

archer-delete-record

Input

Argument Name Description Required
contentId The ID of the content record to delete. Required

Context Output

There is no context output for this command.

Command Example

!archer-delete-record contentId=239642

Context Example

{}

Human Readable Output

Record 239642 deleted successfully

archer-update-record


Updates an existing content record in the given application.
Note: When updating a record, make sure the values are sent through the fieldsToValues argument properly. For more details see the archer-create-record description.

Base Command

archer-update-record

Input

Argument Name Description Required
applicationId The application ID. Required
fieldsToValues Record fields in JSON format: { “Name1”: Value1, “Name2”: Value2 }. Field name is case sensitive Required
contentId The ID of the content record ID. Required
levelId The Level ID to use to update the record. If empty, the command by default takes the first level ID. Optional

Context Output

There is no context output for this command.

Command Example

!archer-update-record applicationId=75 contentId=239326 fieldsToValues={"Priority":["High"]}

Context Example

{}

Human Readable Output

Record 239326 updated successfully

archer-execute-statistic-search-by-report


Performs statistic search by report GUID.

Base Command

archer-execute-statistic-search-by-report

Input

Argument Name Description Required
reportGuid The report GUID. Required
maxResults Maximum number of pages for the reports. Required

Context Output

There is no context output for this command.

Command Example

!archer-execute-statistic-search-by-report maxResults=100 reportGuid=e4b18575-52c0-4f70-b41b-3ff8b6f13b1c

Context Example

{}

Human Readable Output

{
“Groups”: {
“@count”: “3”,
“Metadata”: {
“FieldDefinitions”: {
“FieldDefinition”: [
{
“@alias”: “Classification”,
“@guid”: “769b2548-6a98-49b6-95c5-03e391f0a40e”,
“@id”: “76”,
“@name”: “Classification”
},
{
“@alias”: “Standard_Name”,
“@guid”: “a569fd34-16f9-4965-93b0-889fcb91ba7a”,
“@id”: “1566”,
“@name”: “Standard Name”
}
]
}
},
“Total”: {
“Aggregate”: {
“@Count”: “1497”,
“@FieldId”: “1566”
}
}
}
}

archer-get-reports


Gets all reports from Archer.

Base Command

archer-get-reports

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Context Example

```json
{
    "Archer": {
        "Report": [
            {
                "ApplicationGUID": "982fc3be-7c43-4d79-89a1-858ed262b930",
                "ApplicationName": "Policies",
                "ApplicationDescription": "This report displays a listing of all security Policies.",
                "ReportGUID": "22961b81-4866-40ea-a298-99afb348598d",
                "ReportName": "Policies - Summary view"
            }
        ]
    }
}

Human Readable Output

archer-get-search-options-by-guid


Returns search criteria by report GUID.

Base Command

archer-get-search-options-by-guid

Input

Argument Name Description Required
reportGuid The report GUID. Required

Context Output

There is no context output for this command.

Command Example

!archer-get-search-options-by-guid reportGuid=bce4222c-ecfe-4cef-a556-fe746e959f12

Context Example

{}

Human Readable Output

{
“SearchReport”: {
“Criteria”: {
“ModuleCriteria”: {
“BuildoutRelationship”: “Union”,
“IsKeywordModule”: “True”,
“Module”: “421”,
“SortFields”: {
“SortField”: [
{
“Field”: “15711”,
“SortType”: “Ascending”
},
{
“Field”: “15683”,
“SortType”: “Ascending”
}
]
}
}
},
“DisplayFields”: {
“DisplayField”: [
“15683”,
“15686”,
“15687”,
“15690”,
“15706”,
“15711”,
“15710”,
“15712”,
“15713”,
“15714”,
“15715”,
“15716”,
“15725”,
“15717”,
“15718”
]
},
“PageSize”: “50”
}
}

archer-reset-cache


Resets Archer’s integration cache. This cache is maintained in XSOAR based on previous search results and must be cleared when field mappings no longer make sense. Run this command if you change the fields of your Archer application, the Archer v2 integration’s settings, or if the target Archer user moves between environments or settings.

Base Command

archer-reset-cache

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Context Example

```json
{}

Human Readable Output

archer-get-valuelist


Returns a list of values for a specified field, for example, fieldID=16114. This command is applicable only to value list fields (type 4) and will attempt to fetch the list from the internal integration instance cache. To ensure an up-to-date response, execute the archer-reset-cache command beforehand to clear the cache and force a new request to the API.

Base Command

archer-get-valuelist

Input

Argument Name Description Required        
fieldID The field ID. Required   depth In case of nesting, to which level to go in the depth of the recursion. Default is 0. Optional

Context Output

Path Type Description
Archer.ApplicationField.ValuesList.Id Number The field value ID.
Archer.ApplicationField.ValuesList.IsSelectable Boolean Specifies whether you can select the field value.
Archer.ApplicationField.ValuesList.Name String The field value name.

Command Example

!archer-get-valuelist fieldID=302

Context Example

{
    "Archer": {
        "ApplicationField": {
            "FieldId": "302",
            "ValuesList": [
                {
                    "Id": 466,
                    "IsSelectable": true,
                    "Name": "New"
                },
                {
                    "Id": 467,
                    "IsSelectable": true,
                    "Name": "Assigned"
                },
                {
                    "Id": 468,
                    "IsSelectable": true,
                    "Name": "In Progress"
                },
                {
                    "Id": 469,
                    "IsSelectable": true,
                    "Name": "On Hold"
                },
                {
                    "Id": 470,
                    "IsSelectable": true,
                    "Name": "Closed"
                }
            ]
        }
    }
}

Human Readable Output

Value list for field 302

Id IsSelectable Name
466 true New
467 true Assigned
468 true In Progress
469 true On Hold
470 true Closed

archer-upload-file


Uploads a file to Archer. You can associate the file to a record by providing all of the following arguments:

  • applicationId
  • contentId
  • associatedField

Base Command

archer-upload-file

Input

Argument Name Description Required
entryId A comma seperated list of entry IDs of the files in Cortex XSOAR context. Required
contentId The content record ID to update. Optional
applicationId ID of the application which we want to upload the file to. Optional
associatedField Archer field name to associate the file with. Optional

Context Output

There is no context output for this command.

Command Example

!archer-upload-file entryId=16695@b32fdf18-1c65-43af-8918-7f85a1fab951

Context Example

{}

Human Readable Output

File uploaded successfully, attachment ID: 126

archer-get-file


Downloads a file from Archer to Cortex XSOAR War Room context.

Base Command

archer-get-file

Input

Argument Name Description Required
fileId The file ID. Required

Context Output

There is no context output for this command.

Command Example

!archer-get-file fileId=125

Context Example

{
    "File": {
        "EntryID": "16680@b32fdf18-1c65-43af-8918-7f85a1fab951",
        "Extension": "jpg",
        "Info": "image/jpeg",
        "MD5": "fb80f3fc41f2524",
        "Name": "11.jpg",
        "SHA1": "6898512eaa3",
        "SHA256": "f4bed94abd752",
        "SHA512": "ecce92345fb8b6aa",
        "SSDeep": "768:XYDWR",
        "Size": 52409,
        "Type": "JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 750x561, frames 3"
    }
}

Human Readable Output

archer-list-users


Gets details for a user or a list of all users.

Base Command

archer-list-users

Input

Argument Name Description Required
userId The ID of the user to get details for. Leave empty to get a list of all users. Optional

Context Output

Path Type Description
Archer.User.AccountStatus String The account status of the user.
Archer.User.DisplayName String The display name of the user.
Archer.User.FirstName String The first name of the user.
Archer.User.Id Number The unique ID of the user.
Archer.User.LastLoginDate Date The last login date of user.
Archer.User.LastName String The last name of the user.
Archer.User.MiddleName String The middle name of the user.
Archer.User.UserName String The username associated with the account.

Command Example


#### Context Example

```json
{
    "Archer": {
        "User": {
            "AccountStatus": "Locked",
            "DisplayName": "cash, johnny",
            "FirstName": "johnny",
            "Id": 202,
            "LastLoginDate": "2018-09-03T07:56:51.027",
            "LastName": "cash",
            "MiddleName": null,
            "UserName": "johnnyCash"
        }
    }
}

Human Readable Output

Users list

AccountStatus DisplayName FirstName Id LastLoginDate LastName MiddleName UserName
Locked cash, johnny johnny 202 2018-09-03T07:56:51.027 cash   johnnyCash

archer-search-records


Search for records inside the given application

Base Command

archer-search-records

Input

Argument Name Description Required
applicationId The ID of the application in which to search for records. Required
fieldToSearchOn The name of the field on which to search. Leave empty to search on all fields. Optional
fieldToSearchById The name of the primary Id field on which to search. Used instead of the fieldToSearchOn argument for searching by the application primary field. Optional
searchValue Search value. Leave empty to search for all. Optional
maxResults Maximum number of results to return from the search (default is 10). Optional
fieldsToDisplay Fields to present in the search results in array format. For example, “Title,Incident Summary”. Optional
numericOperator Numeric search operator. Can be “Equals”, “NotEqual”, “GreaterThan”, or “LessThan”. Optional
dateOperator Date search operator. Can be “Equals”, “DoesNotEqual”, “GreaterThan”, or “LessThan”. Optional
fieldsToGet Fields to fetch from the the application. Optional
fullData Whether to get extended responses with all of the data regarding this search. For example, “fullData=true” Required
isDescending Whether to order by descending order. Possible values are: “true”, “false”. Optional
levelId The Level ID to use for searching. This argument is relevant when fullData is True. If empty, the command by default takes the first level ID. Optional
xmlForFiltering The raw XML filter condition. For example: “DateComparisonFilterCondition” or “TextFilterCondition” XML element. Optional

Context Output

Path Type Description
Archer.Record Unknown The content object.
Archer.Record.Id Number The content record ID.

Command Example

!archer-search-records applicationId=75 fullData=False fieldsToDisplay=`Date/Time Occurred,Days Open` fieldsToGet=`Date/Time Occurred,Days Open` fieldToSearchOn=`Date/Time Occurred` dateOperator=GreaterThan searchValue=2018-06-23T07:00:00Z xmlForFiltering=`<TextFilterCondition><Operator>Contains</Operator><Field name="Incident Priority">456</Field><Value>High</Value></TextFilterCondition>` maxResults=100

Context Example

{
    "Archer": {
        "Record": {
            "Date/Time Occurred": "2018-07-10T08:00:00Z",
            "Days Open": "30",
            "Id": "227664"
        }
    }
}

Human Readable Output

Search records results

Date/Time Occurred Days Open
2018-07-10T08:00:00Z 30

archer-search-records-by-report


Searches records by report GUID.

Base Command

archer-search-records-by-report

Input

Argument Name Description Required
reportGuid The report GUID. Required

Context Output

Path Type Description
Archer.SearchByReport.ReportGUID String The report GUID.
Archer.SearchByReport.RecordsAmount Number The number of records found by the search.
Archer.SearchByReport.Record Unknown The records found by the search.

Command Example

!archer-search-records-by-report reportGuid=bce4222c-ecfe-4cef-a556-fe746e959f12

Context Example

{
    "Archer": {
        "SearchByReport": {
            "Record": [
                {
                    "Description": "<p>\u00a0test_procedure_0</p>",
                    "Id": "227528",
                    "Procedure Name": "test_procedure_0",
                    "Threat Category": "Malware",
                    "Tracking ID": "227528"
                },
                {
                    "Description": "<p>\u00a0test_procedure_1</p>",
                    "Id": "227529",
                    "Procedure Name": "test_procedure_1",
                    "Threat Category": "Malware",
                    "Tracking ID": "227529"
                },
                {
                    "Description": "<p>test_procedure_2\u00a0</p>",
                    "Id": "227531",
                    "Procedure Name": "test_procedure_2",
                    "Threat Category": "Malware",
                    "Tracking ID": "227531"
                },
                {
                    "Description": "<p>test_procedure_3</p>",
                    "Id": "227532",
                    "Procedure Name": "test_procedure_3",
                    "Threat Category": "Malware",
                    "Tracking ID": "227532"
                }
            ],
            "RecordsAmount": 4,
            "ReportGUID": "bce4222c-ecfe-4cef-a556-fe746e959f12"
        }
    }
}

Human Readable Output

Search records by report results

Description Id Procedure Name Threat Category Tracking ID
<p> test_procedure_0</p> 227528 test_procedure_0 Malware 227528
<p> test_procedure_1</p> 227529 test_procedure_1 Malware 227529
<p>test_procedure_2 </p> 227531 test_procedure_2 Malware 227531
<p>test_procedure_3</p> 227532 test_procedure_3 Malware 227532

archer-print-cache


Prints the Archer’s integration cache.

Base Command

archer-print-cache

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Context Example

```json
{}

Human Readable Output

{
“75”: [
{
“level”: 67,
“mapping”: {
“10052”: {
“FieldId”: “10052”,
“IsRequired”: false,
“Name”: “Related Incidents (2)”,
“RelatedValuesListId”: null,
“Type”: 23
},
“10172”: {
“FieldId”: “10172”,
“IsRequired”: false,
“Name”: “Source”,
“RelatedValuesListId”: 1176,
“Type”: 4
},
“10183”: {
“FieldId”: “10183”,
“IsRequired”: false,
“Name”: “Is BSA (Bank Secrecy Act) reporting required in the US?”,
“RelatedValuesListId”: 152,
“Type”: 4
},
“10188”: {
“FieldId”: “10188”,
“IsRequired”: false,
“Name”: “Batch File Format”,
“RelatedValuesListId”: 1183,
“Type”: 4
}
}
}
],
“fieldValueList”: {
“7782”: {
“FieldId”: “7782”,
“ValuesList”: [
{
“Id”: 6412,
“IsSelectable”: true,
“Name”: “New”
},
{
“Id”: 6413,
“IsSelectable”: true,
“Name”: “Assigned”
},
{
“Id”: 6414,
“IsSelectable”: true,
“Name”: “In Progress”
},
{
“Id”: 6415,
“IsSelectable”: true,
“Name”: “On Hold”
},
{
“Id”: 6416,
“IsSelectable”: true,
“Name”: “Closed”
}
]
}
}
}

Configuration parameters

  • url — Server URL (required)
  • api_endpoint — API Endpoint
  • instanceName — Instance name (required)
  • credentials — Username (required)
  • userDomain — User domain
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • applicationId — Application ID for fetch (required)
  • applicationDateField — Application date field for fetch (required)
  • fetch_limit — Maximum number of incidents to pull per fetch
  • fetch_time — First fetch timestamp
  • fields_to_fetch — List of fields from the application to get into the incident
  • fetch_xml — XML for fetch filtering
  • timeout — Timeout
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentFetchInterval — Incidents Fetch Interval

Commands (19)

  • archer-create-record

    Creates a new content record in the given application.

  • archer-delete-record

    Deletes an existing content record in the given application.

  • archer-execute-statistic-search-by-report

    Performs a statistic search by report GUID.

  • archer-get-application-fields

    Gets all application fields by application ID.

  • archer-get-field

    Returns a mapping from list value name to list value ID.

  • archer-get-file

    Downloads a file from Archer to Cortex XSOAR context.

  • archer-get-mapping-by-level

    Returns a mapping of fields by level ID.

  • archer-get-record

    Gets information about a content record in the given application.

  • archer-get-reports

    Gets all reports from Archer.

  • archer-get-search-options-by-guid

    Returns search criteria by report GUID.

  • archer-get-valuelist

    Returns a list of values for a specified field, for example, fieldID=16114. This command is applicable only to value list fields (type 4) and will attempt to fetch the list from the internal integration instance cache. To ensure an up-to-date response, execute the `archer-reset-cache` command beforehand to clear the cache and force a new request to the API.

  • archer-list-users

    Gets details for a user or a list of all users.

  • archer-print-cache

    Prints the Archer's integration cache.

  • archer-reset-cache

    Resets Archer's integration cache. Run this command if you change the fields of your Archer application.

  • archer-search-applications

    Gets application details or a list of all applications.

  • archer-search-records

    Searches for records in the given application.

  • archer-search-records-by-report

    Searches records by report GUID.

  • archer-update-record

    Updates an existing content record in the given application.

  • archer-upload-file

    Uploads a file to Archer.

import random
from datetime import UTC, datetime
from enum import Enum

import dateparser
import urllib3

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

""" IMPORTS """

# Disable insecure warnings
urllib3.disable_warnings()

FETCH_PARAM_ID_KEY = "field_time_id"
LAST_FETCH_TIME_KEY = "last_fetch"
OCCURRED_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"

REQUEST_HEADERS = {
    "Accept": "application/json,text/html,application/xhtml +xml,application/xml;q=0.9,*/*;q=0.8",
    "Content-Type": "application/json",
}

FIELD_TYPE_DICT = {
    1: "Text",
    2: "Numeric",
    3: "Date",
    4: "Values List",
    6: "TrackingID",
    7: "External Links",
    8: "Users/Groups List",
    9: "Cross-Reference",
    11: "Attachment",
    12: "Image",
    14: "Cross-Application Status Tracking (CAST)",
    16: "Matrix",
    19: "IP Address",
    20: "Record Status",
    21: "First Published",
    22: "Last Updated Field",
    23: "Related Records",
    24: "Sub-Form",
    25: "History Log",
    26: "Discussion",
    27: "Multiple Reference Display Control",
    28: "Questionnaire Reference",
    29: "Access History",
    30: "V oting",
    31: "Scheduler",
    1001: "Cross-Application Status Tracking Field Value",
}

ACCOUNT_STATUS_DICT = {1: "Active", 2: "Inactive", 3: "Locked"}

API_ENDPOINT = demisto.params().get("api_endpoint", "api")


class FilterConditionTypes(Enum):
    date = "DateComparisonFilterCondition"
    numeric = "NumericFilterCondition"
    text = "TextFilterCondition"
    content = "ContentFilterCondition"


def parser(
    date_str,
    date_formats=None,
    languages=None,
    locales=None,
    region=None,
    settings=None,
) -> datetime:
    """Wrapper of dateparser.parse to support return type value"""
    date_obj = dateparser.parse(
        date_str,
        date_formats=date_formats,
        languages=languages,
        locales=locales,
        region=region,
        settings=settings,
    )
    assert isinstance(date_obj, datetime), f"Could not parse date {date_str}"  # MYPY Fix
    return date_obj.replace(tzinfo=UTC)


def get_token_soap_request(user, password, instance, domain=None):
    if domain:
        # Create the root element
        root = ET.Element(
            "soap:Envelope",
            {
                "xmlns:xsi": "http://www.w3.orecord_to_incidentrg/2001/XMLSchema-instance",
                "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
                "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
            },
        )
        # Create the soap:Body element
        body = ET.SubElement(root, "soap:Body")
        # Create the CreateUserSessionFromInstance element
        create_user_session = ET.SubElement(
            body,
            "CreateDomainUserSessionFromInstance",
            {"xmlns": "http://archer-tech.com/webservices/"},
        )
        # Add the userName, instanceName, and password elements
        ET.SubElement(create_user_session, "userName").text = user
        ET.SubElement(create_user_session, "instanceName").text = instance
        ET.SubElement(create_user_session, "password").text = password
        ET.SubElement(create_user_session, "usersDomain").text = domain
    else:
        # Create the root element
        root = ET.Element(
            "soap:Envelope",
            {
                "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
                "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
                "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
            },
        )
        # Create the soap:Body element
        body = ET.SubElement(root, "soap:Body")
        # Create the CreateUserSessionFromInstance element
        create_user_session = ET.SubElement(
            body,
            "CreateUserSessionFromInstance",
            {"xmlns": "http://archer-tech.com/webservices/"},
        )
        # Add the userName, instanceName, and password elements
        ET.SubElement(create_user_session, "userName").text = user
        ET.SubElement(create_user_session, "instanceName").text = instance
        ET.SubElement(create_user_session, "password").text = password

    return ET.tostring(root)


def get_reports_soap_request(token):
    root = ET.Element(
        "soap:Envelope",
        {
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
            "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
        },
    )
    # Create the soap:Body element
    body = ET.SubElement(root, "soap:Body")
    # Create the GetReports element
    get_reports = ET.SubElement(body, "GetReports", {"xmlns": "http://archer-tech.com/webservices/"})
    # Add the sessionToken element
    ET.SubElement(get_reports, "sessionToken").text = token
    return ET.tostring(root)


def get_statistic_search_report_soap_request(token, report_guid, max_results):
    # Create the root element
    root = ET.Element(
        "soap:Envelope",
        {
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
            "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
        },
    )
    # Create the soap:Body element
    body = ET.SubElement(root, "soap:Body")
    # Create the ExecuteStatisticSearchByReport element
    execute_statistic_search = ET.SubElement(
        body,
        "ExecuteStatisticSearchByReport",
        {"xmlns": "http://archer-tech.com/webservices/"},
    )
    # Add the sessionToken, reportIdOrGuid and pageNumber elements
    ET.SubElement(execute_statistic_search, "sessionToken").text = token
    ET.SubElement(execute_statistic_search, "reportIdOrGuid").text = report_guid
    ET.SubElement(execute_statistic_search, "pageNumber").text = str(max_results)

    return ET.tostring(root)


def get_search_options_soap_request(token, report_guid):
    # Create the root element
    root = ET.Element(
        "soap:Envelope",
        {
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
            "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
        },
    )
    # Create the soap:Body element
    body = ET.SubElement(root, "soap:Body")
    # Create the GetSearchOptionsByGuid element
    get_search_options_by_grid = ET.SubElement(body, "GetSearchOptionsByGuid", {"xmlns": "http://archer-tech.com/webservices/"})
    # Add the sessionToken and searchReportGuid elements
    ET.SubElement(get_search_options_by_grid, "sessionToken").text = token
    ET.SubElement(get_search_options_by_grid, "searchReportGuid").text = report_guid

    return ET.tostring(root)


def search_records_by_report_soap_request(token, report_guid):
    # Create the root element
    root = ET.Element(
        "soap:Envelope",
        {
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
            "xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
        },
    )
    # Create the soap:Body element
    body = ET.SubElement(root, "soap:Body")
    # Create the SearchRecordsByReport element
    search_records_by_report = ET.SubElement(body, "SearchRecordsByReport", {"xmlns": "http://archer-tech.com/webservices/"})
    # Add the sessionToken, reportIdOrGuid and pageNumber elements
    ET.SubElement(search_records_by_report, "sessionToken").text = token
    ET.SubElement(search_records_by_report, "reportIdOrGuid").text = report_guid
    ET.SubElement(search_records_by_report, "pageNumber").text = "1"

    return ET.tostring(root)


def construct_generic_filter_condition(
    condition_type: FilterConditionTypes,
    operator: str,
    field_name: str,
    field_id: str,
    search_value: str,
    sub_elements_tags_values: dict[str, Any] | None = None,
) -> str:
    """
    Constructs an XML string representing any generic filter condition for searching records.

    Args:
        condition_type (FilterConditionTypes): The type of the filter condition.
        operator (str): The comparison operator (e.g., 'Equals', 'GreaterThan').
        field_name (str): The name of the application field.
        field_id (str): The ID of the application field.
        search_value (str): The value for the comparison.
        sub_elements_tags_values (dict | None): Optional tag names and values to embed as sub-elements.

    Returns:
        str: An XML string representing the FilterCondition element.

    Example:
        >>> construct_generic_filter_condition(FilterConditionTypes.text, 'Equals', 'name', '123', 'John')
        '<TextFilterCondition><Operator>Equals</Operator><Field name="name">123</Field><Value>John</Value></TextFilterCondition>'
    """
    root = ET.Element(condition_type.value)

    ET.SubElement(root, "Operator").text = operator
    ET.SubElement(root, "Field", attrib={"name": field_name}).text = field_id
    ET.SubElement(root, "Value").text = search_value

    for tag_name, tag_text in (sub_elements_tags_values or {}).items():
        ET.SubElement(root, tag_name).text = tag_text

    return ET.tostring(root, encoding="unicode")


def construct_content_filter_condition(operator: str, level_id: str, search_value: str) -> str:
    """
    Constructs an XML string representing a content filter condition for searching records.

    Args:
        operator (str): The comparison operator (e.g., 'Equals', 'GreaterThan').
        field_name (str): The name of the application field.
        field_id (str): The ID of the application field.
        search_value (str): The value for the comparison.

    Returns:
        str: An XML string representing the ContentFilterCondition element.

    Example:
        >>> construct_content_filter_condition('Equals', '123', 'test_value')
        '<ContentFilterCondition><Level>123</Level><Operator>Equals</Operator><Values><Value>test_value</Value></Values></ContentFilterCondition>'
    """
    root = ET.Element(FilterConditionTypes.content.value)

    ET.SubElement(root, "Level").text = level_id
    ET.SubElement(root, "Operator").text = operator

    values_element = ET.SubElement(root, "Values")
    ET.SubElement(values_element, "Value").text = search_value

    return ET.tostring(root, encoding="unicode")


def search_records_soap_request(
    token: str,
    app_id: str,
    display_fields: str,
    field_id: str,
    field_name: str,
    search_value: str | None,
    date_operator: str | None = "",
    field_to_search_by_id: str | None = "",
    numeric_operator: str | None = "",
    max_results: int = 10,
    level_id: str = "",
    sort_type: str = "Ascending",
    xml_filter_conditions: str | None = "",
):
    # CDATA is not supported in Element Tree, therefore keeping original structure.
    request_body = (
        '<?xml version="1.0" encoding="UTF-8"?>' + '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" '
        'xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
        ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
        + "    <soap:Body>"
        + '        <ExecuteSearch xmlns="http://archer-tech.com/webservices/">'
        + f"            <sessionToken>{token}</sessionToken>"
        + "            <searchOptions>"
        + "                <![CDATA[<SearchReport>"
        + f"                <PageSize>{max_results}</PageSize>"
        + "                 <PageNumber>1</PageNumber>"
        + f"                <MaxRecordCount>{max_results}</MaxRecordCount>"
        + "                <ShowStatSummaries>false</ShowStatSummaries>"
        + f"                <DisplayFields>{display_fields}</DisplayFields>"
        + f'             <Criteria><ModuleCriteria><Module name="appname">{app_id}</Module></ModuleCriteria>'
    )

    filter_conditions: list[str] = []  # API uses "AND" logical operator by default to join multiple filters

    if xml_filter_conditions:
        filter_conditions.append(xml_filter_conditions)

    if search_value:
        if date_operator:
            filter_conditions.append(
                construct_generic_filter_condition(
                    FilterConditionTypes.date,
                    operator=date_operator,
                    field_name=field_name,
                    field_id=field_id,
                    search_value=search_value,
                    sub_elements_tags_values={"TimeZoneId": "UTC Standard Time", "IsTimeIncluded": "TRUE"},
                )
            )

        elif numeric_operator:
            filter_conditions.append(
                construct_generic_filter_condition(
                    FilterConditionTypes.numeric,
                    operator=numeric_operator,
                    field_name=field_name,
                    field_id=field_id,
                    search_value=search_value,
                )
            )

        elif field_to_search_by_id and field_to_search_by_id.lower() == field_name.lower():
            filter_conditions.append(
                construct_content_filter_condition(
                    operator="Equals",
                    level_id=level_id,
                    search_value=search_value,
                )
            )

        else:
            filter_conditions.append(
                construct_generic_filter_condition(
                    FilterConditionTypes.text,
                    operator="Contains",
                    field_name=field_name,
                    field_id=field_id,
                    search_value=search_value,
                )
            )

    if filter_conditions:
        filter_conditions_xml = "\n".join(filter_conditions)
        request_body += f"<Filter><Conditions>{filter_conditions_xml}</Conditions></Filter>"

    if field_id:
        request_body += (
            "<SortFields>"
            + "    <SortField>"
            + f"        <Field>{field_id}</Field>"
            + f"        <SortType>{sort_type}</SortType>"
            + "    </SortField >"
            + "</SortFields>"
        )

    request_body += (
        " </Criteria></SearchReport>]]>"
        + "</searchOptions>"
        + "<pageNumber>1</pageNumber>"
        + "</ExecuteSearch>"
        + "</soap:Body>"
        + "</soap:Envelope>"
    )

    return request_body


SOAP_COMMANDS = {
    "archer-get-reports": {
        "soapAction": "http://archer-tech.com/webservices/GetReports",
        "urlSuffix": "ws/search.asmx",
        "soapBody": get_reports_soap_request,
        "outputPath": "Envelope.Body.GetReportsResponse.GetReportsResult",
    },
    "archer-execute-statistic-search-by-report": {
        "soapAction": "http://archer-tech.com/webservices/ExecuteStatisticSearchByReport",
        "urlSuffix": "ws/search.asmx",
        "soapBody": get_statistic_search_report_soap_request,
        "outputPath": "Envelope.Body.ExecuteStatisticSearchByReportResponse.ExecuteStatisticSearchByReportResult",
    },
    "archer-get-search-options-by-guid": {
        "soapAction": "http://archer-tech.com/webservices/GetSearchOptionsByGuid",
        "urlSuffix": "ws/search.asmx",
        "soapBody": get_search_options_soap_request,
        "outputPath": "Envelope.Body.GetSearchOptionsByGuidResponse.GetSearchOptionsByGuidResult",
    },
    "archer-search-records": {
        "soapAction": "http://archer-tech.com/webservices/ExecuteSearch",
        "urlSuffix": "ws/search.asmx",
        "soapBody": search_records_soap_request,
        "outputPath": "Envelope.Body.ExecuteSearchResponse.ExecuteSearchResult",
    },
    "archer-search-records-by-report": {
        "soapAction": "http://archer-tech.com/webservices/SearchRecordsByReport",
        "urlSuffix": "ws/search.asmx",
        "soapBody": search_records_by_report_soap_request,
        "outputPath": "Envelope.Body.SearchRecordsByReportResponse.SearchRecordsByReportResult",
    },
}


def merge_integration_context(new_dict):
    old_context = get_integration_context()
    old_context.update(new_dict)
    set_integration_context(old_context)


def get_occurred_time(fields: List[dict] | dict, field_id: str) -> str:
    """
    Occurred time is part of the raw 'Field' key in the response.
    It should be under @xmlConvertedValue, but field can be both a list or a dict.

    Arguments:
        fields: Field to find the occurred utc time on
        field_id: The @id in the response the time should be on

    Returns:
         Time of occurrence according to the field ID.
    """
    try:
        field_id = str(field_id)  # In case it passed as a integer
        if isinstance(fields, dict):
            return fields["@xmlConvertedValue"]
        else:
            for field in fields:
                if str(field["@id"]) == field_id:  # In a rare case @id is an integer
                    return str(field["@xmlConvertedValue"])
        raise KeyError("Could not find @xmlConvertedValue in record.")  # No xmlConvertedValue
    except KeyError as exc:
        raise DemistoException(
            f"Could not find the property @xmlConvertedValue in field id {field_id}. Is that a date field?"
        ) from exc


class Client(BaseClient):
    def __init__(self, base_url, username, password, instance_name, domain, timeout, **kwargs):
        self.username = username
        self.password = password
        self.instance_name = instance_name
        self.domain = domain

        super().__init__(base_url=base_url, timeout=timeout, **kwargs)

    def get_headers(self, create_new_session: bool = False):
        """
        This function returns the relevant headers dict which also contains session id. In case the session doesn't exist in
        context or the create_new_session flag is given, the session will ge re-generated using create_session().
        In order to support some level of concurrency when running tasks simultaneously, the function has a small
        sleeping mechanism to allow tasks to first try and use existing session before moving forward to create a new
        one.
        Args:
            create_new_session (bool): whether to force creation of a new session

        Returns:
             dict: the dictionary containing the headers together with the session id.
        """
        time.sleep(random.uniform(0, 5))
        headers = REQUEST_HEADERS
        context_session_id = get_integration_context().get("session_id")
        session_id = self.create_session() if create_new_session or not context_session_id else context_session_id
        headers["Authorization"] = f"Archer session-id={session_id}"
        return headers

    def try_rest_request(
        self,
        method,
        url_suffix,
        data=None,
        params=None,
        create_new_session=False,
        attempts=1,
    ):
        """
        This function perform several attempts to extract the necessary headers and call the Base client http request
        function. If the create_new_session flag is given it will enforce the creation of a new session, otherwise it
        will try to use the existing one.

        Args:
            method: (str) the HTTP method to use
            url_suffix: (str) the url_suffix to use
            data: (str) the to send in the json body
            params: (str) the url parameters to send
            create_new_session: (bool) whether to enforce creation of new session (will be true in case previous calls
                returned 401)
            attempts: (int) number of attempts to try with the given session extraction/method.

        Returns:
            requests.Response: the response object
        """
        demisto.debug(f"Sending {method.upper()} request to REST endpoint: {url_suffix} with params: {params} and body: {data}")

        for _ in range(attempts):
            headers = self.get_headers(create_new_session=create_new_session)
            res = self._http_request(
                method,
                url_suffix,
                headers=headers,
                json_data=data,
                params=params,
                resp_type="response",
                ok_codes=(200, 401),
            )
            demisto.debug(f"rest status code: {res.status_code}")
            if 200 <= res.status_code <= 300:
                break
        return res

    def do_rest_request(self, method, url_suffix, data=None, params=None):
        """
        This function manages the REST API calls by calling the *try_rest_request* function twice:
            - First without the *create_new_session* flag (this will cause *try_rest_request* to try and use exiting
            session id if exists).
            - In case of bad session (401), another call will be made with the *create_new_session* flag set to true
            which performs force update of the session id.

        Args:
            method: (str) the HTTP method to use
            url_suffix: (str) the url_suffix to use
            data: (dict) the data to send in the json body
            params: (dict) the url parameters to send

        Returns:
            dict: the response json object
        """
        res = self.try_rest_request(method=method, url_suffix=url_suffix, data=data, params=params, attempts=2)
        if res.status_code == 401:
            demisto.debug("trying rest with new session")
            res = self.try_rest_request(
                method=method,
                url_suffix=url_suffix,
                data=data,
                params=params,
                create_new_session=True,
                attempts=4,
            )
        return res.json()

    def create_session(self):
        body = {
            "InstanceName": self.instance_name,
            "Username": self.username,
            "UserDomain": self.domain,
            "Password": self.password,
        }
        try:
            res = self._http_request("POST", f"{API_ENDPOINT}/core/security/login", json_data=body)
        except DemistoException as e:
            if "<html>" in str(e):
                raise DemistoException(f"Check the given URL, it can be a redirect issue. Failed with error: {e!s}")
            raise e
        is_successful_response = res.get("IsSuccessful")
        if not is_successful_response:
            return_error(res.get("ValidationMessages"))
        session = res.get("RequestedObject", {}).get("SessionToken")
        merge_integration_context({"session_id": session})
        return session

    def generate_token(self):
        endpoint = "CreateDomainUserSessionFromInstance" if self.domain else "CreateUserSessionFromInstance"

        body = get_token_soap_request(self.username, self.password, self.instance_name, self.domain)
        headers = {
            "SOAPAction": f"http://archer-tech.com/webservices/{endpoint}",
            "Content-Type": "text/xml; charset=utf-8",
        }
        res = self._http_request("POST", "ws/general.asmx", headers=headers, data=body, resp_type="content")
        token = extract_from_xml(res, f"Envelope.Body.{endpoint}Response.{endpoint}Result")
        merge_integration_context({"token": token})
        return token

    def update_body_with_token(self, request_body_builder_function, create_new_token: bool = False, **kwargs):
        """
        This function returns the updated body dict which also contains api token. In case the token doesn't exist in
        context or the create_new_token flag is given, the token will be re-generated using generate_token().
        In order to support some level of concurrency when running tasks simultaneously, the function has a small
        sleeping mechanism to allow tasks to first try and use existing session before moving forward to create a new
        one.
        Args:
            request_body_builder_function (function): function to build the relevant request body
            create_new_token (bool): whether to force creation of a new session
            kwargs: (dict) dict of additional parameters relevant to the soap request.

        Returns:
             dict: the dictionary containing the necessary body together with the api token.
        """
        time.sleep(random.uniform(0, 5))
        context_token = get_integration_context().get("token")
        token = self.generate_token() if create_new_token or not context_token else context_token
        body = request_body_builder_function(token, **kwargs)
        return body

    def try_soap_request(self, req_data, method, create_new_token=False, attempts=1, **kwargs):
        """
        This function perform several attempts to read/generate api token and call the Base client http request
        function. If the create_new_token flag is given it will enforce the creation of a new token, otherwise it
        will try to use the existing one.

        Args:
            method: (str) the HTTP method to use
            req_data: (dict) dictionary containing API info relevant to the specific API request
            create_new_token: (bool) whether to enforce creation of new session (will be true in case previous calls
                returned 500)
            attempts: (int) number of attempts to try with the given session extraction/method.
            kwargs: (dict) dict of additional parameters relevant to the soap request.

        Returns:
            requests.Response: the response object
        """
        headers = {
            "SOAPAction": req_data["soapAction"],
            "Content-Type": "text/xml; charset=utf-8",
        }
        request_body_builder_function = req_data["soapBody"]
        url_suffix = req_data["urlSuffix"]
        for _ in range(attempts):
            body = self.update_body_with_token(
                request_body_builder_function=request_body_builder_function,
                create_new_token=create_new_token,
                **kwargs,
            )
            res = self._http_request(
                method=method,
                url_suffix=url_suffix,
                headers=headers,
                data=body,
                resp_type="response",
                ok_codes=(200, 500),
            )
            demisto.debug(f"soap status code: {res.status_code}")
            if 200 <= res.status_code <= 300:
                return res
        return res

    def do_soap_request(self, command, **kwargs):
        """
        This function manages the SOAP API calls by calling the *try_soap_request* function twice:
            - First without the *create_new_token* flag (this will cause *try_soap_request* to try and use exiting
            token if exists).
            - In case of bad session (500), another call will be made with the *create_new_token* flag set to true
            which performs force update of the token.

        Args:
            command: (str) the name of the command to use
            kwargs: (dict) dict of additional parameters relevant to the soap request.

        Returns:
            dict: the relevant dict containing the data in the relevant path of the xml response
            bytes: res.content
        """
        req_data = SOAP_COMMANDS[command]
        res = self.try_soap_request(req_data=req_data, method="POST", attempts=2, **kwargs)
        if res.status_code == 500:
            demisto.debug("trying soap with new session")
            res = self.try_soap_request(
                req_data=req_data,
                method="POST",
                create_new_token=True,
                attempts=2,
                **kwargs,
            )
        return extract_from_xml(res.content, req_data["outputPath"]), res.content

    def get_level_by_app_id(self, app_id, specify_level_id=None):
        levels = []
        cache = get_integration_context()

        if cache.get(app_id):
            levels = cache[app_id]
        else:
            all_levels_res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/system/level/module/{app_id}")
            for level in all_levels_res:
                if level.get("RequestedObject") and level.get("IsSuccessful"):
                    level_id = level.get("RequestedObject").get("Id")

                    fields = {}
                    level_res = self.do_rest_request(
                        "GET",
                        f"{API_ENDPOINT}/core/system/fielddefinition/level/{level_id}",
                    )
                    for field in level_res:
                        if field.get("RequestedObject") and field.get("IsSuccessful"):
                            field_item = field.get("RequestedObject")
                            field_id = str(field_item.get("Id"))
                            fields[field_id] = {
                                "Type": field_item.get("Type"),
                                "Name": field_item.get("Name"),
                                "FieldId": field_id,
                                "IsRequired": field_item.get("IsRequired", False),
                                "RelatedValuesListId": field_item.get("RelatedValuesListId"),
                            }

                    levels.append({"level": level_id, "mapping": fields})
            if levels:
                cache[int(app_id)] = levels
                merge_integration_context(cache)

        level_data = None
        if specify_level_id:
            level_data = next(
                (level for level in levels if level.get("level") == int(specify_level_id)),
                None,
            )
        elif levels:
            level_data = levels[0]

        if not level_data:
            raise DemistoException("Got no level by app id. You might be using the wrong application id or level id.")

        return level_data

    def get_record(self, app_id, record_id, depth):
        res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/content/{record_id}")

        if not isinstance(res, dict):
            res = res.json()

        errors = get_errors_from_res(res)
        record = {}
        if res.get("RequestedObject") and res.get("IsSuccessful"):
            content_obj = res.get("RequestedObject")
            level_id = content_obj.get("LevelId")
            level = self.get_level_by_app_id(app_id, level_id)
            if level:
                level_fields = level["mapping"]
            else:
                return {}, res, errors

            for i, (_id, field) in enumerate(content_obj.get("FieldContents").items()):
                field_data = level_fields.get(str(_id), {})  # type: ignore
                field_type = field_data.get("Type")

                # when field type is IP Address
                if field_type == 19:
                    field_value = field.get("IpAddressBytes")
                # when field type is Values List
                elif field_type == 4 and field.get("Value") and field["Value"].get("ValuesListIds"):
                    list_data = self.get_field_value_list(_id, depth)
                    list_ids = field["Value"]["ValuesListIds"]
                    list_ids = list(filter(lambda x: x["Id"] in list_ids, list_data["ValuesList"]))
                    field_value = [x["Name"] for x in list_ids]
                else:
                    field_value = field.get("Value")

                if field_value:
                    if not field_data.get("Name"):
                        demisto.debug(f"{field_data.get('Name')=}\n{field_data.get('Value')=}")
                    record[field_data.get("Name") or self.get_field_value_name(_id) or f"None-{i}"] = field_value

            record["Id"] = content_obj.get("Id")
        return record, res, errors

    @staticmethod
    def record_to_incident(record_item, app_id, fetch_param_id) -> tuple[dict, datetime]:
        """Transform a record to incident

        Args:
            record_item: The record item dict
            app_id: ID of the app
            fetch_param_id: ID of the fetch param.

        Returns:
            incident, incident created time (UTC Time)
        """
        labels = []
        raw_record = record_item["raw"]
        record_item = record_item["record"]
        try:
            occurred_time = get_occurred_time(raw_record["Field"], fetch_param_id)
        except KeyError as exc:
            raise DemistoException(f'Could not find occurred time in record {record_item.get("Id")=}') from exc
        # Will convert value to strs
        for k, v in record_item.items():
            if isinstance(v, str):
                labels.append({"type": k, "value": v})
            else:
                labels.append({"type": k, "value": json.dumps(v)})

        labels.append({"type": "ModuleId", "value": app_id})
        labels.append({"type": "ContentId", "value": record_item.get("Id")})
        labels.append({"type": "rawJSON", "value": json.dumps(raw_record)})
        incident = {
            "name": f'RSA Archer Incident: {record_item.get("Id")}',
            "details": json.dumps(record_item),
            "occurred": occurred_time,
            "labels": labels,
            "rawJSON": json.dumps(raw_record),
        }
        return incident, parser(occurred_time)

    def search_records(
        self,
        app_id: str,
        fields_to_display: list[str] | None = None,
        field_to_search: str | None = "",
        search_value: str | None = "",
        field_to_search_by_id: str | None = "",
        numeric_operator: str | None = "",
        date_operator: str | None = "",
        max_results: int = 10,
        sort_type: str = "Ascending",
        xml_filter_conditions: str | None = "",
    ):
        demisto.debug(f"searching for records {field_to_search}:{search_value}")
        if fields_to_display is None:
            fields_to_display = []

        level_data = self.get_level_by_app_id(app_id)

        # Building request fields
        fields_xml = ""
        search_field_name = ""
        search_field_id = ""
        fields_mapping = level_data["mapping"]
        level_id = level_data["level"]
        for field in fields_mapping:
            field_name = fields_mapping[field]["Name"]
            if field_name in fields_to_display:
                fields_xml += f'<DisplayField name="{field_name}">{field}</DisplayField>'
            if (field_to_search and field_name.lower() == field_to_search.lower()) or (
                field_to_search_by_id and field_name.lower() == field_to_search_by_id.lower()
            ):
                search_field_name = field_name
                search_field_id = field

        res, raw_res = self.do_soap_request(
            "archer-search-records",
            app_id=app_id,
            display_fields=fields_xml,
            field_id=search_field_id,
            field_name=search_field_name,
            field_to_search_by_id=field_to_search_by_id,
            numeric_operator=numeric_operator,
            date_operator=date_operator,
            search_value=search_value,
            max_results=max_results,
            sort_type=sort_type,
            level_id=level_id,
            xml_filter_conditions=xml_filter_conditions,
        )

        if not res:
            return [], raw_res

        records = self.xml_to_records(res, fields_mapping)
        return records, raw_res

    def xml_to_records(self, xml_response, fields_mapping):
        res = json.loads(xml2json(xml_response))
        records = []
        if res.get("Records") and res["Records"].get("Record"):
            records_data = res["Records"]["Record"]
            if isinstance(records_data, dict):
                records_data = [records_data]

            for item in records_data:
                record = {"Id": item.get("@contentId")}
                record_fields = item.get("Field")

                if isinstance(record_fields, dict):
                    record_fields = [record_fields]

                for field in record_fields:
                    field_name = fields_mapping[field.get("@id")]["Name"]
                    field_type = field.get("@type")
                    field_value = ""
                    if field_type == "3":
                        field_value = field.get("@xmlConvertedValue")
                    elif field_type == "4":
                        if field.get("ListValues"):
                            field_value = field["ListValues"]["ListValue"]["@displayName"]
                    elif field_type == "8":
                        field_value = json.dumps(field)
                    else:
                        field_value = field.get("#text")

                    record[field_name] = field_value
                records.append({"record": record, "raw": item})
        return records

    def get_field_value_list_helper(self, child, values_list, depth, parent="root"):
        values_list.append(
            {
                "Id": child["Data"]["Id"],
                "Name": child["Data"]["Name"],
                "IsSelectable": child["Data"]["IsSelectable"],
                "Parent": parent,
                "Depth": child.get("Depth"),
            }
        )
        depth -= 1
        if depth > -1:
            for grandchild in child.get("Children", []):
                self.get_field_value_list_helper(grandchild, values_list, depth, child["Data"]["Name"])

    def get_field_value_list(self, field_id, depth=0):
        cache = get_integration_context()
        cache_key = f"{field_id}__{depth}"  # Value list depends on both field ID and depth

        # Get the value from cache using both field ID and depth (if exists)
        if cached_field_value_list := cache.get("fieldValueList", {}).get(cache_key):
            demisto.debug(f"Getting field value list for field ID: {field_id} and depth: {depth} from integration context.")
            return cached_field_value_list

        # If the value does not exist in cache, get it from the API
        res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/{field_id}")

        if errors := get_errors_from_res(res):
            return_error(errors)

        if res.get("RequestedObject") and res.get("IsSuccessful"):
            if res.get("RequestedObject").get("Type") != 4:
                raise Exception('The command returns values only for fields of type "Values List".\n')

            list_id = res["RequestedObject"]["RelatedValuesListId"]
            values_list_res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/system/valueslistvalue/valueslist/{list_id}")

            if values_list_res.get("RequestedObject") and values_list_res.get("IsSuccessful"):
                values_list: List[dict[str, Any]] = []
                for value in values_list_res["RequestedObject"].get("Children", ()):
                    self.get_field_value_list_helper(value, values_list, depth)

                field_data = {"FieldId": field_id, "ValuesList": values_list}

                # Write the value to cache so it can be retrieved next time using both field ID and depth
                cache["fieldValueList"][cache_key] = field_data
                demisto.debug(f"Merging field value list for field ID: {field_id} and depth: {depth} into integration context.")
                merge_integration_context(cache)
                return field_data
        return {}

    def get_field_value_name(self, field_id):
        cache = get_integration_context()

        if cache["fieldValueNames"].get(field_id):
            return cache.get("fieldValueNames").get(field_id)
        res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/{field_id}")
        errors = get_errors_from_res(res)
        if errors:
            return_error(errors)

        if res.get("RequestedObject") and res.get("IsSuccessful"):
            field_obj = res["RequestedObject"]
            cache["fieldValueNames"][field_obj.get("Id")] = field_obj.get("Name")
            merge_integration_context(cache)
            return field_obj.get("Name")
        return field_id

    def get_field_id(self, app_id: str, field_name: str) -> str:
        """Get field ID by field name

        Args:
            app_id: app id to search on
            field_name: field name to search on

        Raises:
            DemistoException: If could not find field ID

        Returns:
            The ID of the field
        """
        fields, _ = self.get_application_fields(app_id)
        for field in fields:
            if field_name == field.get("FieldName"):
                try:
                    return str(field["FieldId"])
                except KeyError:
                    raise DemistoException(f"Could not find FieldId for {field_name=}")
        raise DemistoException(f"Could not find field ID {field_name}")

    def get_application_fields(self, app_id: str) -> tuple[list, list]:
        """Getting all fields in the application

        Args:
            app_id: Application to find the fields on

        Returns:
            fields, raw response
        """
        res = self.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/application/{app_id}")

        fields = []
        for field in res:
            if field.get("RequestedObject") and field.get("IsSuccessful"):
                field_obj = field["RequestedObject"]
                field_type = field_obj.get("Type")
                fields.append(
                    {
                        "FieldId": field_obj.get("Id"),
                        "FieldType": FIELD_TYPE_DICT.get(field_type, "Unknown"),
                        "FieldName": field_obj.get("Name"),
                        "LevelID": field_obj.get("LevelId"),
                    }
                )
            else:
                errors = get_errors_from_res(field)
                if errors:
                    raise DemistoException(errors)
        return fields, res


def extract_from_xml(xml, path):
    xml = json.loads(xml2json(xml))
    path = path.split(".")

    for item in path:
        if xml.get(item):
            xml = xml[item]
            continue
        return ""
    return xml


def generate_field_contents(client, fields_values, level_fields, depth):
    if fields_values and not isinstance(fields_values, dict):
        demisto.debug(f"Fields values string before escaping: {fields_values}")

        """
        PRE-PROCESSING: Handle even number of backslashes (2, 4, 6, etc.) before quotes
        Pattern: (\\\\)+ matches pairs of backslashes (each \\\\ in regex = \\ in string = one pair)
        When we have an even number of backslashes before a quote, the quote is NOT escaped
        and will terminate the string. We need to escape it.
        Example: \\" in string = one backslash + quote (quote terminates) -> need \\\" = one backslash + escaped quote
        """
        fields_values = re.sub(r'((?:\\\\)+)"', lambda m: m.group(1) + r"\"", fields_values)
        demisto.debug(f"Fields values string after pre-processing: {fields_values}")

        # Pattern explanation:
        # Group 1 (valid): Matches any valid JSON escape sequence (e.g., \", \\, \n, \u1234)
        # Group 2 (invalid): Matches any remaining backslash that wasn't captured in Group 1
        pattern = r'(?P<valid>\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})|(?P<invalid>\\)'

        def fix_escape(match):
            # If it matched a valid escape sequence (Group 1), keep it exactly as is.
            if match.group("valid"):
                return match.group("valid")
            # If it matched an invalid backslash (Group 2), double escape it.
            return "\\\\"

        # Apply the substitution using the callback
        fields_values = re.sub(pattern, fix_escape, fields_values)
        demisto.debug(f"Fields values string after escaping: {fields_values}")

        try:
            demisto.debug(f"Loading JSON fields values string: {fields_values}")
            fields_values = json.loads(fields_values)
        except Exception as e:
            raise Exception(f"Failed to load JSON fields values string. Error: {str(e)}")
        else:
            demisto.debug("Successfully loaded JSON fields values string")

    field_content = {}
    for field_name in fields_values:
        field_data = None

        for _id, field in level_fields.items():
            if field.get("Name") == field_name:
                field_data = field
                break

        if field_data:
            field_key, field_value = generate_field_value(client, field_name, field_data, fields_values[field_name], depth)

            field_content[_id] = {
                "Type": field_data["Type"],
                field_key: field_value,
                "FieldId": _id,
            }
    return field_content


def generate_field_value(client, field_name, field_data, field_val, depth):
    field_type = field_data["Type"]

    # when field type is Values List, call get_field_value_list method to get the value ID
    # for example: {"Type":["Switch"], fieldname:[value1, value2]}
    if field_type == 4:
        field_data = client.get_field_value_list(field_data["FieldId"], depth)
        list_ids = []
        other_text = None
        if isinstance(field_val, dict) and (other_text := field_val.get("OtherText", None)):
            field_val = field_val.get("ValuesList")
        if not isinstance(field_val, list):
            field_val = [field_val]
        for item in field_val:
            tmp_id = next((f for f in field_data["ValuesList"] if f["Name"] == item), None)
            if tmp_id:
                list_ids.append(tmp_id["Id"])
            else:
                raise Exception(f"Failed to create the field: {field_name} with the value: {item}")
        res = {"ValuesListIds": list_ids}
        if other_text:
            res["OtherText"] = other_text
        return "Value", res

    # when field type is External Links
    # for example: {"Patch URL":[{"value":"github", "link": "https://github.com"}]}
    elif field_type == 7:
        list_urls = []
        for item in field_val:
            list_urls.append({"Name": item.get("value"), "URL": item.get("link")})
        return "Value", list_urls

    # when field type is Users/Groups List
    # for example: {"Policy Owner":{"users":[20],"groups":[30]}}
    elif field_type == 8:
        try:
            users = field_val.get("users")
            groups = field_val.get("groups")
        except AttributeError:
            raise DemistoException(
                f"The value of the field: {field_name} must be a dictionary type and include a list"
                f' under "users" key or "groups" key e.g: {{"Policy Owner":{{"users":[20],'
                f'"groups":[30]}}}}'
            )

        field_val = {"UserList": [], "GroupList": []}
        if users:
            for user in users:
                field_val["UserList"].append({"ID": user})
        if groups:
            for group in groups:
                field_val["GroupList"].append({"ID": group})
        return "Value", field_val

    # when field type is Cross- Reference
    # for example: {"Area Reference(s)":[20]}
    elif field_type == 9:
        list_cross_reference = []
        if isinstance(field_val, list):
            for content in field_val:
                list_cross_reference.append({"ContentID": content})

        else:
            list_cross_reference = [{"ContentID": field_val}]
        return "Value", list_cross_reference

    elif field_type == 19:
        return "IpAddressBytes", field_val

    else:
        return "Value", field_val


def get_errors_from_res(res):
    if isinstance(res, dict) and res.get("ValidationMessages"):
        messages = []
        for message in res.get("ValidationMessages"):  # type: ignore
            messages.append(message.get("ResourcedMessage"))
        return "\n".join(messages)
    return None


def get_file(entry_id):
    get_file_path_res = demisto.getFilePath(entry_id)
    file_path = get_file_path_res["path"]
    file_name = get_file_path_res["name"]
    with open(file_path, "rb") as fopen:
        file_bytes = fopen.read()

    file_bytes = base64.b64encode(file_bytes)
    return file_name, file_bytes.decode("utf-8")


def test_module(client: Client, params: dict) -> str:
    if params.get("isFetch", False):
        last_run = {FETCH_PARAM_ID_KEY: get_fetch_param_id(client, {}, params["applicationId"], params["applicationDateField"])}
        fetch_incidents_command(client, params, last_run)
        return "ok"

    return "ok" if client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/application") else "Connection failed."


def search_applications_command(client: Client, args: dict[str, str]):
    app_id = args.get("applicationId")
    limit = args.get("limit")
    endpoint_url = f"{API_ENDPOINT}/core/system/application/"

    res: dict | list[dict] = {}
    if app_id:
        endpoint_url = f"{API_ENDPOINT}/core/system/application/{app_id}"
        res = client.do_rest_request("GET", endpoint_url)
    elif limit:
        res = client.do_rest_request("GET", endpoint_url, params={"$top": limit})
    else:
        res = {}
        demisto.debug(f"No condition was met {res=}")

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if isinstance(res, dict):
        res = [res]

    applications = []
    for app in res:
        if app.get("RequestedObject") and app.get("IsSuccessful"):
            app_obj = app["RequestedObject"]
            applications.append(
                {
                    "Id": app_obj.get("Id"),
                    "Type": app_obj.get("Type"),
                    "Name": app_obj.get("Name"),
                    "LanguageId": app_obj.get("LanguageId"),
                    "Status": app_obj.get("Status"),
                    "Guid": app_obj.get("Guid"),
                }
            )

    markdown = tableToMarkdown("Search applications results", applications)
    context: dict = {"Archer.Application(val.Id && val.Id == obj.Id)": applications}
    return_outputs(markdown, context, res)


def get_application_fields_command(client: Client, args: dict[str, str]):
    app_id = args["applicationId"]
    fields, res = client.get_application_fields(app_id)

    markdown = tableToMarkdown("Application fields", fields)
    context: dict = {"Archer.ApplicationField(val.FieldId && val.FieldId == obj.FieldId)": fields}
    return_outputs(markdown, context, res)


def get_field_command(client: Client, args: dict[str, str]):
    field_id = args.get("fieldID")

    res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/{field_id}")

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    field = {}
    if res.get("RequestedObject") and res.get("IsSuccessful"):
        field_obj = res["RequestedObject"]
        item_type = field_obj.get("Type")
        item_type = FIELD_TYPE_DICT.get(item_type, "Unknown")
        field = {
            "FieldId": field_obj.get("Id"),
            "FieldType": item_type,
            "FieldName": field_obj.get("Name"),
            "LevelID": field_obj.get("LevelId"),
        }

    markdown = tableToMarkdown("Application field", field)
    context: dict = {"Archer.ApplicationField(val.FieldId && val.FieldId == obj.FieldId)": field}
    return_outputs(markdown, context, res)


def get_mapping_by_level_command(client: Client, args: dict[str, str]):
    level = args.get("level")

    res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/level/{level}")

    items = []
    for item in res:
        if item.get("RequestedObject") and item.get("IsSuccessful"):
            item_obj = item["RequestedObject"]
            item_type = item_obj.get("Type")
            item_type = FIELD_TYPE_DICT.get(item_type, "Unknown") if item_type else "Unknown"
            items.append(
                {
                    "Id": item_obj.get("Id"),
                    "Name": item_obj.get("Name"),
                    "Type": item_type,
                    "LevelId": item_obj.get("LevelId"),
                }
            )
        else:
            errors = get_errors_from_res(item)
            if errors:
                return_error(errors)

    markdown = tableToMarkdown(f"Level mapping for level {level}", items)
    context: dict = {"Archer.LevelMapping(val.Id && val.Id == obj.Id)": items}
    return_outputs(markdown, context, res)


def get_record_command(client: Client, args: dict[str, str]):
    record_id = args.get("contentId")
    app_id = args.get("applicationId")

    depth = arg_to_number(args.get("depth", "0"))
    record, res, errors = client.get_record(app_id, record_id, depth)
    if errors:
        return_error(errors)

    markdown = tableToMarkdown("Record details", record)
    context: dict = {"Archer.Record(val.Id && val.Id == obj.Id)": record}
    return_outputs(markdown, context, res)


def create_record_command(client: Client, args: dict[str, str]):
    app_id = args.get("applicationId")
    fields_values = args.get("fieldsToValues")
    level_id = args.get("levelId")
    level_data = client.get_level_by_app_id(app_id, level_id)
    depth = arg_to_number(args.get("depth", "0"))
    field_contents = generate_field_contents(client, fields_values, level_data["mapping"], depth)

    body = {"Content": {"LevelId": level_data["level"], "FieldContents": field_contents}}

    res = client.do_rest_request("Post", f"{API_ENDPOINT}/core/content", data=body)

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if res.get("RequestedObject") and res.get("IsSuccessful"):
        rec_id = res["RequestedObject"]["Id"]
        return_outputs(
            f"Record created successfully, record id: {rec_id}",
            {"Archer.Record.Id": rec_id},
            res,
        )


def delete_record_command(client: Client, args: dict[str, str]):
    record_id = args.get("contentId")
    res = client.do_rest_request("Delete", f"{API_ENDPOINT}/core/content/{record_id}")

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)
    return_outputs(f"Record {record_id} deleted successfully", {}, res)


def update_record_command(client: Client, args: dict[str, str]):
    app_id = args.get("applicationId")
    record_id = args.get("contentId")
    fields_values = args.get("fieldsToValues")
    level_id = args.get("levelId")
    level_data = client.get_level_by_app_id(app_id, level_id)
    depth = arg_to_number(args.get("depth", "0"))
    field_contents = generate_field_contents(client, fields_values, level_data["mapping"], depth)

    body = {
        "Content": {
            "Id": record_id,
            "LevelId": level_data["level"],
            "FieldContents": field_contents,
        }
    }
    res = client.do_rest_request("Put", f"{API_ENDPOINT}/core/content", data=body)

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if res.get("IsSuccessful"):
        return_outputs(f"Record {record_id} updated successfully", {}, res)
    else:
        raise DemistoException("Update record failed")


def execute_statistics_command(client: Client, args: dict[str, str]):
    report_guid = args.get("reportGuid")
    max_results = args.get("maxResults")
    res, raw_res = client.do_soap_request(
        "archer-execute-statistic-search-by-report",
        report_guid=report_guid,
        max_results=max_results,
    )
    if res:
        res = json.loads(xml2json(res))
    return_outputs(res, {}, {})


def get_reports_command(client: Client, args: dict[str, str]):
    res, raw_res = client.do_soap_request("archer-get-reports")
    res = json.loads(xml2json(res))
    ec = res.get("ReportValues").get("ReportValue")

    context: dict = {"Archer.Report(val.ReportGUID && val.ReportGUID == obj.ReportGUID)": ec}
    return_outputs(ec, context, json.loads(xml2json(raw_res)))


def search_options_command(client: Client, args: dict[str, str]):
    report_guid = args.get("reportGuid")
    res, raw_res = client.do_soap_request("archer-get-search-options-by-guid", report_guid=report_guid)
    if res.startswith("<"):
        res = json.loads(xml2json(res))
    return_outputs(res, {}, {})


def reset_cache_command(client: Client, args: dict[str, str]):
    set_integration_context({})
    return_outputs("", {}, "")


def get_value_list_command(client: Client, args: dict[str, str]):
    field_id = args.get("fieldID")
    depth = arg_to_number(args.get("depth", "0"))
    field_data = client.get_field_value_list(field_id, depth)

    markdown = tableToMarkdown(f"Value list for field {field_id}", field_data["ValuesList"])

    context: dict = {"Archer.ApplicationField(val.FieldId && val.FieldId == obj.FieldId)": field_data}
    return_outputs(markdown, context, field_data)


def upload_file_command(client: Client, args: dict[str, str]) -> str:
    """Uploading a file to archer as an attachment

    Arguments:
        client: A client to use in order to send the api callarcher-get-file
        args: demisto args

    Returns:
        An attachment id from Archer
    """
    entry_id = args.get("entryId")
    file_name, file_bytes = get_file(entry_id)
    body = {"AttachmentName": file_name, "AttachmentBytes": file_bytes}

    res = client.do_rest_request("POST", f"{API_ENDPOINT}/core/content/attachment", data=body)

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if res.get("RequestedObject") and res.get("IsSuccessful"):
        attachment_id = res["RequestedObject"].get("Id")
    else:
        raise DemistoException("Upload file failed")

    return_outputs(f"File uploaded successfully, attachment ID: {attachment_id}", {}, res)
    return attachment_id


def upload_and_associate_command(client: Client, args: dict[str, str]):
    """Uploading an entry to archer. than, if needed, associate it to a record."""
    app_id = args.get("applicationId")
    content_id = args.get("contentId")
    associate_field = args.get("associatedField")

    should_associate_to_record = app_id and content_id
    if not should_associate_to_record and (
        app_id or content_id
    ):  # If both app_id and content_id and one of app_id or content_id, raise error. User's mistake
        raise DemistoException("Found arguments to associate an attachment to a record, but not all required arguments supplied")

    entry_ids: list = argToList(args.get("entryId"))
    attachment_ids: list = []
    for entry_id in entry_ids:
        attachment_ids.append(upload_file_command(client, {"entryId": entry_id}))
    demisto.debug(f"All new uploaded {attachment_ids=}")

    if should_associate_to_record:
        # Check if there are already attachments associated with this record.
        record, _, errors = client.get_record(app_id, content_id, 0)
        if errors:
            return_error(errors)
        record_attachments = record.get(associate_field, []) or []
        demisto.debug(f"Record id {content_id} already has {record_attachments=} will add the new {attachment_ids=} as well")
        attachment_ids.extend(record_attachments)
        demisto.debug(f"All {attachment_ids=}")
        args["fieldsToValues"] = json.dumps({associate_field: attachment_ids})
        update_record_command(client, args)


def download_file_command(client: Client, args: dict[str, str]):
    attachment_id = args.get("fileId")
    res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/content/attachment/{attachment_id}")

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if res.get("RequestedObject") and res.get("IsSuccessful"):
        content = base64.b64decode(res["RequestedObject"].get("AttachmentBytes"))
        filename = res["RequestedObject"].get("AttachmentName")
        return demisto.results(fileResult(filename, content))
    else:
        return_error("File downloading failed", outputs=res)
        return None


def list_users_command(client: Client, args: dict[str, str]):
    user_id = args.get("userId")
    if user_id:
        res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/user/{user_id}")
    else:
        res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/user")

    errors = get_errors_from_res(res)
    if errors:
        return_error(errors)

    if isinstance(res, dict):
        res = [res]
    users = []
    for user in res:
        if user.get("RequestedObject") and user.get("IsSuccessful"):
            user_obj = user["RequestedObject"]
            users.append(
                {
                    "Id": user_obj.get("Id"),
                    "DisplayName": user_obj.get("DisplayName"),
                    "FirstName": user_obj.get("FirstName"),
                    "MiddleName": user_obj.get("MiddleName"),
                    "LastName": user_obj.get("LastName"),
                    "AccountStatus": ACCOUNT_STATUS_DICT[user_obj.get("AccountStatus")],
                    "LastLoginDate": user_obj.get("LastLoginDate"),
                    "UserName": user_obj.get("UserName"),
                }
            )

    markdown = tableToMarkdown("Users list", users)
    context: dict = {"Archer.User(val.Id && val.Id == obj.Id)": users}
    return_outputs(markdown, context, res)


def validate_xml_conditions(xml_conditions: str, blacklisted_tags: list[str] | None = None) -> None:
    """
    Validates if the string is syntactically valid XML document and if the XML conditions contain any forbidden tags.

    Args:
        xml_conditions (str): String for checking.
        blacklisted_tags (list): List of forbidden XML tags.

    Raises:
        ValueError: If invalid syntax or any forbidden XML tag is used,
    """
    blacklisted_tags = blacklisted_tags or []

    try:
        root = ET.fromstring(f"<Conditions>{xml_conditions}</Conditions>")
    except ET.ParseError:
        raise ValueError("Invalid XML filter condition syntax")

    for blacklisted_tag in blacklisted_tags:
        if root.find(blacklisted_tag) is not None:
            raise ValueError(f'XML filter condition cannot contain the "{blacklisted_tag}" tag')


def search_records_command(client: Client, args: dict[str, str]):
    app_id = args["applicationId"]
    field_to_search = args.get("fieldToSearchOn")
    field_to_search_by_id = args.get("fieldToSearchById")
    search_value = args.get("searchValue")
    max_results = arg_to_number(args.get("maxResults")) or 10
    date_operator = args.get("dateOperator")
    numeric_operator = args.get("numericOperator")
    fields_to_display = argToList(args.get("fieldsToDisplay"))
    fields_to_get = argToList(args.get("fieldsToGet"))
    full_data = argToBoolean(args.get("fullData"))
    sort_type = "Descending" if argToBoolean(args.get("isDescending", "false")) else "Ascending"
    level_id = args.get("levelId")

    if xml_filter_conditions := args.get("xmlForFiltering"):
        validate_xml_conditions(xml_filter_conditions)

    if fields_to_get and "Id" not in fields_to_get:
        fields_to_get.append("Id")

    if not all(f in fields_to_get for f in fields_to_display):
        return_error("fields-to-display param should have only values from fields-to-get")

    if full_data:
        level_data = client.get_level_by_app_id(app_id, level_id)
        fields_mapping = level_data["mapping"]
        fields_to_get = [fields_mapping[next(iter(fields_mapping))]["Name"]]

    records, raw_res = client.search_records(
        app_id,
        fields_to_get,
        field_to_search,
        search_value,
        field_to_search_by_id,
        numeric_operator,
        date_operator,
        max_results=max_results,
        sort_type=sort_type,
        xml_filter_conditions=xml_filter_conditions,
    )

    records = [x["record"] for x in records]

    if full_data:
        records_full = []
        depth = arg_to_number(args.get("depth", "0"))
        for rec in records:
            record_item, _, errors = client.get_record(app_id, rec["Id"], depth)
            if not errors:
                records_full.append(record_item)
        records = records_full

    hr = []

    if full_data:
        hr = records
    else:
        for record in records:
            hr.append({f: record[f] for f in fields_to_display})

    markdown = tableToMarkdown("Search records results", hr)
    context: dict = {"Archer.Record(val.Id && val.Id == obj.Id)": records}
    return_outputs(markdown, context, json.loads(xml2json(raw_res)))


def search_records_by_report_command(client: Client, args: dict[str, str]):
    report_guid = args.get("reportGuid")
    res, raw_res = client.do_soap_request("archer-search-records-by-report", report_guid=report_guid)
    if not res:
        return_outputs(
            f"No records found for report {report_guid}",
            {},
            json.loads(xml2json(raw_res)),
        )
        return

    raw_records = json.loads(xml2json(res))
    records = []
    ec = {}
    if raw_records.get("Records") and raw_records["Records"].get("Record"):
        if isinstance(raw_records["Records"].get("Record"), list):
            level_id = raw_records["Records"]["Record"][0]["@levelId"]
        else:
            level_id = raw_records["Records"]["Record"]["@levelId"]

        level_res = client.do_rest_request("GET", f"{API_ENDPOINT}/core/system/fielddefinition/level/{level_id}")
        fields = {}
        for field in level_res:
            if field.get("RequestedObject") and field.get("IsSuccessful"):
                field_item = field.get("RequestedObject")
                field_id = str(field_item.get("Id"))
                fields[field_id] = {
                    "Type": field_item.get("Type"),
                    "Name": field_item.get("Name"),
                }

        records = client.xml_to_records(res, fields)
        records = [x["record"] for x in records]

        ec = {
            "Record": records,
            "RecordsAmount": len(records),
            "ReportGUID": report_guid,
        }

    markdown = tableToMarkdown("Search records by report results", records)
    context: dict = {"Archer.SearchByReport(val.ReportGUID && val.ReportGUID == obj.ReportGUID)": ec}

    return_outputs(markdown, context, json.loads(xml2json(raw_res)))


def print_cache_command(client: Client, args: dict[str, str]):
    cache = get_integration_context()
    return_outputs(cache, {}, {})


def fetch_incidents(client: Client, params: dict, from_time: datetime, fetch_param_id: str) -> tuple[list, datetime]:
    """Fetches incidents.

    Args:
        client: Client derived from BaseClient
        params: demisto.params dict.
        from_time: Time to start the fetch from
        fetch_param_id: Param ID to find occurred time. can be acquired by get_fetch_param_id

    Returns:
        incidents, next_run datetime in archer's local time
    """
    # Not using get method as those params are a must
    app_id = params["applicationId"]
    date_field = params["applicationDateField"]
    max_results = arg_to_number(params.get("fetch_limit")) or 10
    fields_to_display = argToList(params.get("fields_to_fetch"))
    fields_to_display.append(date_field)
    xml_filter_conditions = params.get("fetch_xml")  # API uses "AND" logical operator by default to join multiple filters

    # If XML filter is given, verify syntax and check no additional date filter that would interfere with the fetch filter
    if xml_filter_conditions:
        validate_xml_conditions(xml_filter_conditions, blacklisted_tags=[FilterConditionTypes.date.value])

    # API Call
    records, _ = client.search_records(
        app_id=app_id,
        fields_to_display=fields_to_display,
        field_to_search=date_field,
        search_value=from_time.strftime(OCCURRED_FORMAT),
        date_operator="GreaterThan",
        max_results=max_results,
        xml_filter_conditions=xml_filter_conditions,
    )
    demisto.debug(f"Found {len(records)=}.")
    # Build incidents
    incidents = []
    # Encountered that sometimes, somehow, on of next_fetch is not UTC.
    last_fetch_time = from_time.replace(tzinfo=UTC)
    next_fetch = last_fetch_time
    for record in records:
        incident, incident_created_time = client.record_to_incident(record, app_id, fetch_param_id)
        # Encountered that sometimes, somehow, incident_created_time is not UTC.
        incident_created_time = incident_created_time.replace(tzinfo=UTC)
        if last_fetch_time < incident_created_time:
            incidents.append(incident)
            if next_fetch < incident_created_time:
                next_fetch = incident_created_time
        else:
            demisto.debug(f"The newly fetched incident is older than last fetch. {incident_created_time=} {next_fetch=}")
    demisto.debug(f"Going out fetch incidents with {next_fetch=}, {len(incidents)=}")
    return incidents, next_fetch


def get_fetch_time(last_fetch: dict, first_fetch_time: str) -> datetime:
    """Gets lastRun object and first fetch time (str, 3 days) and returns
    a datetime object of the last run if exists, else datetime of the first fetch time

    Args:
        last_fetch: a dict that may contain 'last_fetch'
        first_fetch_time: time back in simple format (3 days)

    Returns:
        Time to start fetch from. UTC timezone.
    """
    if next_run := last_fetch.get(LAST_FETCH_TIME_KEY):
        start_fetch = parser(next_run)
    else:
        start_fetch, _ = parse_date_range(first_fetch_time)
    start_fetch.replace(tzinfo=UTC)
    return start_fetch


def get_fetch_param_id(client: Client, last_run: dict, app_id: str, app_date_field: str) -> str:
    """Get the from lastRun if available. Else ask the instance for the ID
    Args:
        client: Archer's client
        last_run: Last run object
        app_id: app id to search on
        app_date_field: the name of the date_field
    Returns:
        ID of the field
    """
    try:  # If exists
        fetch_param_id = last_run[FETCH_PARAM_ID_KEY]
    except KeyError:  # If not, search for it
        fetch_param_id = client.get_field_id(app_id, app_date_field)
    demisto.debug(f"Found a field ID {fetch_param_id=}")
    return fetch_param_id


def fetch_incidents_command(client: Client, params: dict, last_run: dict) -> tuple[list, datetime]:
    """Fetches incidents
    Arguments:
        client: Archer's client
        params: demisto.params()
        last_run: demisto.getLastRun()
    Returns:
        incidents, next fetch
    """
    from_time = get_fetch_time(last_run, params.get("fetch_time", "3 days"))
    fetch_param_id = last_run[FETCH_PARAM_ID_KEY]
    demisto.debug(f"Starting fetch incidents with {from_time=} and {fetch_param_id=}")
    return fetch_incidents(client=client, params=params, from_time=from_time, fetch_param_id=fetch_param_id)


def main():
    params = demisto.params()
    credentials = params.get("credentials")
    base_url = params.get("url").strip("/")

    cache = get_integration_context()
    if not cache.get("fieldValueList"):
        cache["fieldValueList"] = {}
        merge_integration_context(cache)

    client = Client(
        base_url,
        credentials.get("identifier"),
        credentials.get("password"),
        params.get("instanceName"),
        params.get("userDomain"),
        verify=not params.get("insecure", False),
        proxy=params.get("proxy", False),
        timeout=int(params.get("timeout", 600)),
    )
    commands = {
        "archer-search-applications": search_applications_command,
        "archer-get-application-fields": get_application_fields_command,
        "archer-get-field": get_field_command,
        "archer-get-mapping-by-level": get_mapping_by_level_command,
        "archer-get-record": get_record_command,
        "archer-create-record": create_record_command,
        "archer-delete-record": delete_record_command,
        "archer-update-record": update_record_command,
        "archer-execute-statistic-search-by-report": execute_statistics_command,
        "archer-get-reports": get_reports_command,
        "archer-get-search-options-by-guid": search_options_command,
        "archer-reset-cache": reset_cache_command,
        "archer-get-valuelist": get_value_list_command,
        "archer-upload-file": upload_and_associate_command,
        "archer-get-file": download_file_command,
        "archer-list-users": list_users_command,
        "archer-search-records": search_records_command,
        "archer-search-records-by-report": search_records_by_report_command,
        "archer-print-cache": print_cache_command,
    }

    command = demisto.command()
    LOG(f"Command being called is {command}")
    try:
        if command == "fetch-incidents":
            last_run = demisto.getLastRun()
            last_run[FETCH_PARAM_ID_KEY] = get_fetch_param_id(
                client,
                last_run,
                params["applicationId"],
                params["applicationDateField"],
            )
            incidents, next_fetch = fetch_incidents_command(client, params, last_run)
            demisto.debug(f"Setting next run to {next_fetch}")
            last_run[LAST_FETCH_TIME_KEY] = next_fetch.strftime(OCCURRED_FORMAT)
            demisto.setLastRun(last_run)
            demisto.incidents(incidents)
        elif command == "test-module":
            demisto.results(test_module(client, params))
        elif command in commands:
            return commands[command](client, demisto.args())
        else:
            return_error("Command not found.")
    except Exception as exc:
        return_error(f"Unexpected error: {exc!s}", error=exc)


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