diff --git a/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.py b/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.py
index 4eb56fa4b4a7..35640d6a6f50 100644
--- a/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.py
+++ b/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.py
@@ -1,6 +1,7 @@
import demistomock as demisto # noqa: F401
from CommonServerPython import * # noqa: F401
from datetime import UTC, datetime
+from enum import Enum
import random
import dateparser
@@ -54,6 +55,13 @@
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,
@@ -220,19 +228,86 @@ def search_records_by_report_soap_request(token, report_guid):
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')
+ 'Equals123John'
+ """
+ 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')
+ '123Equalstest_value'
+ """
+ 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,
- app_id,
- display_fields,
- field_id,
- field_name,
- search_value,
- date_operator="",
- field_to_search_by_id="",
- numeric_operator="",
- max_results=10,
- level_id="",
+ 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 = (
@@ -253,64 +328,61 @@ def search_records_soap_request(
+ f' {app_id}'
)
- if search_value:
- request_body += ""
+ 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:
- request_body += (
- ""
- + f" {date_operator}"
- + f' {field_id}'
- + f" {search_value}"
- + " UTC Standard Time"
- + " TRUE"
- + ""
+ 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:
- request_body += (
- ""
- + f" {numeric_operator}"
- + f' {field_id}'
- + f" {search_value}"
- + ""
- )
- else:
- if (
- field_to_search_by_id
- and field_to_search_by_id.lower() == field_name.lower()
- ):
- request_body += (
- ""
- + f" {level_id}"
- + " Equals"
- + f" {search_value}"
- + ""
+ filter_conditions.append(
+ construct_generic_filter_condition(
+ FilterConditionTypes.numeric,
+ operator=numeric_operator,
+ field_name=field_name,
+ field_id=field_id,
+ search_value=search_value,
)
- else:
- request_body += (
- ""
- + " Contains"
- + f' {field_id}'
- + f" {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,
)
+ )
- request_body += ""
+ 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 date_operator: # Fetch incidents must present date_operator
- request_body += (
- ""
- + ""
- + " "
- + f" {date_operator}"
- + f' {field_id}'
- + f" {search_value}"
- + " UTC Standard Time"
- + " TRUE"
- + " "
- + ""
- + ""
- )
+ if filter_conditions:
+ filter_conditions_xml = '\n'.join(filter_conditions)
+ request_body += f'{filter_conditions_xml}'
if field_id:
request_body += (
@@ -604,7 +676,7 @@ def try_soap_request(
kwargs: (dict) dict of additional parameters relevant to the soap request.
Returns:
- requets.Response: the response object
+ requests.Response: the response object
"""
headers = {
"SOAPAction": req_data["soapAction"],
@@ -817,15 +889,16 @@ def record_to_incident(
def search_records(
self,
- app_id,
- fields_to_display=None,
- field_to_search="",
- search_value="",
- field_to_search_by_id="",
- numeric_operator="",
- date_operator="",
- max_results=10,
+ 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:
@@ -865,6 +938,7 @@ def search_records(
max_results=max_results,
sort_type=sort_type,
level_id=level_id,
+ xml_filter_conditions=xml_filter_conditions,
)
if not res:
@@ -1570,12 +1644,35 @@ def list_users_command(client: Client, args: dict[str, str]):
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'{xml_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.get("applicationId")
+ 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 = args.get("maxResults", 10)
+ 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"))
@@ -1586,6 +1683,9 @@ def search_records_command(client: Client, args: dict[str, str]):
)
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")
@@ -1609,6 +1709,7 @@ def search_records_command(client: Client, args: dict[str, str]):
date_operator,
max_results=max_results,
sort_type=sort_type,
+ xml_filter_conditions=xml_filter_conditions,
)
records = [x["record"] for x in records]
@@ -1709,17 +1810,24 @@ def fetch_incidents(
# Not using get method as those params are a must
app_id = params["applicationId"]
date_field = params["applicationDateField"]
- max_results = params.get("fetch_limit", 10)
+ 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,
- fields_to_display,
- date_field,
- from_time.strftime(OCCURRED_FORMAT),
+ 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
diff --git a/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.yml b/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.yml
index b0a3f01716ce..de61596f300d 100644
--- a/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.yml
+++ b/Packs/ArcherRSA/Integrations/ArcherV2/ArcherV2.yml
@@ -1,79 +1,113 @@
category: Case Management
+sectionOrder:
+- Connect
+- Collect
commonfields:
id: RSA Archer v2
version: -1
configuration:
-- display: Server URL (e.g., https://192.168.0.1/rsaarcher or https://192.168.0.1/ or https://192.168.0.1/archer)
+- additionalinfo: 'For example: https://192.168.0.1/rsaarcher, https://192.168.0.1/, or https://192.168.0.1/archer.'
+ display: Server URL
name: url
required: true
+ section: Connect
type: 0
-- additionalinfo: Change only if you have another API endpoint.
+- additionalinfo: Change only if using another API endpoint.
defaultvalue: api
- display: 'Advanced: API Endpoint'
+ display: 'API Endpoint'
name: api_endpoint
+ section: Connect
+ advanced: true
type: 0
required: false
- display: Instance name
name: instanceName
required: true
+ section: Connect
type: 0
- display: Username
name: credentials
- required: true
+ section: Connect
type: 9
+ required: true
- display: User domain
name: userDomain
+ section: Connect
type: 0
required: false
- display: Fetch incidents
name: isFetch
+ section: Collect
type: 8
required: false
- display: Incident type
name: incidentType
+ section: Collect
type: 13
required: false
- display: Application ID for fetch
name: applicationId
+ section: Collect
required: true
type: 0
- defaultvalue: Date/Time Occurred
display: Application date field for fetch
name: applicationDateField
required: true
+ section: Collect
type: 0
- additionalinfo: The value should be the field name
+ additionalinfo: The value should be the field name.
- defaultvalue: '10'
display: Maximum number of incidents to pull per fetch
name: fetch_limit
+ section: Collect
type: 0
required: false
- defaultvalue: 3 days
- display: First fetch timestamp (