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
| ID | RSA Archer v2 |
|---|---|
| Provider | RSAsecurity |
| Category | Case Management |
| From Version | 5.0.0 |
| Docker Image | demisto/python3-deb:3.12.13.10116658 |
| Supported Modules | Agentix 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
- Navigate to Settings > Integrations > Servers & Services.
- Search for RSA Archer v2.
-
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 - 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: null2018-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: nullSummary… ValuesListIds: 835
OtherText: nullValuesListIds: 6422
OtherText: nullValuesListIds: 9565
OtherText: nullValuesListIds: 466
OtherText: nullValuesListIds: 156
OtherText: null125
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 EndpointinstanceName— Instance name (required)credentials— Username (required)userDomain— User domainisFetch— Fetch incidentsincidentType— Incident typeapplicationId— Application ID for fetch (required)applicationDateField— Application date field for fetch (required)fetch_limit— Maximum number of incidents to pull per fetchfetch_time— First fetch timestampfields_to_fetch— List of fields from the application to get into the incidentfetch_xml— XML for fetch filteringtimeout— Timeoutinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsincidentFetchInterval— Incidents Fetch Interval
Commands (19)
-
archer-create-recordCreates a new content record in the given application.
-
archer-delete-recordDeletes an existing content record in the given application.
-
archer-execute-statistic-search-by-reportPerforms a statistic search by report GUID.
-
archer-get-application-fieldsGets all application fields by application ID.
-
archer-get-fieldReturns a mapping from list value name to list value ID.
-
archer-get-fileDownloads a file from Archer to Cortex XSOAR context.
-
archer-get-mapping-by-levelReturns a mapping of fields by level ID.
-
archer-get-recordGets information about a content record in the given application.
-
archer-get-reportsGets all reports from Archer.
-
archer-get-search-options-by-guidReturns search criteria by report GUID.
-
archer-get-valuelistReturns 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-usersGets details for a user or a list of all users.
-
archer-print-cachePrints the Archer's integration cache.
-
archer-reset-cacheResets Archer's integration cache. Run this command if you change the fields of your Archer application.
-
archer-search-applicationsGets application details or a list of all applications.
-
archer-search-recordsSearches for records in the given application.
-
archer-search-records-by-reportSearches records by report GUID.
-
archer-update-recordUpdates an existing content record in the given application.
-
archer-upload-fileUploads a file to Archer.
import copy from datetime import UTC, datetime import demistomock as demisto import pytest from pytest_mock import MockerFixture from requests_mock import Mocker as RequestsMock from ArcherV2 import ( OCCURRED_FORMAT, Client, FilterConditionTypes, construct_generic_filter_condition, extract_from_xml, fetch_incidents, generate_field_contents, generate_field_value, get_errors_from_res, get_fetch_time, parser, search_records_by_report_command, search_records_soap_request, upload_and_associate_command, validate_xml_conditions, ) from CommonServerPython import DemistoException BASE_URL = "https://test.com/" GET_TOKEN_SOAP = ( '<?xml version="1.0" encoding="utf-8"?>' + '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"' ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body>' + ' <CreateUserSessionFromInstanceResponse xmlns="http://archer-tech.com/webservices/">' + " <CreateUserSessionFromInstanceResult>TOKEN</CreateUserSessionFromInstanceResult>" + " </CreateUserSessionFromInstanceResponse>" + " </soap:Body>" + "</soap:Envelope>" ) XML_FOR_TEST = ( '<?xml version="1.0" encoding="utf-8"?>' + '<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/">' + " <soap:Body>" + ' <GetValueListForField xmlns="http://archer-tech.com/webservices/">' + " <fieldId>6969</fieldId>" + " </GetValueListForField>" + " </soap:Body>" + "</soap:Envelope>" ) AUTH_LOGIN_RES = {"RequestedObject": {"SessionToken": "session-id"}, "IsSuccessful": True} GET_LEVEL_RES = [{"IsSuccessful": True, "RequestedObject": {"Id": 123}}] FIELD_DEFINITION_RES = [ {"IsSuccessful": True, "RequestedObject": {"Id": 1, "Type": 7, "Name": "External Links", "IsRequired": False}}, { "IsSuccessful": True, "RequestedObject": {"Id": 2, "Type": 1, "Name": "Device Name", "IsRequired": True, "RelatedValuesListId": 8}, }, ] GET_LEVELS_BY_APP = { "level": 123, "mapping": { "1": {"Type": 7, "Name": "External Links", "FieldId": "1", "IsRequired": False, "RelatedValuesListId": None}, "2": {"Type": 1, "Name": "Device Name", "FieldId": "2", "IsRequired": True, "RelatedValuesListId": 8}, }, } GET_FIElD_DEFINITION_RES = { "RequestedObject": {"RelatedValuesListId": 62, "Type": 4}, "IsSuccessful": True, "ValidationMessages": [], } VALUE_LIST_RES = { "RequestedObject": { "Children": [ {"Data": {"Id": 471, "Name": "Low", "IsSelectable": True}}, {"Data": {"Id": 472, "Name": "Medium", "IsSelectable": True}}, {"Data": {"Id": 473, "Name": "High", "IsSelectable": True}}, ] }, "IsSuccessful": True, "ValidationMessages": [], } NESTED_VALUE_LIST_RES = { "Links": [], "RequestedObject": { "Children": [ { "Data": { "Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, }, "Children": [{"Data": {"Id": 88888, "Name": "level 2", "IsSelectable": False}, "Depth": 1}], "Depth": 0, }, { "Data": { "Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Generation": 0, }, "Children": [ { "Data": { "Id": 84000, "Name": "Group D&L, Run-off Businesses (Operating)", "IsSelectable": False, }, "Children": [], "Depth": 1, } ], "Depth": 0, }, { "Data": { "Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, }, "Children": [ { "Data": { "Id": 84002, "Name": "Pharmacy Operations (Operating)", "IsSelectable": False, "Generation": 1, }, "Children": [ { "Data": { "Id": 84003, "Name": "Cigna Home Delivery (Sub Segments)", "IsSelectable": False, }, "Children": [], "Depth": 2, }, { "Data": { "Id": 84004, "Name": "ESI PBM (including Evicore) (Sub Segments)", "IsSelectable": False, }, "Children": [], "Depth": 2, }, ], "Depth": 1, } ], "Depth": 0, }, { "Data": { "Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, }, "Children": [ { "Data": { "Id": 84006, "Name": "Commercial (Operating)", "IsSelectable": False, }, "Children": [ { "Data": { "Id": 84007, "Name": "Behavioral (Sub Segments)", "IsSelectable": False, }, "Children": [], "Depth": 2, }, ], "Depth": 1, }, { "Data": { "Id": 84012, "Name": "Government (Operating)", "IsSelectable": False, }, "Children": [ { "Data": { "Id": 84013, "Name": "CareAllies (Sub Segments)", "IsSelectable": False, }, "Children": [], "Depth": 2, }, ], "Depth": 1, }, ], "Depth": 0, }, { "Data": { "Id": 107694, "Name": "US Commercial", "IsSelectable": False, }, "Children": [], "Depth": 0, }, ] }, "IsSuccessful": "true", "ValidationMessages": [], } VALUE_LIST_RES_FOR_SOURCE = { "RequestedObject": { "Children": [ {"Data": {"Id": 471, "Name": "ArcSight", "IsSelectable": True}}, {"Data": {"Id": 472, "Name": "Medium", "IsSelectable": True}}, {"Data": {"Id": 473, "Name": "High", "IsSelectable": True}}, ] }, "IsSuccessful": True, "ValidationMessages": [], } VALUE_LIST_FIELD_DATA = { "FieldId": 304, "ValuesList": [ {"Id": 471, "Name": "Low", "IsSelectable": True, "Parent": "root", "Depth": None}, {"Id": 472, "Name": "Medium", "IsSelectable": True, "Parent": "root", "Depth": None}, {"Id": 473, "Name": "High", "IsSelectable": True, "Parent": "root", "Depth": None}, ], } RES_WITH_ERRORS = { "ValidationMessages": [ {"ResourcedMessage": "The Type field is a required field."}, {"ResourcedMessage": "The Device Name field is a required field."}, ] } GET_RECORD_RES_failed = {"ValidationMessages": [{"ResourcedMessage": "No resource found."}]} GET_RECORD_RES_SUCCESS = { "Links": [], "RequestedObject": { "Id": 1010, "LevelId": 123, "FieldContents": {"2": {"Type": 1, "Value": "The device name", "FieldId": 2}}, }, "IsSuccessful": True, "ValidationMessages": [], } INCIDENT_RECORD = { "record": {"Id": "227602", "Status": "New", "Name": "Incident 01", "Date/Time Reported": "2018-03-26T10:03:32.243Z"}, "raw": { "@contentId": "227602", "@levelId": "67", "@levelGuid": "b0c2d9a1-167c-4fee-ad91-4b4e7b098b4b", "@moduleId": "75", "@parentId": "0", "Field": [ { "@id": "302", "@guid": "3ec0f462-4c17-4036-b0fa-2f04f3aba3d0", "@type": "4", "ListValues": {"ListValue": {"@id": "466", "@displayName": "New", "#text": "New"}}, }, { "@id": "305", "@guid": "9c5e3de1-299b-430f-998a-185ad86e2e79", "@type": "3", "@xmlConvertedValue": "2018-03-26T10:03:32.243Z", "#text": "26/03/2018 06:03:32", }, ], }, } INCIDENT_RECORD_US_TZ = { "record": {"Id": "227603", "Title": "Test", "created date": "2/25/2021 8:45:55 AM"}, "raw": { "@contentId": "227603", "@levelId": "67", "@levelGuid": "b0c2d9a1-167c-4fee-ad91-4b4e7b098b4b", "@moduleId": "75", "@parentId": "0", "Field": [ {"@id": "35339", "@guid": "9c5e3de1-299b-430f-998a-185ad86e2e79", "@type": "1", "#text": "Test"}, { "@id": "53075", "@guid": "9c5e3de1-299b-430f-998a-185ad86e2e80", "@type": "21", "@xmlConvertedValue": "2021-02-25T08:45:55.977Z", "#text": "2/25/2021 8:45:55 AM", }, ], }, } SEARCH_RECORDS_RES = ( '<?xml version="1.0" encoding="utf-8"?>' + '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"' ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' + " <soap:Body>" + ' <ExecuteSearchResponse xmlns="http://archer-tech.com/webservices/">' + " <ExecuteSearchResult>" + '<?xml version="1.0" encoding="utf-16"?><Records count="6"><Metadata><' 'FieldDefinitions><FieldDefinition id="2" name="Device Name" alias="Name_Full" /><' '/FieldDefinitions></Metadata><LevelCounts><LevelCount id="37" count="6" /><' '/LevelCounts><Record contentId="238756" levelId="37" moduleId="84" parentId="0"><Field id="2" guid=' '"9bc24614-2bc7-4849-a3a3-054729854ab4" type="1">DEVICE NAME</Field></Record></Records>' + " </ExecuteSearchResult>" + " </ExecuteSearchResponse>" + " </soap:Body>" + "</soap:Envelope>" ) GET_RESPONSE_NOT_SUCCESSFUL_JSON = { "IsSuccessful": False, "RequestedObject": None, "ValidationMessages": [ { "Reason": "Validation", "Severity": 3, "MessageKey": "ValidationMessageTemplates:LoginNotValid", "Description": "", "Location": -1, "ErroredValue": None, "Validator": "ArcherApi." "Controllers.Security" "Controller, ArcherApi, " "Version=6.5.200.1045, " "Culture=neutral, " "PublicKeyToken=null", "XmlData": None, "ResourcedMessage": None, } ], } GET_RESPONSE_SUCCESSFUL_JSON = {"IsSuccessful": True, "RequestedObject": {"SessionToken": "session-id"}} SEARCH_RECORDS_BY_REPORT_RES = ( '<Records count="18">' + "<Metadata>" + " <FieldDefinitions>" + ' <FieldDefinition id="1580" name="Policy Name" alias="Policy_Name"/>' + ' <FieldDefinition id="1583" name="Policy Statement"' + ' alias="Policy_Statement"/>' + " </FieldDefinitions>" + "</Metadata>" + "<LevelCounts>" + ' <LevelCount id="3" count="18"/>' + "</LevelCounts>" + '<Record contentId="1720" levelId="3" moduleId="65" parentId="0">' + ' <Field id="1580" type="1">00.0 Introduction</Field>' + ' <Field id="1583" type="1">Information' + " </Field>" + "</Record>" + "</Records>" ) MOCK_READABLE_SEARCH_RECORDS_BY_REPORT = ( "### Search records by report results\n|Id|Policy Name|Policy " "Statement|\n|---|---|---|\n| 1720 | 00.0 Introduction | Information |\n" ) MOCK_RESULTS_SEARCH_RECORDS_BY_REPORT = { "Records": { "@count": "18", "Metadata": { "FieldDefinitions": { "FieldDefinition": [ {"@id": "1580", "@name": "Policy Name", "@alias": "Policy_Name"}, {"@id": "1583", "@name": "Policy Statement", "@alias": "Policy_Statement"}, ] } }, "LevelCounts": {"LevelCount": {"@id": "3", "@count": "18"}}, "Record": { "@contentId": "1720", "@levelId": "3", "@moduleId": "65", "@parentId": "0", "Field": [ {"@id": "1580", "@type": "1", "#text": "00.0 Introduction"}, {"@id": "1583", "@type": "1", "#text": "Information"}, ], }, } } GET_LEVEL_RES_2 = [ { "RequestedObject": {"Type": 1, "Id": 1580, "LevelId": 3, "Name": "Policy Name", "Alias": "Policy_Name"}, "IsSuccessful": True, }, { "RequestedObject": {"Type": 1, "Id": 1583, "LevelId": 3, "Name": "Policy Statement", "Alias": "Policy_Statement"}, "IsSuccessful": True, }, ] RES_DEPTH_0 = { "FieldId": 304, "ValuesList": [ {"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0}, ], } RES_DEPTH_1 = { "FieldId": 304, "ValuesList": [ {"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 88888, "Name": "level 2", "IsSelectable": False, "Parent": "Corporate (Reportable)", "Depth": 1}, {"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84000, "Name": "Group D&L, Run-off Businesses (Operating)", "IsSelectable": False, "Parent": "Group & Other Non-Healthcare (Reportable)", "Depth": 1, }, {"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84002, "Name": "Pharmacy Operations (Operating)", "IsSelectable": False, "Parent": "Health Services (Reportable)", "Depth": 1, }, {"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84006, "Name": "Commercial (Operating)", "IsSelectable": False, "Parent": "Integrated Medical (Reportable)", "Depth": 1, }, { "Id": 84012, "Name": "Government (Operating)", "IsSelectable": False, "Parent": "Integrated Medical (Reportable)", "Depth": 1, }, {"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0}, ], } RES_DEPTH_2 = { "FieldId": 304, "ValuesList": [ {"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, {"Id": 88888, "Name": "level 2", "IsSelectable": False, "Parent": "Corporate (Reportable)", "Depth": 1}, {"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84000, "Name": "Group D&L, Run-off Businesses (Operating)", "IsSelectable": False, "Parent": "Group & Other Non-Healthcare (Reportable)", "Depth": 1, }, {"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84002, "Name": "Pharmacy Operations (Operating)", "IsSelectable": False, "Parent": "Health Services (Reportable)", "Depth": 1, }, { "Id": 84003, "Name": "Cigna Home Delivery (Sub Segments)", "IsSelectable": False, "Parent": "Pharmacy Operations (Operating)", "Depth": 2, }, { "Id": 84004, "Name": "ESI PBM (including Evicore) (Sub Segments)", "IsSelectable": False, "Parent": "Pharmacy Operations (Operating)", "Depth": 2, }, {"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0}, { "Id": 84006, "Name": "Commercial (Operating)", "IsSelectable": False, "Parent": "Integrated Medical (Reportable)", "Depth": 1, }, {"Id": 84007, "Name": "Behavioral (Sub Segments)", "IsSelectable": False, "Parent": "Commercial (Operating)", "Depth": 2}, { "Id": 84012, "Name": "Government (Operating)", "IsSelectable": False, "Parent": "Integrated Medical (Reportable)", "Depth": 1, }, {"Id": 84013, "Name": "CareAllies (Sub Segments)", "IsSelectable": False, "Parent": "Government (Operating)", "Depth": 2}, {"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0}, ], } class TestArcherV2: def test_extract_from_xml(self): field_id = extract_from_xml(XML_FOR_TEST, "Envelope.Body.GetValueListForField.fieldId") assert field_id == "6969" def test_get_level_by_app_id(self, requests_mock): requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES) client = Client(BASE_URL, "", "", "", "", 400) levels = client.get_level_by_app_id("1") assert levels == GET_LEVELS_BY_APP @pytest.mark.parametrize( "requested_object, is_successful", [(GET_RESPONSE_NOT_SUCCESSFUL_JSON, False), (GET_RESPONSE_SUCCESSFUL_JSON, True)] ) def test_update_session(self, mocker, requests_mock, requested_object, is_successful): requests_mock.post(f"{BASE_URL}api/core/security/login", json=requested_object) mocker.patch.object(demisto, "results") client = Client(BASE_URL, "", "", "", "", 400) if is_successful: client.create_session() assert demisto.results.call_count == 0 else: with pytest.raises(SystemExit) as e: # in case login wasn't successful, return_error will exit with a reason (for example, LoginNotValid) # return_error reached client.create_session() assert e def test_update_session_fail_parsing(self, mocker): """ Given: an exception raised from _http_request who failed to pares json object When: - initiating session Then: - Raise exception with message to check the provided url """ mocker.patch.object( Client, "_http_request", side_effect=DemistoException( "Failed to parse json object from " 'response: b"<html><head><script>' "window.top.location='/Default.aspx';" "</script></head><body>" "</body></html>" ), ) client = Client(BASE_URL, "", "", "", "", 400) with pytest.raises(DemistoException) as e: client.create_session() assert "Check the given URL, it can be a redirect issue" in str(e.value) @pytest.mark.parametrize( "json_fields_values, expected_value", [ pytest.param('{"Device Name":"Macbook\\Name\\"Test"}', 'Macbook\\Name"Test', id="Escaped double quote"), pytest.param('{"Device Name":"Phone\\r\\n(Certified)"}', "Phone\r\n(Certified)", id="New line & carriage return"), pytest.param('{"Device Name":"Employee\\A\\PC"}', "Employee\\A\\PC", id="Escaped Backslash"), pytest.param('{"Device Name":"Laptop\\t#406"}', "Laptop\t#406", id="Tab"), pytest.param('{"Device Name":"I \\u2764 Tech"}', "I \u2764 Tech", id="Unicode"), pytest.param('{"Device Name":"Generic Tablet 2025"}', "Generic Tablet 2025", id="No special characters"), pytest.param(r'{"Device Name":"Path: \\user\\123"}', r"Path: \user\123", id="Invalid unicode escape - user path"), pytest.param( r'{"Device Name":"C:\\users\\file.txt"}', r"C:\users\file.txt", id="Invalid unicode escape - Windows path" ), pytest.param(r'{"Device Name":"Two:\\\\."}', r"Two:\\.", id="XSUP-59455 - Backslash before period"), pytest.param( r'{"Device Name":"Test of two double back-slashes followed by double-quote: \\". End of test."}', r"Test of two double back-slashes followed by double-quote: \". End of test.", id="XSUP-59455 Regression - Two backslashes before quote", ), ], ) def test_generate_field_contents(self, json_fields_values: str, expected_value: str): """ Given: - A string of fields values with special characters. When: - Loading a JSON string into a Python object. Then: - Return a valid object with the expected field value. """ client = Client(BASE_URL, "", "", "", "", 400) field = generate_field_contents(client, json_fields_values, GET_LEVELS_BY_APP["mapping"], {"depth": 1}) assert field == {"2": {"Type": 1, "Value": expected_value, "FieldId": "2"}} def test_get_errors_from_res(self): errors = get_errors_from_res(RES_WITH_ERRORS) assert errors == "The Type field is a required field.\nThe Device Name field is a required field." def test_get_record_failed(self, requests_mock): requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/content/1010", json=GET_RECORD_RES_failed) client = Client(BASE_URL, "", "", "", "", 400) record, res, errors = client.get_record(75, 1010, {"depth": 1}) assert errors == "No resource found." assert res assert record == {} def test_get_record_success(self, requests_mock): requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/content/1010", json=GET_RECORD_RES_SUCCESS) requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES) client = Client(BASE_URL, "", "", "", "", 400) record, res, errors = client.get_record(1, 1010, {"depth": 1}) assert errors is None assert res assert record == {"Device Name": "The device name", "Id": 1010} def test_record_to_incident(self): client = Client(BASE_URL, "", "", "", "", 400) record = copy.deepcopy(INCIDENT_RECORD) record["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z" incident, incident_created_time = client.record_to_incident(record, 75, "305") assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC) assert incident["name"] == "RSA Archer Incident: 227602" assert incident["occurred"] == "2018-03-26T10:03:00Z" def test_search_records(self, requests_mock): requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.post(f"{BASE_URL}ws/general.asmx", text=GET_TOKEN_SOAP) requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES) requests_mock.post(f"{BASE_URL}ws/search.asmx", text=SEARCH_RECORDS_RES) client = Client(BASE_URL, "", "", "", "", 400) records, raw_res = client.search_records(1, ["External Links", "Device Name"]) assert raw_res assert len(records) == 1 assert records[0]["record"]["Id"] == "238756" assert records[0]["record"]["Device Name"] == "DEVICE NAME" @pytest.mark.parametrize( "field_name, field_to_search_by_id, expected_condition", [ pytest.param( # Inputs ↓ "id_field_name", "", # Expected ↓ "<TextFilterCondition>" "<Operator>Contains</Operator>" '<Field name="id_field_name">field_id</Field>' "<Value>1234</Value>" "</TextFilterCondition>", id="Generic text filter", ), pytest.param( # Inputs ↓ "id_field_name", "id_field_name", # Expected ↓ "<ContentFilterCondition>" "<Level>5678</Level>" "<Operator>Equals</Operator>" "<Values><Value>1234</Value></Values>" "</ContentFilterCondition>", id="Content filter by ID", ), ], ) def test_search_records_soap_request(self, field_name: str, field_to_search_by_id: str, expected_condition: str): """ Given: - Fields to search on records and id fields to search by ID. When: - Running search_records_soap_request to build the XML body. Then: - Ensure the correct condition is exist in the XML request body. """ xml_request = search_records_soap_request( "token", "app_id", "display_fields", "field_id", field_name, "1234", field_to_search_by_id=field_to_search_by_id, level_id="5678", ) assert expected_condition in xml_request @pytest.mark.parametrize( "integration_context, expected_api_call_count", [ pytest.param( {"fieldValueList": {}}, 1, id="Cache miss (empty cache)", ), pytest.param( {"fieldValueList": {"304__1": VALUE_LIST_FIELD_DATA}}, 0, id="Cache hit (same id and depth)", ), pytest.param( {"fieldValueList": {"304__2": VALUE_LIST_FIELD_DATA}}, 1, id="Cache miss (same id but different depth)", ), ], ) def test_get_field_value_list( self, mocker: MockerFixture, requests_mock: RequestsMock, integration_context: dict, expected_api_call_count: int, ): """ Given: - Integration context cache. When: - Running get_field_value_list. Then: - Ensure API requests are not performed if the matching data exists in the integration context. - Ensure correct field value list is returned. """ field_id = 304 depth = 1 mocker.patch("ArcherV2.get_integration_context", return_value=integration_context) login_request = requests_mock.post( f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES, ) field_definition_request = requests_mock.get( f"{BASE_URL}api/core/system/fielddefinition/{field_id}", json=GET_FIElD_DEFINITION_RES, ) value_list_request = requests_mock.get( f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=VALUE_LIST_RES, ) client = Client(BASE_URL, "", "", "", "", 400) field_data = client.get_field_value_list(field_id, depth) assert login_request.call_count == expected_api_call_count assert field_definition_request.call_count == expected_api_call_count assert value_list_request.call_count == expected_api_call_count assert field_data == VALUE_LIST_FIELD_DATA @pytest.mark.parametrize("args, expected_response", [(0, RES_DEPTH_0), (1, RES_DEPTH_1), (2, RES_DEPTH_2)]) def test_get_field_value_list_nested_response(self, requests_mock, args, expected_response): cache = demisto.getIntegrationContext() cache["fieldValueList"] = {} demisto.setIntegrationContext(cache) requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/304", json=GET_FIElD_DEFINITION_RES) requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=NESTED_VALUE_LIST_RES) client = Client(BASE_URL, "", "", "", "", 400) field_data = client.get_field_value_list(304, args) assert field_data.get("FieldId") == expected_response.get("FieldId") for expected, result in zip(expected_response.get("ValuesList"), field_data.get("ValuesList")): assert expected == result def test_generate_field_value_text_input(self): client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value(client, "", {"Type": 1}, "Demisto", {"depth": 1}) assert field_key == "Value" assert field_value == "Demisto" def test_generate_field_value_values_list_input(self, requests_mock): cache = demisto.getIntegrationContext() cache["fieldValueList"] = {} demisto.setIntegrationContext(cache) requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/304", json=GET_FIElD_DEFINITION_RES) requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=VALUE_LIST_RES) client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value(client, "", {"Type": 4, "FieldId": 304}, ["High"], 1) assert field_key == "Value" assert field_value == {"ValuesListIds": [473]} def test_generate_field_external_link_input(self): client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value( client, "", {"Type": 7}, [{"value": "github", "link": "https://github.com"}, {"value": "google", "link": "https://google.com"}], {"depth": 1}, ) assert field_key == "Value" assert field_value == [{"Name": "github", "URL": "https://github.com"}, {"Name": "google", "URL": "https://google.com"}] def test_generate_field_users_groups_input(self): """ Given: Valid value from dictionary type under "fieldsToValues" argument When: - running archer-update-record Then: - assert fields are generated correctly """ client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value(client, "", {"Type": 8}, {"users": [20], "groups": [30]}, {"depth": 1}) assert field_key == "Value" assert field_value == {"UserList": [{"ID": 20}], "GroupList": [{"ID": 30}]} def test_generate_field_values_list_with_other(self, requests_mock, mocker): """ Given: list values with "OtherText" from dictionary type under "fieldsToValues" argument When: - running archer-update-record Then: - assert fields are generated correctly """ mocker.patch.object(Client, "get_field_value_list", return_value={"ValuesList": [{"Name": "NA", "Id": 222}]}) client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value( client, "", {"Type": 4, "FieldId": 1234}, {"ValuesList": ["NA"], "OtherText": "test"}, {"depth": 1} ) assert field_key == "Value" assert field_value == {"ValuesListIds": [222], "OtherText": "test"} def test_generate_invalid_field_users_groups_input(self): """ Given: Invalid value under "fieldsToValues" argument with type 8 (lists) When: - running archer-update-record Then: - Raise exception indicates that the value is not with the right format """ client = Client(BASE_URL, "", "", "", "", 400) with pytest.raises(DemistoException) as e: generate_field_value(client, "test", {"Type": 8}, "user1, user2", {"depth": 1}) assert ( 'The value of the field: test must be a dictionary type and include a list under "users" key or ' '"groups" key e.g: {"Policy Owner":{"users":[20],"groups":[30]}}' in str(e.value) ) @pytest.mark.parametrize( "field_value, result", [([1, 2], [{"ContentID": 1}, {"ContentID": 2}]), (1234, [{"ContentID": 1234}])] ) def test_generate_field_cross_reference_input(self, field_value, result): client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value(client, "", {"Type": 9}, field_value, {"depth": 1}) assert field_key == "Value" assert field_value == result def test_generate_field_ip_address_input(self): client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value(client, "", {"Type": 19}, "127.0.0.1", {"depth": 1}) assert field_key == "IpAddressBytes" assert field_value == "127.0.0.1" def test_generate_field_value(self, requests_mock): """ Given - generate_field_value on Values List type When - the source is not a list Then - ensure generate_field_value will handle it """ cache = demisto.getIntegrationContext() cache["fieldValueList"] = {} demisto.setIntegrationContext(cache) requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/16172", json=GET_FIElD_DEFINITION_RES) requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=VALUE_LIST_RES_FOR_SOURCE) client = Client(BASE_URL, "", "", "", "", 400) field_key, field_value = generate_field_value( client, "Source", {"FieldId": "16172", "IsRequired": False, "Name": "Source", "RelatedValuesListId": 2092, "Type": 4}, "ArcSight", 1, ) assert field_key == "Value" assert field_value == {"ValuesListIds": [471]} def test_record_to_incident_europe_time(self): """ Given: record with european time (day first) When: fetching incidents Then: assert return dates are right """ client = Client(BASE_URL, "", "", "", "", 400) incident = INCIDENT_RECORD.copy() incident["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z" incident["record"]["Date/Time Reported"] = "26/03/2018 10:03 AM" incident, incident_created_time = client.record_to_incident(INCIDENT_RECORD, 75, "305") assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC) assert incident["occurred"] == "2018-03-26T10:03:00Z" def test_record_to_incident_american_time(self): """ Given: record with american time (month first) When: fetching incidents Then: assert return dates are right """ client = Client(BASE_URL, "", "", "", "", 400) incident = INCIDENT_RECORD.copy() incident["record"]["Date/Time Reported"] = "03/26/2018 10:03 AM" incident["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z" incident, incident_created_time = client.record_to_incident(INCIDENT_RECORD, 75, "305") assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC) assert incident["occurred"] == "2018-03-26T10:03:00Z" def test_fetch_time_change(self, mocker): """ Given: incident with date/time reported european time (day first) - True or false When: Fetching incidents Then: Check that the new next fetch is greater than last_fetch Check the wanted next_fetch is true Assert occurred time """ client = Client(BASE_URL, "", "", "", "", 400) date_time_reported = "2018-04-03T10:03:00.000Z" params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"} record = copy.deepcopy(INCIDENT_RECORD) record["record"]["Date/Time Reported"] = date_time_reported record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported last_fetch = get_fetch_time({"last_fetch": "2018-03-01T10:03:00Z"}, params.get("fetch_time", "3 days")) mocker.patch.object(client, "search_records", return_value=([record], {})) incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305") assert last_fetch < next_fetch assert next_fetch == datetime(2018, 4, 3, 10, 3, tzinfo=UTC) assert incidents[0]["occurred"] == date_time_reported def test_two_fetches(self, mocker): """ Given: 2 incident with date/time reported running two fetches. When: Fetching incidents Then: Check that the new next fetch is greater than last_fetch on both calls. Check the wanted next_fetch is equals to the date in the incident in both calls. Assert occurred time """ client = Client(BASE_URL, "", "", "", "", 400) params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"} record1, record2 = copy.deepcopy(INCIDENT_RECORD), copy.deepcopy(INCIDENT_RECORD) record1["record"]["Date/Time Reported"] = "18/03/2020 10:30 AM" record2["record"]["Date/Time Reported"] = "18/03/2020 03:30 PM" record1["raw"]["Field"][1]["@xmlConvertedValue"] = "2020-03-18T10:30:00.000Z" record2["raw"]["Field"][1]["@xmlConvertedValue"] = "2020-03-18T15:30:00.000Z" last_fetch = parser("2020-18-03T09:00:00Z") mocker.patch.object(client, "search_records", side_effect=[([record1], {}), ([record2], {})]) incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305") assert last_fetch < next_fetch assert next_fetch == datetime(2020, 3, 18, 10, 30, tzinfo=UTC) assert incidents[0]["occurred"] == "2020-03-18T10:30:00.000Z" incidents, next_fetch = fetch_incidents(client, params, next_fetch, "305") assert last_fetch < next_fetch assert next_fetch == datetime(2020, 3, 18, 15, 30, tzinfo=UTC) assert incidents[0]["occurred"] == "2020-03-18T15:30:00.000Z" def test_fetch_got_old_incident(self, mocker): """ Given: last_fetch is newer than new incident When: Fetching incidents Then: Check that the next fetch is equals last fetch (no new incident) Check that no incidents brought back """ client = Client(BASE_URL, "", "", "", "", 400) date_time_reported = "2018-03-01T10:02:00.000Z" params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"} record = copy.deepcopy(INCIDENT_RECORD) record["record"]["Date/Time Reported"] = date_time_reported record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported last_fetch = get_fetch_time({"last_fetch": "2018-03-01T10:03:00Z"}, params.get("fetch_time", "3 days")) mocker.patch.object(client, "search_records", return_value=([record], {})) incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305") assert last_fetch == next_fetch assert not incidents, "Should not get new incidents." def test_fetch_got_exact_same_time(self, mocker): """ Given: last_fetch is in the exact same time as the incident When: Fetching incidents Then: Check that the next fetch is equals last fetch (no new incident) Check that no incidents brought back """ client = Client(BASE_URL, "", "", "", "", 400) date_time_reported = "2018-03-01T10:02:00.000Z" params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"} record = copy.deepcopy(INCIDENT_RECORD) record["record"]["Date/Time Reported"] = date_time_reported record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported last_fetch = get_fetch_time({"last_fetch": date_time_reported}, params.get("fetch_time", "3 days")) mocker.patch.object(client, "search_records", return_value=([record], {})) incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305") assert last_fetch == next_fetch assert not incidents, "Should not get new incidents." @staticmethod def test_fetch_blacklisted_date_filter(): """ Given: fetch_xml parameter with a forbidden DateComparisonFilterCondition When: Fetching incidents Then: Check that a ValueError is raised with the appropriate error message """ client = Client(BASE_URL, "", "", "", "", 400) params = { "applicationId": "75", "applicationDateField": "Date/Time Reported", "fetch_xml": ( "<DateComparisonFilterCondition>" "<Operator>GreaterThan</Operator>" '<Field name="Last Updated">7195</Field>' "<Value>2023-06-04T13:08:43.433385Z</Value>" "<TimeZoneId>UTC Standard Time</TimeZoneId>" "<IsTimeIncluded>TRUE</IsTimeIncluded>" "</DateComparisonFilterCondition>" ), } from_time = datetime(2024, 12, 11) expected_error_message = 'XML filter condition cannot contain the "DateComparisonFilterCondition" tag' with pytest.raises(ValueError, match=expected_error_message): fetch_incidents(client, params, from_time, "204") def test_same_record_returned_in_two_fetches(self, mocker): """ Given: - Same record returned in 2 fetch queries When: - Fetching incidents (2 iterations) Then: Check that the new next fetch is greater than last_fetch on both calls. Check the wanted next_fetch is equals to the date in the incident in both calls. Assert occurred time """ client = Client(BASE_URL, "", "", "", "", 400) mocker.patch.object(client, "search_records", side_effect=[([INCIDENT_RECORD_US_TZ], {}), ([INCIDENT_RECORD_US_TZ], {})]) params = {"applicationId": "75", "applicationDateField": "created date"} field_time_id = "53075" first_fetch = parser("2021-02-24T08:45:55Z") incidents, first_next_fetch = fetch_incidents(client, params, first_fetch, field_time_id) assert first_fetch < first_next_fetch assert first_next_fetch == datetime(2021, 2, 25, 8, 45, 55, 977000, tzinfo=UTC) assert incidents[0]["occurred"] == "2021-02-25T08:45:55.977Z" # first_next_fetch_dt simulates the set to last_run done in fetch-incidents first_next_fetch_dt = parser(first_next_fetch.strftime(OCCURRED_FORMAT)) incidents, second_next_fetch = fetch_incidents(client, params, first_next_fetch_dt, field_time_id) assert first_next_fetch == datetime(2021, 2, 25, 8, 45, 55, 977000, tzinfo=UTC) assert not incidents def test_search_records_by_report_command(self, mocker): """ Given: - search_records_by_report_command command args When: - run search_records_by_report_command Then: - Verify response outputs - verify response readable output """ mock_args = {"reportGuid": "id"} client = Client(BASE_URL, "", "", "", "", 400) mocker.patch.object(client, "do_soap_request", return_value=[SEARCH_RECORDS_BY_REPORT_RES, SEARCH_RECORDS_BY_REPORT_RES]) mocker.patch.object(client, "do_rest_request", return_value=GET_LEVEL_RES_2) mocker.patch.object(demisto, "results") search_records_by_report_command(client, mock_args) assert demisto.results.call_args_list[0][0][0]["HumanReadable"] == MOCK_READABLE_SEARCH_RECORDS_BY_REPORT assert demisto.results.call_args_list[0][0][0]["Contents"] == MOCK_RESULTS_SEARCH_RECORDS_BY_REPORT @pytest.mark.parametrize( "integration_context, is_login_expected, http_call_attempt_results", [ ({}, True, [{"status_code": 200, "json": {"res": "some_res"}}]), ({"session_id": "test_session_id"}, False, [{"status_code": 200, "json": {"res": "some_res"}}]), ({"session_id": "test_session_id"}, False, [{"status_code": 401}, {"status_code": 200, "json": {"res": "some_res"}}]), ( {"session_id": "test_session_id"}, True, [{"status_code": 401}, {"status_code": 401}, {"status_code": 200, "json": {"res": "some_res"}}], ), ], ) def test_do_rest_request(self, mocker, requests_mock, integration_context, is_login_expected, http_call_attempt_results): """ Test for the do_rest_request function. Given: Case 1: Empty integration context (no cached session_id). Case 2: Integration context with cached session_id. Case 3: Integration context with cached session_id. Case 4: Integration context with cached session_id. When: Case 1: rest API request succeed on first run with newly generated session_id. Case 2: rest API request succeed on first run with cached the session_id. Case 3: rest API request fails on first attempt and succeed on second run with the cached session_id. Case 4: rest API request fails on two first attempts with the cached session_id and succeed on third run after creating new session_id. Then: Case 1: Ensure new session_id was generated, and only one call to the search API was done (success). Case 2: Ensure no new session_id was generated, and only one call to the search API was done (success). Case 3: Ensure no new session_id was generated, and two calls to the search API were done (failure and success). Case 3: Ensure new session_id was generated, and three calls to the search API were done (failure, failure and success). """ client = Client(BASE_URL, "", "", "", "", 400) mocker.patch("ArcherV2.get_integration_context", return_value=integration_context) login_mocker = requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES) rest_mocker = requests_mock.get(f"{BASE_URL}test_requests", http_call_attempt_results) dummy_response = client.do_rest_request("GET", "test_requests") if is_login_expected: assert login_mocker.called_once else: assert not login_mocker.called assert rest_mocker.call_count == len(http_call_attempt_results) assert dummy_response @pytest.mark.parametrize( "integration_context, is_new_token_expected, http_call_attempt_results", [ ({}, True, [{"status_code": 200, "text": SEARCH_RECORDS_RES}]), ({"token": "TOKEN"}, False, [{"status_code": 200, "text": SEARCH_RECORDS_RES}]), ({"token": "TOKEN"}, False, [{"status_code": 500}, {"status_code": 200, "text": SEARCH_RECORDS_RES}]), ( {"token": "TOKEN"}, True, [{"status_code": 500}, {"status_code": 500}, {"status_code": 200, "text": SEARCH_RECORDS_RES}], ), ], ) def test_do_soap_request(self, mocker, requests_mock, integration_context, is_new_token_expected, http_call_attempt_results): """ Test for the do_soap_request function. (we use the archer-search-records template as test case, but it doesn't really matter) Given: Case 1: Empty integration context (no cached token). Case 2: Integration context with cached token. Case 3: Integration context with cached token. Case 4: Integration context with cached token. When: Case 1: Soap API request succeed on first run with newly generated the token. Case 2: Soap API request succeed on first run with cached the token. Case 3: Soap API request fails on first attempt and succeed on second run with the cached token. Case 4: Soap API request fails on two first attempts and succeed on third run after creating new token. Then: Case 1: Ensure new token was generated, and only one call to the search API was done (success). Case 2: Ensure no new token was generated, and only one call to the search API was done (success). Case 3: Ensure no new token was generated, and two calls to the search API were done (failure and success). Case 3: Ensure new token was generated, and three calls to the search API were done (failure, failure and success). """ client = Client(BASE_URL, "", "", "", "", 400) mocker.patch("ArcherV2.get_integration_context", return_value=integration_context) new_token_mocker = requests_mock.post(f"{BASE_URL}ws/general.asmx", text=GET_TOKEN_SOAP) soap_mocker = requests_mock.post(f"{BASE_URL}ws/search.asmx", http_call_attempt_results) search_commands_args = { "app_id": 1, "display_fields": '<DisplayField name="External Links">1</DisplayField>' '<DisplayField name="Device Name">2</DisplayField>', "field_id": "", "field_name": "", "field_to_search_by_id": "", "numeric_operator": "", "date_operator": "", "search_value": "", "max_results": 10, "sort_type": "Ascending", "level_id": 123, } client.do_soap_request("archer-search-records", **search_commands_args) if is_new_token_expected: assert new_token_mocker.called_once else: assert not new_token_mocker.called assert soap_mocker.call_count == len(http_call_attempt_results) def test_validate_xml_conditions_valid(self): """ Given: - A string that is meant to represents a valid XML document. When: - Calling validate_xml_conditions. Assert: - Ensure no exception is raised. """ xml_conditions = ( "<TextFilterCondition>" "<Operator>Equals</Operator>" '<Field name="Job">7</Field>' "<Value>Dev</Value>" "</TextFilterCondition>" "<NumericFilterCondition>" "<Operator>GreaterThan</Operator>" '<Field name="Age">8</Field>' "<Value>25</Value>" "</NumericFilterCondition>" ) validate_xml_conditions(xml_conditions) # if exception raised, test would fail @pytest.mark.parametrize( "xml_document, blacklisted_tags, expected_error_message", [ pytest.param( # Inputs ↓ "<ShowStatSummaries>false</ShowSummaries>", [], # Expected ↓ "Invalid XML filter condition syntax", id="Mismatched tags", ), pytest.param( # Inputs ↓ '<ModuleCriteria><Module name="appname">5</Module></ModuleCriteria>', ["ModuleCriteria"], # Expected ↓ 'XML filter condition cannot contain the "ModuleCriteria" tag', id="Blacklisted tag", ), ], ) def test_validate_xml_conditions_raise_exception( self, xml_document: str, blacklisted_tags: list[str], expected_error_message: str, ): """ Given: - A malformed XML document and one that contains a forbidden XML tag. When: - Calling validate_xml_conditions. Assert: - Ensure a ValueError is raised with the correct error message. """ with pytest.raises(ValueError, match=expected_error_message): validate_xml_conditions(xml_document, blacklisted_tags) @pytest.mark.parametrize( "condition_type, operator, field_name, field_id, search_value, expected_xml_condition", [ pytest.param( # Inputs ↓ FilterConditionTypes.date, "GreaterThan", "Last Updated", "1234", "2024-12-11T11:11:24.433385Z", # Expected ↓ "<DateComparisonFilterCondition>" "<Operator>GreaterThan</Operator>" '<Field name="Last Updated">1234</Field>' "<Value>2024-12-11T11:11:24.433385Z</Value>" "</DateComparisonFilterCondition>", id="Date greater than condition", ), pytest.param( # Inputs ↓ FilterConditionTypes.text, "Contains", "Incident Priority", "456", "High", # Expected ↓ "<TextFilterCondition>" "<Operator>Contains</Operator>" '<Field name="Incident Priority">456</Field>' "<Value>High</Value>" "</TextFilterCondition>", id="Text contains condition", ), ], ) def test_construct_generic_filter_condition( self, condition_type: FilterConditionTypes, operator: str, field_name: str, field_id: str, search_value: str, expected_xml_condition: str, ): """ Given: - A filter condition with a comparison operator on a given field. When: - Calling construct_generic_filter_condition. Assert: - Ensure a valid condition XML element with the correct sub-elements. """ xml_condition = construct_generic_filter_condition( condition_type=condition_type, operator=operator, field_name=field_name, field_id=field_id, search_value=search_value, ) assert xml_condition == expected_xml_condition def test_upload_and_associate_command_record_has_attachments(self, mocker): """ Given: A record with existing attachments and multiple files to upload When: The upload_and_associate_command is called Then: Files are uploaded, associated with the record, and existing attachments are preserved """ associatedField = "Supporting Documentation" client = Client(BASE_URL, "", "", "", "", 400) mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123") mock_update_record = mocker.patch("ArcherV2.update_record_command") mock_get_record = mocker.patch.object(client, "get_record", return_value=({associatedField: ["456", "789"]}, "", "")) args = {"applicationId": "app1", "contentId": "content1", "associatedField": associatedField, "entryId": "entry1, entry2"} upload_and_associate_command(client, args) assert mock_upload_file.call_count == 2 assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"}) assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"}) mock_update_record.assert_called_once_with( client, { "applicationId": "app1", "contentId": "content1", "associatedField": associatedField, "entryId": "entry1, entry2", "fieldsToValues": f'{{"{associatedField}": ["123", "123", "456", "789"]}}', }, ) mock_get_record.assert_called_once_with("app1", "content1", 0) def test_upload_and_associate_command_single_file(self, mocker): """ Given: A single file to upload and associate When: The upload_and_associate_command is called Then: The file is uploaded and associated with the record """ client = Client(BASE_URL, "", "", "", "", 400) mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123") mock_update_record = mocker.patch("ArcherV2.update_record_command") mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", "")) args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1"} upload_and_associate_command(client, args) assert mock_upload_file.call_count == 1 assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"}) mock_get_record.assert_called_once_with("app1", "content1", 0) mock_update_record.assert_called_once_with( client, { "applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1", "fieldsToValues": '{"field1": ["123"]}', }, ) def test_upload_and_associate_command_record_has_no_attachments(self, mocker): """ Given: A record without existing attachments and multiple files to upload When: The upload_and_associate_command is called Then: Files are uploaded and associated with the record """ client = Client(BASE_URL, "", "", "", "", 400) mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123") mock_update_record = mocker.patch("ArcherV2.update_record_command") mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", "")) args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2"} upload_and_associate_command(client, args) assert mock_upload_file.call_count == 2 assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"}) assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"}) mock_update_record.assert_called_once_with( client, { "applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2", "fieldsToValues": '{"field1": ["123", "123"]}', }, ) mock_get_record.assert_called_once_with("app1", "content1", 0) def test_upload_and_associate_command_record_with_error(self, mocker): """ Given: An error occurs during record retrieval When: The upload_and_associate_command is called Then: Files are uploaded, association is attempted, and an error is returned """ client = Client(BASE_URL, "", "", "", "", 400) mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123") mock_update_record = mocker.patch("ArcherV2.update_record_command") mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", "error")) mock_error = mocker.patch("ArcherV2.return_error") args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2"} upload_and_associate_command(client, args) assert mock_upload_file.call_count == 2 assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"}) assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"}) mock_update_record.assert_called_once_with( client, { "applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2", "fieldsToValues": '{"field1": ["123", "123"]}', }, ) mock_get_record.assert_called_once_with("app1", "content1", 0) mock_error.assert_called_once_with("error") def test_upload_and_associate_command_without_association(self, mocker): """ Given: A file to upload without association to a record When: The upload_and_associate_command is called Then: The file is uploaded without being associated to any record """ client = Client(BASE_URL, "", "", "", "", 400) mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123") args = {"entryId": "entry1"} upload_and_associate_command(client, args) assert mock_upload_file.call_count == 1 mock_upload_file.assert_called_once_with( client, { "entryId": "entry1", }, ) def test_upload_and_associate_command_missing_args(self, mocker): """ Given: Incomplete arguments for upload and associate command When: The upload_and_associate_command is called Then: An exception is raised indicating missing required arguments """ client = Client(BASE_URL, "", "", "", "", 400) # Test error when only applicationId is provided args = {"applicationId": "app1", "entryId": "entry1"} with pytest.raises(DemistoException) as e: upload_and_associate_command(client, args) assert str(e.value) == "Found arguments to associate an attachment to a record, but not all required arguments supplied" # Test error when only contentId is provided args = {"contentId": "content1", "entryId": "entry1"} with pytest.raises(DemistoException) as e: upload_and_associate_command(client, args) assert str(e.value) == "Found arguments to associate an attachment to a record, but not all required arguments supplied"