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 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()