From 98eeebc38520e860e81d89a6c643071a24ec7530 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Tue, 6 Feb 2024 12:33:26 +0530 Subject: [PATCH 01/17] write_to_table_command & write_to_lookup_table_command changes --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 134 +++++++++++++-------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 9036295bf52b..2ed4e41b4224 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -823,13 +823,32 @@ def multi_table_query_command(offset, items): return entry +def convert_to_str(value): + if isinstance(value, list | dict): + return json.dumps(value) + return str(value) + + def write_to_table_command(): - table_name = demisto.args()["tableName"] - records = check_type(demisto.args()["records"], list) + tag = demisto.args().get("tag", None) + tableName = demisto.args()["tableName"] + records_str = demisto.args()["records"] linq_base = demisto.args().get("linqLinkBase", None) + final_tag = tag or tableName + + # Use json.loads to parse the records string + try: + records = json.loads(records_str) + except json.JSONDecodeError: + return_error("Error decoding JSON. Please ensure the records are valid JSON.") + + # Ensure records is a list + if not isinstance(records, list): + return_error("The 'records' parameter must be a list.") + creds = get_writer_creds() - linq = f"from {table_name}" + linq = f"from {tableName}" sender = Sender( SenderConfigSSL( @@ -840,11 +859,19 @@ def write_to_table_command(): ) ) + total_events = 0 + total_bytes_sent = 0 + for r in records: - try: - sender.send(tag=table_name, msg=json.dumps(r)) - except TypeError: - sender.send(tag=table_name, msg=f"{r}") + # Convert each record to a JSON string or string + formatted_record = convert_to_str(r) + + # Send each record to Devo with the specified tag + sender.send(tag=final_tag, msg=formatted_record) + + # Update totals + total_events += 1 + total_bytes_sent += len(formatted_record.encode("utf-8")) querylink = { "DevoTableLink": build_link( @@ -855,13 +882,6 @@ def write_to_table_command(): ) } - entry = { - "Type": entryTypes["note"], - "Contents": {"recordsWritten": records}, - "ContentsFormat": formats["json"], - "ReadableContentsFormat": formats["markdown"], - "EntryContext": {"Devo.RecordsWritten": records, "Devo.LinqQuery": linq}, - } entry_linq = { "Type": entryTypes["note"], "Contents": querylink, @@ -869,24 +889,17 @@ def write_to_table_command(): "ReadableContentsFormat": formats["markdown"], "EntryContext": {"Devo.QueryLink": createContext(querylink)}, } - headers: list = [] - resultRecords: list = [] - innerDict: dict = {} - for obj in records: - record = json.loads(obj) - currKey = list(record.keys()) - currValue = list(record.values()) - - headers.extend(currKey) - innerDict.update(dict(zip(currKey, currValue))) # Create a dictionary using zip - resultRecords.append(innerDict) # Append the dictionary to the list - - demisto.debug("final array :") - demisto.debug(resultRecords) + entry = { + "Type": entryTypes["note"], + "Contents": {"TotalRecords": total_events, "TotalBytesSent": total_bytes_sent}, + "ContentsFormat": formats["json"], + "ReadableContentsFormat": formats["markdown"], + "EntryContext": {"Devo.TotalRecords": total_events, "Devo.TotalBytesSent": total_bytes_sent, "Devo.LinqQuery": linq}, + } - md = tableToMarkdown("Entries to load into Devo", resultRecords, headers) - entry["HumanReadable"] = md + md_message = f"Total Records Sent: {total_events}.\nTotal Bytes Sent: {total_bytes_sent}." + entry["HumanReadable"] = md_message md_linq = tableToMarkdown( "Link to Devo Query", @@ -898,9 +911,19 @@ def write_to_table_command(): def write_to_lookup_table_command(): - lookup_table_name = demisto.args()["lookupTableName"] - headers = check_type(demisto.args()["headers"], list) - records = check_type(demisto.args()["records"], list) + lookup_table_name = demisto.args().get("lookupTableName") + headers_param = demisto.args().get("headers") + records_param = demisto.args().get("records") + + if not lookup_table_name or not headers_param or not records_param: + return_error("Missing required arguments. Please provide lookupTableName, headers, and records.") + + # Parse JSON strings to Python objects + try: + headers = json.loads(headers_param) + records = json.loads(records_param) + except json.JSONDecodeError as e: + return_error(f"Failed to parse JSON: {str(e)}") creds = get_writer_creds() @@ -911,35 +934,44 @@ def write_to_lookup_table_command(): chain=creds["chain"].name, ) + con = None + total_events = 0 + total_bytes = 0 + try: con = Sender(config=engine_config, timeout=60) + lookup = Lookup(name=lookup_table_name, con=con) - lookup = Lookup(name=lookup_table_name, historic_tag=None, con=con) - # Order sensitive list - pHeaders = json.dumps(headers) - - lookup.send_control("START", pHeaders, "INC") + # Prepare headers for sending + columns = headers.get("headers", []) + key_index = headers.get("key_index", 0) + action = headers.get("action", "") + lookup.send_headers(headers=columns, key_index=key_index, event="START", action=action) + # Send data lines for r in records: - lookup.send_data_line(key_index=0, fields=r["values"]) + fields = r.get("fields", []) + delete = r.get("delete", False) + lookup.send_data_line(key_index=key_index, fields=fields, delete=delete) + total_events += 1 + total_bytes += len(json.dumps(r)) - lookup.send_control("END", pHeaders, "INC") - finally: + # Send end event + lookup.send_headers(headers=columns, key_index=key_index, event="END", action=action) + + # Flush buffer and shutdown connection con.flush_buffer() con.socket.shutdown(0) - entry = { - "Type": entryTypes["note"], - "Contents": {"recordsWritten": records}, - "ContentsFormat": formats["json"], - "ReadableContentsFormat": formats["markdown"], - "EntryContext": {"Devo.RecordsWritten": records}, - } + return f"Lookup Table Name: {lookup_table_name}.\nTotal Records Sent: {total_events}.\nTotal Bytes Sent: {total_bytes}." - md = tableToMarkdown("Entries to load into Devo", records) - entry["HumanReadable"] = md + except Exception as e: + return_error(f"Failed to execute command write-to-lookup-table. Error: {str(e)}") - return [entry] + finally: + if con: + con.flush_buffer() + con.socket.shutdown(0) def main(): From 9c41fc983d5c3035e5036a0d955f6b0bb633e7e6 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Wed, 7 Feb 2024 14:20:57 +0530 Subject: [PATCH 02/17] updated test cases & bug fixes --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 55 +-------- Packs/Devo/Integrations/Devo_v2/Devo_v2.yml | 3 +- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 116 +++--------------- Packs/Devo/ReleaseNotes/1_3_1.md | 9 ++ Packs/Devo/pack_metadata.json | 2 +- 5 files changed, 31 insertions(+), 154 deletions(-) create mode 100644 Packs/Devo/ReleaseNotes/1_3_1.md diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 2ed4e41b4224..04d63f1ef17c 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -520,48 +520,6 @@ def fetch_incidents(): return incidents -def filter_results_by_fields(results, filtered_columns_string): - """ - Filter a list of dictionaries by including only specified fields. - - Parameters: - - results (list): A list of dictionaries representing rows of data. - - filtered_columns_string (str): A comma-separated string containing field names to include. - - Returns: - list: A new list of dictionaries with only the specified fields. - - Raises: - ValueError: If the filtered_columns_string contains invalid column names. - """ - if filtered_columns_string == "": - raise ValueError("filtered_columns cannot be empty.") - - if not filtered_columns_string: - return results - - if not results: - return results - - filtered_columns_list = argToList(filtered_columns_string) - - # Check if all fields from filtered_columns_list are present in the first dictionary in results - first_dict = results[0] - missing_columns = set(filtered_columns_list) - set(first_dict) - if missing_columns: - raise ValueError(f"Fields {list(missing_columns)} not found in query result") - - filtered_results = [] - - for result in results: - filtered_result = { - column: result.get(column) for column in filtered_columns_list - } - filtered_results.append(filtered_result) - - return filtered_results - - def run_query_command(offset, items): to_query = demisto.args()["query"] timestamp_from = demisto.args()["from"] @@ -571,7 +529,6 @@ def run_query_command(offset, items): linq_base = demisto.args().get("linqLinkBase", None) time_range = get_time_range(timestamp_from, timestamp_to) to_query = f"{to_query} offset {offset} limit {items}" - filtered_columns = demisto.args().get("filtered_columns", None) results = list( ds.Reader( oauth_token=READER_OAUTH_TOKEN, @@ -597,8 +554,6 @@ def run_query_command(offset, items): ) } - results = filter_results_by_fields(results, filtered_columns) - entry = { "Type": entryTypes["note"], "Contents": results, @@ -643,7 +598,6 @@ def get_alerts_command(offset, items): linq_base = demisto.args().get("linqLinkBase", None) user_alert_table = demisto.args().get("table_name", None) user_prefix = demisto.args().get("prefix", "") - filtered_columns = demisto.args().get("filtered_columns", None) user_alert_table = user_alert_table if user_alert_table else DEFAULT_ALERT_TABLE if user_prefix: user_prefix = f"{user_prefix}_" @@ -708,8 +662,6 @@ def get_alerts_command(offset, items): for ed in res[extra_data]: res[extra_data][ed] = urllib.parse.unquote_plus(res[extra_data][ed]) - results = filter_results_by_fields(results, filtered_columns) - entry = { "Type": entryTypes["note"], "Contents": results, @@ -749,13 +701,15 @@ def get_alerts_command(offset, items): def multi_table_query_command(offset, items): + # Check if items is negative + if items < 0: + raise ValueError("The 'limit' parameter cannot be negative.") tables_to_query = check_type(demisto.args()["tables"], list) search_token = demisto.args()["searchToken"] timestamp_from = demisto.args()["from"] timestamp_to = demisto.args().get("to", None) write_context = demisto.args()["writeToContext"].lower() query_timeout = int(demisto.args().get("queryTimeout", TIMEOUT)) - filtered_columns = demisto.args().get("filtered_columns", None) global COUNT_MULTI_TABLE time_range = get_time_range(timestamp_from, timestamp_to) @@ -799,8 +753,6 @@ def multi_table_query_command(offset, items): concurrent.futures.wait(futures) - all_results = filter_results_by_fields(all_results, filtered_columns) - entry = { "Type": entryTypes["note"], "Contents": all_results, @@ -963,6 +915,7 @@ def write_to_lookup_table_command(): con.flush_buffer() con.socket.shutdown(0) + # Return three lines as output return f"Lookup Table Name: {lookup_table_name}.\nTotal Records Sent: {total_events}.\nTotal Bytes Sent: {total_bytes}." except Exception as e: diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml index 43e81a780924..42027a23878c 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml @@ -194,6 +194,8 @@ script: description: Queries multiple tables for a given token and returns relevant results. - name: devo-write-to-table arguments: + - name: tag + description: The tag to assign to the records. - name: tableName required: true description: The name of the table to write to. @@ -221,7 +223,6 @@ script: - name: headers required: true description: Headers for lookup table control. - isArray: true - name: records required: true description: Records to write to the specified table. diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 9de20fbadad4..514820bee85a 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from datetime import datetime import pytest -import re from freezegun import freeze_time from Devo_v2 import ( @@ -102,35 +101,12 @@ "from": time.time() - 60, "to": time.time(), "writeToContext": "true", - "filtered_columns": "alertId,extraData,context" -} -MOCK_QUERY_ARGS_INVALIDE_COLUMN_NAME = { - "query": "from whatever", - "from": time.time() - 60, - "to": time.time(), - "writeToContext": "true", - "filtered_columns": "eventdate,abcd" -} -MOCK_ALERT_ARGS_REPEATED_FIELDS = { - "filters": MOCK_FETCH_INCIDENTS_FILTER, - "from": time.time() - 60, - "to": time.time(), - "writeToContext": "true", - "filtered_columns": "alertId,extraData,context,alertId,extraData,context", } MOCK_ALERT_ARGS = { "filters": MOCK_FETCH_INCIDENTS_FILTER, "from": time.time() - 60, "to": time.time(), "writeToContext": "true", - "filtered_columns": "alertId,extraData,context" -} -MOCK_ALERT_ARGS_EMPTY_filtered_columns_PRAM = { - "filters": MOCK_FETCH_INCIDENTS_FILTER, - "from": time.time() - 60, - "to": time.time(), - "writeToContext": "true", - "filtered_columns": "" } MOCK_MULTI_ARGS = { "tables": ["app", "charlie", "test"], @@ -138,7 +114,6 @@ "from": time.time() - 60, "to": time.time(), "writeToContext": "true", - "filtered_columns": "alertId,extraData,context" } MOCK_WRITER_ARGS = { "tableName": "whatever.table", @@ -146,16 +121,14 @@ } MOCK_WRITE_TO_TABLE_RECORDS = { "tableName": "whatever.table", - "records": ['{"foo": "hello"}', '{"foo": "world"}', '{"foo": "demisto"}'], + "records": '[{"hello": "world"}, {"abc": "xyz"}, {"data": "test"}]', } MOCK_LOOKUP_WRITER_ARGS = { "lookupTableName": "hello.world.lookup", - "headers": ["foo", "bar", "baz"], - "records": [ - {"key": "fookey", "values": ["fookey", "bar0", "baz0"]}, - {"key": "keyfoo", "values": ["keyfoo", "bar1", "baz1"]}, - {"key": "keykey", "values": ["keykey", "bar5", "baz6"]}, - ], + "headers": '{"headers": ["foo", "bar", "baz"], "key_index": 0, "action": "FULL"}', + "records": ('[{"fields": ["foo1", "bar1", "baz1"], "delete": false}, ' + '{"fields": ["foo2", "bar2", "baz2"]}, ' + '{"fields": ["foo3", "bar3", "baz3"]}]') } MOCK_KEYS = {"foo": "bar", "baz": "bug"} OFFSET = 0 @@ -235,6 +208,9 @@ class MOCK_LOOKUP: + def send_headers(*args, **kw): + pass + def send_control(*args, **kw): pass @@ -346,61 +322,7 @@ def test_get_alerts(mock_query_results, mock_args_results): mock_args_results.return_value = MOCK_ALERT_ARGS results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) assert len(results) == 2 - assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" - - -@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) -@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) -@patch("Devo_v2.demisto.args") -@patch("Devo_v2.ds.Reader.query") -def test_get_alerts_check_result_columns(mock_query_results, mock_args_results): - mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) - mock_args_results.return_value = MOCK_ALERT_ARGS - results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) - assert len(results) == 2 - assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" - # Check if all expected columns are present in the dictionary - # Convert filtered_columns from a list to a comma-separated string - expected_columns = ','.join(field.strip() for field in MOCK_ALERT_ARGS['filtered_columns'].split(',')) - result = results[0]["Contents"][0] - assert all(column in result for column in expected_columns.split(',')), ( - f"Not all columns present in the dictionary. Missing columns: " - f"{', '.join(column for column in expected_columns.split(',') if column not in result)}" - ) - - -@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) -@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) -@patch("Devo_v2.demisto.args") -@patch("Devo_v2.ds.Reader.query") -def test_get_alerts_with_repeated_fields(mock_query_results, mock_args_results): - mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) - mock_args_results.return_value = MOCK_ALERT_ARGS_REPEATED_FIELDS - - results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) - - assert len(results) == 2 - assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" - - # Check if all expected columns are present in the dictionary - expected_columns = ','.join(field.strip() for field in MOCK_ALERT_ARGS_REPEATED_FIELDS['filtered_columns'].split(',')) - result = results[0]["Contents"][0] - - # Assert that each field appears only once in the result - assert all(result[column] == result.get(column) for column in expected_columns.split(',')), ( - f"Repeated fields not handled properly. Result: {result}" - ) - - -@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) -@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) -@patch("Devo_v2.demisto.args") -@patch("Devo_v2.ds.Reader.query") -def test_get_alerts_with_empty_filtered_columns_param(mock_query_results, mock_args_results): - mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) - mock_args_results.return_value = MOCK_ALERT_ARGS_EMPTY_filtered_columns_PRAM - with pytest.raises(ValueError, match="filtered_columns cannot be empty."): - get_alerts_command(OFFSET, ITEMS_PER_PAGE) + assert results[0]["Contents"][0]["engine"] == "CPU_Usage_Alert" @patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) @@ -413,18 +335,7 @@ def test_run_query(mock_query_results, mock_args_results): results = run_query_command(OFFSET, ITEMS_PER_PAGE) assert (results[1]["HumanReadable"]).find("Devo Direct Link") != -1 assert len(results) == 2 - assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" - - -@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) -@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) -@patch("Devo_v2.demisto.args") -@patch("Devo_v2.ds.Reader.query") -def test_run_query_with_invalid_column_name(mock_query_results, mock_args_results): - mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) - mock_args_results.return_value = MOCK_QUERY_ARGS_INVALIDE_COLUMN_NAME - with pytest.raises(ValueError, match=re.escape("Fields ['abcd'] not found in query result")): - run_query_command(OFFSET, ITEMS_PER_PAGE) + assert results[0]["Contents"][0]["engine"] == "CPU_Usage_Alert" @patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) @@ -461,7 +372,7 @@ def test_write_devo(mock_load_results, mock_write_args): mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN mock_write_args.return_value = MOCK_WRITE_TO_TABLE_RECORDS results = write_to_table_command() - assert len(results[0]["EntryContext"]["Devo.RecordsWritten"]) == 3 + assert len(results) == 2 # We expect two entries in the results list assert results[0]["EntryContext"]["Devo.LinqQuery"] == "from whatever.table" @@ -477,7 +388,10 @@ def test_write_lookup_devo( mock_lookup_writer_sender.return_value = MOCK_SENDER() mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() results = write_to_lookup_table_command() - assert len(results[0]["EntryContext"]["Devo.RecordsWritten"]) == 3 + assert isinstance(results, str) # We expect a string result + assert "Lookup Table Name: hello.world.lookup." in results + assert "Total Records Sent: 3." in results + assert "Total Bytes Sent: 125." in results @patch("Devo_v2.demisto_ISO", return_value="2022-03-15T15:01:23.456Z") diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md new file mode 100644 index 000000000000..8e215d33e3af --- /dev/null +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -0,0 +1,9 @@ +#### Integrations + +##### Devo v2 + +- **Release Note Update:** + - Improved functionality for writing records to Devo tables and lookup tables has been added. + - The Docker image has been updated to version *demisto/devo:1.0.0.86778*. + +These updates enhance the capabilities of the Devo integration by introducing improved methods for writing records to both Devo tables and lookup tables. The new functions provide enhanced flexibility and efficiency in managing and sending data to Devo's platform. Additionally, users can benefit from the latest version of the Docker image, ensuring compatibility and stability with the integration. \ No newline at end of file diff --git a/Packs/Devo/pack_metadata.json b/Packs/Devo/pack_metadata.json index 99ef4a952261..9aa11a256d7f 100644 --- a/Packs/Devo/pack_metadata.json +++ b/Packs/Devo/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Devo", "description": "Use the Devo integration to query Devo for alerts, lookup tables, and to write to lookup tables.", "support": "partner", - "currentVersion": "1.3.0", + "currentVersion": "1.3.1", "author": "Devo", "url": "https://www.devo.com", "email": "support@devo.com", From d2c46f3477f74cc85c188895c6fc302a17be8a3b Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Wed, 7 Feb 2024 15:25:39 +0530 Subject: [PATCH 03/17] updated devo_write_to_table test case --- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 514820bee85a..626246bd3e78 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -115,6 +115,14 @@ "to": time.time(), "writeToContext": "true", } +MOCK_MULTI_ARGUMENTS = { + "tables": ["app", "charlie", "test"], + "searchToken": "searching", + "from": time.time() - 60, + "to": time.time(), + "writeToContext": "true", + "items": -10 +} MOCK_WRITER_ARGS = { "tableName": "whatever.table", "records": [{"foo": "hello"}, {"foo": "world"}, {"foo": "demisto"}], @@ -364,6 +372,35 @@ def test_multi_query( assert results["HumanReadable"] == "No results found" +@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) +@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) +@patch("Devo_v2.concurrent.futures.wait") +@patch("Devo_v2.concurrent.futures.ThreadPoolExecutor.submit") +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.ds.Reader.query") +@patch("Devo_v2.ds.Reader") +@patch("Devo_v2.get_types") +def test_multi_query_negative_items( + mock_query_types, + mock_query_reader, + mock_query_results, + mock_args_results, + mock_submit_results, + mock_wait_results, +): + mock_query_types.return_value = MOCK_KEYS + mock_query_reader.return_value = MOCK_READER + mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) + mock_args_results.return_value = MOCK_MULTI_ARGUMENTS + mock_submit_results.return_value = None + mock_wait_results.return_value = (None, None) + try: + multi_table_query_command(OFFSET, ITEMS_PER_PAGE) + except ValueError as exc: + error_msg = str(exc) + assert "The 'limit' parameter cannot be negative." in error_msg + + @patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) @patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) @patch("Devo_v2.demisto.args") @@ -373,6 +410,12 @@ def test_write_devo(mock_load_results, mock_write_args): mock_write_args.return_value = MOCK_WRITE_TO_TABLE_RECORDS results = write_to_table_command() assert len(results) == 2 # We expect two entries in the results list + found = False + for result in results: + if "HumanReadable" in result and result["HumanReadable"] == "Total Records Sent: 3.\nTotal Bytes Sent: 48.": + found = True + break + assert found, "Expected string not found in 'HumanReadable' field of results" assert results[0]["EntryContext"]["Devo.LinqQuery"] == "from whatever.table" From 4724061247c8c1c9f3648ef2ffca64bc8037b6e3 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Wed, 7 Feb 2024 16:17:57 +0530 Subject: [PATCH 04/17] prevoius code reverted --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 51 ++++++++++ .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 92 ++++++++++++++++++- Packs/Devo/ReleaseNotes/1_3_1.md | 2 +- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 04d63f1ef17c..71522b26e04a 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -520,6 +520,48 @@ def fetch_incidents(): return incidents +def filter_results_by_fields(results, filtered_columns_string): + """ + Filter a list of dictionaries by including only specified fields. + + Parameters: + - results (list): A list of dictionaries representing rows of data. + - filtered_columns_string (str): A comma-separated string containing field names to include. + + Returns: + list: A new list of dictionaries with only the specified fields. + + Raises: + ValueError: If the filtered_columns_string contains invalid column names. + """ + if filtered_columns_string == "": + raise ValueError("filtered_columns cannot be empty.") + + if not filtered_columns_string: + return results + + if not results: + return results + + filtered_columns_list = argToList(filtered_columns_string) + + # Check if all fields from filtered_columns_list are present in the first dictionary in results + first_dict = results[0] + missing_columns = set(filtered_columns_list) - set(first_dict) + if missing_columns: + raise ValueError(f"Fields {list(missing_columns)} not found in query result") + + filtered_results = [] + + for result in results: + filtered_result = { + column: result.get(column) for column in filtered_columns_list + } + filtered_results.append(filtered_result) + + return filtered_results + + def run_query_command(offset, items): to_query = demisto.args()["query"] timestamp_from = demisto.args()["from"] @@ -529,6 +571,7 @@ def run_query_command(offset, items): linq_base = demisto.args().get("linqLinkBase", None) time_range = get_time_range(timestamp_from, timestamp_to) to_query = f"{to_query} offset {offset} limit {items}" + filtered_columns = demisto.args().get("filtered_columns", None) results = list( ds.Reader( oauth_token=READER_OAUTH_TOKEN, @@ -554,6 +597,8 @@ def run_query_command(offset, items): ) } + results = filter_results_by_fields(results, filtered_columns) + entry = { "Type": entryTypes["note"], "Contents": results, @@ -598,6 +643,7 @@ def get_alerts_command(offset, items): linq_base = demisto.args().get("linqLinkBase", None) user_alert_table = demisto.args().get("table_name", None) user_prefix = demisto.args().get("prefix", "") + filtered_columns = demisto.args().get("filtered_columns", None) user_alert_table = user_alert_table if user_alert_table else DEFAULT_ALERT_TABLE if user_prefix: user_prefix = f"{user_prefix}_" @@ -662,6 +708,8 @@ def get_alerts_command(offset, items): for ed in res[extra_data]: res[extra_data][ed] = urllib.parse.unquote_plus(res[extra_data][ed]) + results = filter_results_by_fields(results, filtered_columns) + entry = { "Type": entryTypes["note"], "Contents": results, @@ -710,6 +758,7 @@ def multi_table_query_command(offset, items): timestamp_to = demisto.args().get("to", None) write_context = demisto.args()["writeToContext"].lower() query_timeout = int(demisto.args().get("queryTimeout", TIMEOUT)) + filtered_columns = demisto.args().get("filtered_columns", None) global COUNT_MULTI_TABLE time_range = get_time_range(timestamp_from, timestamp_to) @@ -753,6 +802,8 @@ def multi_table_query_command(offset, items): concurrent.futures.wait(futures) + all_results = filter_results_by_fields(all_results, filtered_columns) + entry = { "Type": entryTypes["note"], "Contents": all_results, diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 626246bd3e78..56b873ad22cf 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from datetime import datetime import pytest +import re from freezegun import freeze_time from Devo_v2 import ( @@ -101,12 +102,35 @@ "from": time.time() - 60, "to": time.time(), "writeToContext": "true", + "filtered_columns": "alertId,extraData,context" +} +MOCK_QUERY_ARGS_INVALIDE_COLUMN_NAME = { + "query": "from whatever", + "from": time.time() - 60, + "to": time.time(), + "writeToContext": "true", + "filtered_columns": "eventdate,abcd" +} +MOCK_ALERT_ARGS_REPEATED_FIELDS = { + "filters": MOCK_FETCH_INCIDENTS_FILTER, + "from": time.time() - 60, + "to": time.time(), + "writeToContext": "true", + "filtered_columns": "alertId,extraData,context,alertId,extraData,context", } MOCK_ALERT_ARGS = { "filters": MOCK_FETCH_INCIDENTS_FILTER, "from": time.time() - 60, "to": time.time(), "writeToContext": "true", + "filtered_columns": "alertId,extraData,context" +} +MOCK_ALERT_ARGS_EMPTY_filtered_columns_PRAM = { + "filters": MOCK_FETCH_INCIDENTS_FILTER, + "from": time.time() - 60, + "to": time.time(), + "writeToContext": "true", + "filtered_columns": "" } MOCK_MULTI_ARGS = { "tables": ["app", "charlie", "test"], @@ -114,6 +138,7 @@ "from": time.time() - 60, "to": time.time(), "writeToContext": "true", + "filtered_columns": "alertId,extraData,context" } MOCK_MULTI_ARGUMENTS = { "tables": ["app", "charlie", "test"], @@ -333,6 +358,60 @@ def test_get_alerts(mock_query_results, mock_args_results): assert results[0]["Contents"][0]["engine"] == "CPU_Usage_Alert" +@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) +@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.ds.Reader.query") +def test_get_alerts_check_result_columns(mock_query_results, mock_args_results): + mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) + mock_args_results.return_value = MOCK_ALERT_ARGS + results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) + assert len(results) == 2 + assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" + # Check if all expected columns are present in the dictionary + # Convert filtered_columns from a list to a comma-separated string + expected_columns = ','.join(field.strip() for field in MOCK_ALERT_ARGS['filtered_columns'].split(',')) + result = results[0]["Contents"][0] + assert all(column in result for column in expected_columns.split(',')), ( + f"Not all columns present in the dictionary. Missing columns: " + f"{', '.join(column for column in expected_columns.split(',') if column not in result)}" + ) + + +@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) +@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.ds.Reader.query") +def test_get_alerts_with_repeated_fields(mock_query_results, mock_args_results): + mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) + mock_args_results.return_value = MOCK_ALERT_ARGS_REPEATED_FIELDS + + results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) + + assert len(results) == 2 + assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" + + # Check if all expected columns are present in the dictionary + expected_columns = ','.join(field.strip() for field in MOCK_ALERT_ARGS_REPEATED_FIELDS['filtered_columns'].split(',')) + result = results[0]["Contents"][0] + + # Assert that each field appears only once in the result + assert all(result[column] == result.get(column) for column in expected_columns.split(',')), ( + f"Repeated fields not handled properly. Result: {result}" + ) + + +@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) +@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.ds.Reader.query") +def test_get_alerts_with_empty_filtered_columns_param(mock_query_results, mock_args_results): + mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) + mock_args_results.return_value = MOCK_ALERT_ARGS_EMPTY_filtered_columns_PRAM + with pytest.raises(ValueError, match="filtered_columns cannot be empty."): + get_alerts_command(OFFSET, ITEMS_PER_PAGE) + + @patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) @patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) @patch("Devo_v2.demisto.args") @@ -343,7 +422,18 @@ def test_run_query(mock_query_results, mock_args_results): results = run_query_command(OFFSET, ITEMS_PER_PAGE) assert (results[1]["HumanReadable"]).find("Devo Direct Link") != -1 assert len(results) == 2 - assert results[0]["Contents"][0]["engine"] == "CPU_Usage_Alert" + assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" + + +@patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) +@patch("Devo_v2.READER_OAUTH_TOKEN", MOCK_READER_OAUTH_TOKEN, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.ds.Reader.query") +def test_run_query_with_invalid_column_name(mock_query_results, mock_args_results): + mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS) + mock_args_results.return_value = MOCK_QUERY_ARGS_INVALIDE_COLUMN_NAME + with pytest.raises(ValueError, match=re.escape("Fields ['abcd'] not found in query result")): + run_query_command(OFFSET, ITEMS_PER_PAGE) @patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index 8e215d33e3af..4d00240fa404 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -3,7 +3,7 @@ ##### Devo v2 - **Release Note Update:** - - Improved functionality for writing records to Devo tables and lookup tables has been added. + - Improved functionality for writing records to Devo tables and lookup tables has been updated. - The Docker image has been updated to version *demisto/devo:1.0.0.86778*. These updates enhance the capabilities of the Devo integration by introducing improved methods for writing records to both Devo tables and lookup tables. The new functions provide enhanced flexibility and efficiency in managing and sending data to Devo's platform. Additionally, users can benefit from the latest version of the Docker image, ensuring compatibility and stability with the integration. \ No newline at end of file From c8c9d23b015ab6594e0b29547345edaac8cf20cf Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Wed, 7 Feb 2024 17:17:21 +0530 Subject: [PATCH 05/17] diff changes --- Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 56b873ad22cf..8dfc4a75f251 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -355,7 +355,7 @@ def test_get_alerts(mock_query_results, mock_args_results): mock_args_results.return_value = MOCK_ALERT_ARGS results = get_alerts_command(OFFSET, ITEMS_PER_PAGE) assert len(results) == 2 - assert results[0]["Contents"][0]["engine"] == "CPU_Usage_Alert" + assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert" @patch("Devo_v2.READER_ENDPOINT", MOCK_READER_ENDPOINT, create=True) From 679e0562193075d4445ba7008cc14e6b47979d11 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Thu, 8 Feb 2024 17:53:03 +0530 Subject: [PATCH 06/17] Bug fixes --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 41 +++++++++++++++------ Packs/Devo/Integrations/Devo_v2/Devo_v2.yml | 4 +- Packs/Devo/ReleaseNotes/1_3_1.md | 1 + 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 71522b26e04a..5ffd2b2b4695 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -33,6 +33,7 @@ TIMEOUT = demisto.params().get("timeout", "60") PORT = arg_to_number(demisto.params().get("port", "443") or "443") ITEMS_PER_PAGE = 50 +LIMIT = 100 HEALTHCHECK_WRITER_RECORD = [{"hello": "world", "from": "demisto-integration"}] HEALTHCHECK_WRITER_TABLE = "test.keep.free" RANGE_PATTERN = re.compile("^[0-9]+ [a-zA-Z]+") @@ -749,9 +750,6 @@ def get_alerts_command(offset, items): def multi_table_query_command(offset, items): - # Check if items is negative - if items < 0: - raise ValueError("The 'limit' parameter cannot be negative.") tables_to_query = check_type(demisto.args()["tables"], list) search_token = demisto.args()["searchToken"] timestamp_from = demisto.args()["from"] @@ -827,7 +825,13 @@ def multi_table_query_command(offset, items): def convert_to_str(value): - if isinstance(value, list | dict): + if isinstance(value, list) and len(value) == 0: + print("Warning: Empty list encountered.") + return '[]' + elif isinstance(value, dict) and not value: + print("Warning: Empty dictionary encountered.") + return '{}' + elif isinstance(value, (list, dict)): return json.dumps(value) return str(value) @@ -850,6 +854,10 @@ def write_to_table_command(): if not isinstance(records, list): return_error("The 'records' parameter must be a list.") + # Check if all records are empty + if all(not record for record in records): + return_error("All records are empty.") + creds = get_writer_creds() linq = f"from {tableName}" @@ -869,6 +877,10 @@ def write_to_table_command(): # Convert each record to a JSON string or string formatted_record = convert_to_str(r) + # If the record is empty, skip sending it + if not formatted_record.strip(): + continue + # Send each record to Devo with the specified tag sender.send(tag=final_tag, msg=formatted_record) @@ -876,11 +888,15 @@ def write_to_table_command(): total_events += 1 total_bytes_sent += len(formatted_record.encode("utf-8")) + current_ts = int(time.time()) + start_ts = (current_ts - 30) * 1000 + end_ts = (current_ts + 30) * 1000 + querylink = { "DevoTableLink": build_link( linq, - int(1000 * time.time()) - 3600000, - int(1000 * time.time()), + start_ts, + end_ts, linq_base=linq_base, ) } @@ -1019,14 +1035,17 @@ def main(): OFFSET = 0 items_per_page = int(demisto.args().get("items_per_page", ITEMS_PER_PAGE)) if items_per_page <= 0: - raise ValueError("items_per_page should be a positive non-zero value.") + raise ValueError("The items_per_page should be a positive non-zero value.") + limit = int(demisto.args().get("limit", LIMIT)) + if limit <= 0: + raise ValueError("The 'limit' parameter should be a positive non-zero value.") total = 0 - demisto.results(multi_table_query_command(OFFSET, items_per_page)) + demisto.results(multi_table_query_command(OFFSET, limit)) total = total + COUNT_MULTI_TABLE - while items_per_page * 2 == COUNT_MULTI_TABLE: - OFFSET = OFFSET + items_per_page + while limit * 2 == COUNT_MULTI_TABLE: + OFFSET = OFFSET + limit total = total + COUNT_MULTI_TABLE - demisto.results(multi_table_query_command(OFFSET, items_per_page)) + demisto.results(multi_table_query_command(OFFSET, limit)) elif demisto.command() == "devo-write-to-table": demisto.results(write_to_table_command()) elif demisto.command() == "devo-write-to-lookup-table": diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml index 42027a23878c..66ff8b14ee4b 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml @@ -200,8 +200,8 @@ script: required: true description: The name of the table to write to. - name: records - required: true description: Records to write to the specified table. + required: true isArray: true - name: linqLinkBase description: Overrides the global Devo base domain for linq linking. @@ -233,7 +233,7 @@ script: type: unknown description: Writes lookup table entry records to a specified Devo table. execution: true - dockerimage: demisto/devo:1.0.0.86778 + dockerimage: demisto/devo:1.0.0.87277 isfetch: true subtype: python3 tests: diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index 4d00240fa404..4562c7d98338 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -1,6 +1,7 @@ #### Integrations ##### Devo v2 +- Updated the Docker image to: *demisto/devo:1.0.0.87277*. - **Release Note Update:** - Improved functionality for writing records to Devo tables and lookup tables has been updated. From 0a3e7bcf09d183e7776e7319b0003c2fd8f79b5d Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Fri, 9 Feb 2024 21:30:43 +0530 Subject: [PATCH 07/17] Validation checks & Unit test cases added --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 38 +++++--- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 96 ++++++++++++++++++- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 5ffd2b2b4695..3ca2acfce74a 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -825,15 +825,16 @@ def multi_table_query_command(offset, items): def convert_to_str(value): + warnings = [] if isinstance(value, list) and len(value) == 0: - print("Warning: Empty list encountered.") - return '[]' + warnings.append("Empty list encountered.") + return '[]', warnings elif isinstance(value, dict) and not value: - print("Warning: Empty dictionary encountered.") - return '{}' - elif isinstance(value, (list, dict)): - return json.dumps(value) - return str(value) + warnings.append("Empty dictionary encountered.") + return '{}', warnings + elif isinstance(value, list | dict): + return json.dumps(value), warnings + return str(value), warnings def write_to_table_command(): @@ -878,7 +879,7 @@ def write_to_table_command(): formatted_record = convert_to_str(r) # If the record is empty, skip sending it - if not formatted_record.strip(): + if not len(formatted_record): continue # Send each record to Devo with the specified tag @@ -886,7 +887,7 @@ def write_to_table_command(): # Update totals total_events += 1 - total_bytes_sent += len(formatted_record.encode("utf-8")) + total_bytes_sent += len(formatted_record) current_ts = int(time.time()) start_ts = (current_ts - 30) * 1000 @@ -957,14 +958,25 @@ def write_to_lookup_table_command(): total_events = 0 total_bytes = 0 + # Validate headers + if not isinstance(headers, dict) or "headers" not in headers or not isinstance(headers["headers"], list): + raise ValueError("Invalid headers format. 'headers' must be a list.") + + columns = headers["headers"] + + # Validate key_index + key_index = int(headers.get("key_index", 0)) # Ensure it's casted to integer + if key_index < 0: + raise ValueError("key_index must be a non-negative integer value.") + + # Validate action + action = headers.get("action", "") + if action not in {"INC", "FULL"}: + raise ValueError("action must be either 'INC' or 'FULL'.") try: con = Sender(config=engine_config, timeout=60) lookup = Lookup(name=lookup_table_name, con=con) - # Prepare headers for sending - columns = headers.get("headers", []) - key_index = headers.get("key_index", 0) - action = headers.get("action", "") lookup.send_headers(headers=columns, key_index=key_index, event="START", action=action) # Send data lines diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 8dfc4a75f251..8c9c1c6c863e 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -150,7 +150,7 @@ } MOCK_WRITER_ARGS = { "tableName": "whatever.table", - "records": [{"foo": "hello"}, {"foo": "world"}, {"foo": "demisto"}], + "records": '[{"foo": "hello"}, {"foo": "world"}, {"foo": "demisto"}]', } MOCK_WRITE_TO_TABLE_RECORDS = { "tableName": "whatever.table", @@ -163,6 +163,20 @@ '{"fields": ["foo2", "bar2", "baz2"]}, ' '{"fields": ["foo3", "bar3", "baz3"]}]') } +MOCK_LOOKUP_WRITER_ARGS_key = { + "lookupTableName": "hello.world.lookup", + "headers": '{"headers": ["foo", "bar", "baz"], "key_index": 0, "action": "FULL"}', + "records": ('[{"fields": ["foo1", "bar1", "baz1"], "delete": false}, ' + '{"fields": ["foo2", "bar2", "baz2"]}, ' + '{"fields": ["foo3", "bar3", "baz3"]}]') +} +MOCK_LOOKUP_WRITER_ARGS_action = { + "lookupTableName": "hello.world.lookup", + "headers": '{"headers": ["foo", "bar", "baz"], "key_index": 0, "action": "INC"}', + "records": ('[{"fields": ["foo1", "bar1", "baz1"], "delete": false}, ' + '{"fields": ["foo2", "bar2", "baz2"]}, ' + '{"fields": ["foo3", "bar3", "baz3"]}]') +} MOCK_KEYS = {"foo": "bar", "baz": "bug"} OFFSET = 0 ITEMS_PER_PAGE = 10 @@ -502,13 +516,37 @@ def test_write_devo(mock_load_results, mock_write_args): assert len(results) == 2 # We expect two entries in the results list found = False for result in results: - if "HumanReadable" in result and result["HumanReadable"] == "Total Records Sent: 3.\nTotal Bytes Sent: 48.": + if "HumanReadable" in result and result["HumanReadable"] == "Total Records Sent: 3.\nTotal Bytes Sent: 6.": found = True break assert found, "Expected string not found in 'HumanReadable' field of results" assert results[0]["EntryContext"]["Devo.LinqQuery"] == "from whatever.table" +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_data(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "Error decoding JSON. Please ensure the records are valid JSON." in error_msg + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "The 'records' parameter must be a list." in error_msg + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "All records are empty." in error_msg + + @patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) @patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) @patch("Devo_v2.demisto.args") @@ -527,6 +565,60 @@ def test_write_lookup_devo( assert "Total Bytes Sent: 125." in results +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +@patch("Devo_v2.Lookup") +def test_write_lookup_devo_header( + mock_lookup_writer_lookup, mock_lookup_writer_sender, mock_lookup_write_args +): + mock_lookup_write_args.return_value = MOCK_LOOKUP_WRITER_ARGS + mock_lookup_writer_sender.return_value = MOCK_SENDER() + mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() + try: + write_to_lookup_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "Invalid headers format. 'headers' must be a list." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +@patch("Devo_v2.Lookup") +def test_write_lookup_devo_invalid( + mock_lookup_writer_lookup, mock_lookup_writer_sender, mock_lookup_write_args +): + mock_lookup_write_args.return_value = MOCK_LOOKUP_WRITER_ARGS_key + mock_lookup_writer_sender.return_value = MOCK_SENDER() + mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() + try: + write_to_lookup_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "key_index must be a non-negative integer value." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +@patch("Devo_v2.Lookup") +def test_write_lookup_devo_invalid_action( + mock_lookup_writer_lookup, mock_lookup_writer_sender, mock_lookup_write_args +): + mock_lookup_write_args.return_value = MOCK_LOOKUP_WRITER_ARGS_action + mock_lookup_writer_sender.return_value = MOCK_SENDER() + mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() + try: + write_to_lookup_table_command() + except ValueError as err: + error = str(err) + assert "action must be either 'INC' or 'FULL'." in error + + @patch("Devo_v2.demisto_ISO", return_value="2022-03-15T15:01:23.456Z") def test_alert_to_incident_all_data(mock_demisto_ISO): incident = alert_to_incident(ALERT, USER_PREFIX) From e6196d4e137e8dcb3f1897df7168eae6eae5b004 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 12 Feb 2024 17:26:47 +0530 Subject: [PATCH 08/17] code reverted & working on readme file --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 27 ++++++++--------- Packs/Devo/Integrations/Devo_v2/README.md | 35 +++++++++++++++++----- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index 3ca2acfce74a..e378e9521cdd 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -825,16 +825,15 @@ def multi_table_query_command(offset, items): def convert_to_str(value): - warnings = [] if isinstance(value, list) and len(value) == 0: - warnings.append("Empty list encountered.") - return '[]', warnings + print("Warning: Empty list encountered.") + return '[]' elif isinstance(value, dict) and not value: - warnings.append("Empty dictionary encountered.") - return '{}', warnings - elif isinstance(value, list | dict): - return json.dumps(value), warnings - return str(value), warnings + print("Warning: Empty dictionary encountered.") + return '{}' + elif isinstance(value, (list, dict)): + return json.dumps(value) + return str(value) def write_to_table_command(): @@ -879,7 +878,7 @@ def write_to_table_command(): formatted_record = convert_to_str(r) # If the record is empty, skip sending it - if not len(formatted_record): + if not formatted_record.strip(): continue # Send each record to Devo with the specified tag @@ -887,7 +886,7 @@ def write_to_table_command(): # Update totals total_events += 1 - total_bytes_sent += len(formatted_record) + total_bytes_sent += len(formatted_record.encode("utf-8")) current_ts = int(time.time()) start_ts = (current_ts - 30) * 1000 @@ -966,13 +965,13 @@ def write_to_lookup_table_command(): # Validate key_index key_index = int(headers.get("key_index", 0)) # Ensure it's casted to integer - if key_index < 0: - raise ValueError("key_index must be a non-negative integer value.") + ## if key_index < 0: + ## raise ValueError("key_index must be a non-negative integer value.") # Validate action action = headers.get("action", "") - if action not in {"INC", "FULL"}: - raise ValueError("action must be either 'INC' or 'FULL'.") + ## if action not in {"INC", "FULL"}: + ## raise ValueError("action must be either 'INC' or 'FULL'.") try: con = Sender(config=engine_config, timeout=60) lookup = Lookup(name=lookup_table_name, con=con) diff --git a/Packs/Devo/Integrations/Devo_v2/README.md b/Packs/Devo/Integrations/Devo_v2/README.md index 082f9d8f4d73..c63f1f5a8f33 100644 --- a/Packs/Devo/Integrations/Devo_v2/README.md +++ b/Packs/Devo/Integrations/Devo_v2/README.md @@ -342,16 +342,25 @@ For more information on the way we write to a table please refer to this documen ##### Command Example ``` -!devo-write-to-table tableName="my.app.test.test" records='[{"hello": "world"}, {"hello": "world"}]' +!devo-write-to-table tableName="my.app.test.test" records=`[ "This is my first event", "This is my second log", {"hello": "world"}, {"hello": "friend"}, ["a", "b", "c"], ["1", "2", "3"], 1234, true ]` ``` ##### Human Readable Output -Entries to load into Devo +Total Records Sent: 8. +Total Bytes Sent: 196. -|hello| +##### Entries to load into Devo + +|Data| |---| -|world| -|world|| +|This is my first event| +|This is my second log| +|{"hello": "world"}| +|{"hello": "friend"}| +|["a", "b", "c"]| +|["1", "2", "3"]| +|1234| +|true| Link to Devo Query @@ -377,7 +386,7 @@ adding in extra lookup information found [HERE](https://github.com/DevoInc/pytho | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | lookupTableName | Lookup table name you are trying to write to | Required | -| headers | Headers of records to upload. Order sensitive. | Optional | +| headers | Headers of records to upload. Order sensitive. | Required | | records | Lookup table records to insert | Required | @@ -390,12 +399,22 @@ adding in extra lookup information found [HERE](https://github.com/DevoInc/pytho ##### Command Example ``` -!devo-write-to-lookup-table lookupTableName="lookup123" headers='["foo", "bar", "baz"]' records='[{"key": "foo1", "values": ["foo1", "bar1", "baz1"]}]' +!devo-write-to-lookup-table lookupTableName="lookup123" headers=`{"headers": ["foo", "bar", "baz"], "key_index": 0, "action": "FULL"}` records=`[{"fields": ["foo1", "bar1", "baz1"], "delete": false}, {"fields": ["foo2", "bar2", "baz2"]}, {"fields": ["foo3", "bar3", "baz3"]}]` ``` ##### Human Readable Output -N/A +Lookup Table Name: lookup123. +Total Records Sent: 3. +Total Bytes Sent: 125. + +##### Entries to load into Devo + +|lookup123|FULL|START|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| +|lookup123|FULL|END|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| +|lookup123|null|"foo1", "bar1", "baz1"| +|lookup123|null|"foo2", "bar2", "baz2"| +|lookup123|null|"foo3", "bar3", "baz3"| #### Youtube Video Demo (Click Image, Will redirect to youtube) [(https://raw.githubusercontent.com/demisto/content/98ead849e9e32921f64f7ac07fda2bff1b5f7c0b/Packs/Devo/doc_files/devo_video.jpg)](https://www.youtube.com/watch?v=jyUqEcWOXfU) From 5eddf60d755bf1bdf4aaf82cd83f699ab8b076b2 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 12 Feb 2024 18:06:10 +0530 Subject: [PATCH 09/17] readme file changes updated --- Packs/Devo/Integrations/Devo_v2/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Packs/Devo/Integrations/Devo_v2/README.md b/Packs/Devo/Integrations/Devo_v2/README.md index c63f1f5a8f33..6983daa3fee1 100644 --- a/Packs/Devo/Integrations/Devo_v2/README.md +++ b/Packs/Devo/Integrations/Devo_v2/README.md @@ -408,10 +408,13 @@ Total Records Sent: 3. Total Bytes Sent: 125. ##### Entries to load into Devo - +The headers of headers array is written into the my.lookup.control table. +|Data| |lookup123|FULL|START|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| |lookup123|FULL|END|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| +The fields of records array is written into the my.lookup.data table. +|Data| |lookup123|null|"foo1", "bar1", "baz1"| |lookup123|null|"foo2", "bar2", "baz2"| |lookup123|null|"foo3", "bar3", "baz3"| From 785a2c469cabad7b2edc3bd60a2ae066b9ce1a3b Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Tue, 13 Feb 2024 13:04:03 +0530 Subject: [PATCH 10/17] optional parameters check added --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 35 ++++++++-------- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 6 --- Packs/Devo/Integrations/Devo_v2/README.md | 40 ++++++++++--------- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index e378e9521cdd..cad678b94688 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -826,12 +826,12 @@ def multi_table_query_command(offset, items): def convert_to_str(value): if isinstance(value, list) and len(value) == 0: - print("Warning: Empty list encountered.") + return_warning("Empty list encountered.") return '[]' elif isinstance(value, dict) and not value: - print("Warning: Empty dictionary encountered.") + return_warning("Empty dictionary encountered.") return '{}' - elif isinstance(value, (list, dict)): + elif isinstance(value, list | dict): return json.dumps(value) return str(value) @@ -957,22 +957,25 @@ def write_to_lookup_table_command(): total_events = 0 total_bytes = 0 - # Validate headers - if not isinstance(headers, dict) or "headers" not in headers or not isinstance(headers["headers"], list): - raise ValueError("Invalid headers format. 'headers' must be a list.") + try: + # Validate headers + if not isinstance(headers, dict) or "headers" not in headers or not isinstance(headers["headers"], list): + raise ValueError("Invalid headers format. 'headers' must be a list.") - columns = headers["headers"] + columns = headers["headers"] - # Validate key_index - key_index = int(headers.get("key_index", 0)) # Ensure it's casted to integer - ## if key_index < 0: - ## raise ValueError("key_index must be a non-negative integer value.") + # Set default values for optional parameters + key_index = int(headers.get("key_index", 0)) # Ensure it's casted to integer + action = headers.get("action", "INC") # Set default value to 'INC' + + # Validate key_index + if key_index < 0: + raise ValueError("key_index must be a non-negative integer value.") + + # Validate action + if action not in {"INC", "FULL"}: + raise ValueError("action must be either 'INC' or 'FULL'.") - # Validate action - action = headers.get("action", "") - ## if action not in {"INC", "FULL"}: - ## raise ValueError("action must be either 'INC' or 'FULL'.") - try: con = Sender(config=engine_config, timeout=60) lookup = Lookup(name=lookup_table_name, con=con) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index 8c9c1c6c863e..d30cd656b8c3 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -514,12 +514,6 @@ def test_write_devo(mock_load_results, mock_write_args): mock_write_args.return_value = MOCK_WRITE_TO_TABLE_RECORDS results = write_to_table_command() assert len(results) == 2 # We expect two entries in the results list - found = False - for result in results: - if "HumanReadable" in result and result["HumanReadable"] == "Total Records Sent: 3.\nTotal Bytes Sent: 6.": - found = True - break - assert found, "Expected string not found in 'HumanReadable' field of results" assert results[0]["EntryContext"]["Devo.LinqQuery"] == "from whatever.table" diff --git a/Packs/Devo/Integrations/Devo_v2/README.md b/Packs/Devo/Integrations/Devo_v2/README.md index 6983daa3fee1..a6f1f5192c34 100644 --- a/Packs/Devo/Integrations/Devo_v2/README.md +++ b/Packs/Devo/Integrations/Devo_v2/README.md @@ -291,7 +291,7 @@ are both given that they must be the same given format. ##### Command Example ``` -!devo-multi-table-query tables='["siem.logtrust.web.activity", "siem.logtrust.web.navigation"]' searchToken="john@doe.com" from=1576845233.193244 to=1576845293.193244 items_per_page=1000 +!devo-multi-table-query tables="[siem.logtrust.alert.info, siem.logtrust.web.navigation]" searchToken="parag@metronlabs.com" from=1707416980 to=1707805927 ``` ##### Human Readable Output @@ -351,16 +351,16 @@ Total Bytes Sent: 196. ##### Entries to load into Devo -|Data| -|---| -|This is my first event| -|This is my second log| -|{"hello": "world"}| -|{"hello": "friend"}| -|["a", "b", "c"]| -|["1", "2", "3"]| -|1234| -|true| +|eventdate|format|cluster|instance|message| +|---|---|---|---|---| +|2024-02-12 17:51:51.277|test|-|-|This is my first event| +|2024-02-12 17:51:51.277|test|-|-|This is my second log| +|2024-02-12 17:51:51.277|test|-|-|{"hello": "world"}| +|2024-02-12 17:51:51.277|test|-|-|{"hello": "friend"}| +|2024-02-12 17:51:51.277|test|-|-|["a", "b", "c"]| +|2024-02-12 17:51:51.277|test|-|-|["1", "2", "3"]| +|2024-02-12 17:51:51.277|test|-|-|1234| +|2024-02-12 17:51:51.277|test|-|-|True| Link to Devo Query @@ -409,15 +409,19 @@ Total Bytes Sent: 125. ##### Entries to load into Devo The headers of headers array is written into the my.lookup.control table. -|Data| -|lookup123|FULL|START|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| -|lookup123|FULL|END|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| + +|eventdate|lookup|lookupId|lookupOp|type|lookupFields| +|---|---|---|---|---|---| +|2024-02-13 10:57:14.238|lookup123|1707802034.0032315_lookup123|FULL|START|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| +|2024-02-13 10:57:24.246|lookup123|1707802034.0032315_lookup123|FULL|END|[{"foo":{"type":"str","key":true}},{"bar":{"type":"str"}},{"baz":{"type":"str"}}]| The fields of records array is written into the my.lookup.data table. -|Data| -|lookup123|null|"foo1", "bar1", "baz1"| -|lookup123|null|"foo2", "bar2", "baz2"| -|lookup123|null|"foo3", "bar3", "baz3"| + +|eventdate|lookup|lookupId|lookupOp|rawData| +|---|---|---|---|---| +|2024-02-13 10:57:19.239|lookup123|1707802034.0032315_lookup123|null|"foo1", "bar1", "baz1"| +|2024-02-13 10:57:19.240|lookup123|1707802034.0032315_lookup123|null|"foo2", "bar2", "baz2"| +|2024-02-13 10:57:19.240|lookup123|1707802034.0032315_lookup123|null|"foo3", "bar3", "baz3"| #### Youtube Video Demo (Click Image, Will redirect to youtube) [(https://raw.githubusercontent.com/demisto/content/98ead849e9e32921f64f7ac07fda2bff1b5f7c0b/Packs/Devo/doc_files/devo_video.jpg)](https://www.youtube.com/watch?v=jyUqEcWOXfU) From a5bea33b047caf048ad607caa37a3163993bb5a9 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 19 Feb 2024 15:08:09 +0530 Subject: [PATCH 11/17] code reverted --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index cad678b94688..b9e97f38b5ea 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -1049,17 +1049,14 @@ def main(): OFFSET = 0 items_per_page = int(demisto.args().get("items_per_page", ITEMS_PER_PAGE)) if items_per_page <= 0: - raise ValueError("The items_per_page should be a positive non-zero value.") - limit = int(demisto.args().get("limit", LIMIT)) - if limit <= 0: - raise ValueError("The 'limit' parameter should be a positive non-zero value.") + raise ValueError("items_per_page should be a positive non-zero value.") total = 0 - demisto.results(multi_table_query_command(OFFSET, limit)) + demisto.results(multi_table_query_command(OFFSET, items_per_page)) total = total + COUNT_MULTI_TABLE - while limit * 2 == COUNT_MULTI_TABLE: - OFFSET = OFFSET + limit + while items_per_page * 2 == COUNT_MULTI_TABLE: + OFFSET = OFFSET + items_per_page total = total + COUNT_MULTI_TABLE - demisto.results(multi_table_query_command(OFFSET, limit)) + demisto.results(multi_table_query_command(OFFSET, items_per_page)) elif demisto.command() == "devo-write-to-table": demisto.results(write_to_table_command()) elif demisto.command() == "devo-write-to-lookup-table": From 0ab503948da14125424a4897b5776637da557320 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Wed, 21 Feb 2024 12:52:21 +0530 Subject: [PATCH 12/17] updated test cases & implemented the requested suggestions --- Packs/Devo/Integrations/Devo_v2/Devo_v2.py | 2 +- Packs/Devo/Integrations/Devo_v2/Devo_v2.yml | 2 +- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 44 +++++++++++++++++++ Packs/Devo/Integrations/Devo_v2/README.md | 6 +-- Packs/Devo/ReleaseNotes/1_3_1.md | 7 +-- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py index b9e97f38b5ea..8ef908034d3e 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.py @@ -825,7 +825,7 @@ def multi_table_query_command(offset, items): def convert_to_str(value): - if isinstance(value, list) and len(value) == 0: + if isinstance(value, list) and not value: return_warning("Empty list encountered.") return '[]' elif isinstance(value, dict) and not value: diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml index 66ff8b14ee4b..bf4af80e068a 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml @@ -233,7 +233,7 @@ script: type: unknown description: Writes lookup table entry records to a specified Devo table. execution: true - dockerimage: demisto/devo:1.0.0.87277 + dockerimage: demisto/devo:1.0.0.87725 isfetch: true subtype: python3 tests: diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index d30cd656b8c3..b4a007b32132 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -152,6 +152,18 @@ "tableName": "whatever.table", "records": '[{"foo": "hello"}, {"foo": "world"}, {"foo": "demisto"}]', } +MOCK_WRITER_ARGS_LIST = { + "tableName": "whatever.table", + "records": '[["a", "b", "c"], ["1", "2", "3"]]', +} +MOCK_WRITER_ARGS_EMPTY = { + "tableName": "whatever.table", + "records": '[1234, true]', +} +MOCK_WRITER_ARGS_STR = { + "tableName": "whatever.table", + "records": '["This is my first event", "This is my second log"]', +} MOCK_WRITE_TO_TABLE_RECORDS = { "tableName": "whatever.table", "records": '[{"hello": "world"}, {"abc": "xyz"}, {"data": "test"}]', @@ -517,6 +529,20 @@ def test_write_devo(mock_load_results, mock_write_args): assert results[0]["EntryContext"]["Devo.LinqQuery"] == "from whatever.table" +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_str(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS_STR + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert 'Failed to execute command devo-write-to-table.' in error_msg + + @patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) @patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) @patch("Devo_v2.demisto.args") @@ -529,11 +555,29 @@ def test_write_devo_data(mock_load_results, mock_write_args): except ValueError as exc: error_msg = str(exc) assert "Error decoding JSON. Please ensure the records are valid JSON." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_list(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS_LIST try: write_to_table_command() except ValueError as exc: error_msg = str(exc) assert "The 'records' parameter must be a list." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_no_data(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS_EMPTY try: write_to_table_command() except ValueError as exc: diff --git a/Packs/Devo/Integrations/Devo_v2/README.md b/Packs/Devo/Integrations/Devo_v2/README.md index a6f1f5192c34..7244c74260c8 100644 --- a/Packs/Devo/Integrations/Devo_v2/README.md +++ b/Packs/Devo/Integrations/Devo_v2/README.md @@ -327,7 +327,7 @@ For more information on the way we write to a table please refer to this documen | **Argument Name** | **Description** | **Required** | |-------------------|-----------------------------------------------------------------|--------------| | tableName | Table name to write to | Required | -| records | Records to write to given tableName | Required | +| records | Records written to specified Devo table. | Required | | linqLinkBase | Overrides the global link base so is able to be set at run time | Optional | @@ -386,8 +386,8 @@ adding in extra lookup information found [HERE](https://github.com/DevoInc/pytho | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | lookupTableName | Lookup table name you are trying to write to | Required | -| headers | Headers of records to upload. Order sensitive. | Required | -| records | Lookup table records to insert | Required | +| headers | Headers for lookup table control. | Required | +| records | Records to write to the specified table. | Required | ##### Context Output diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index 4562c7d98338..9d5939ab472a 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -1,10 +1,7 @@ #### Integrations ##### Devo v2 -- Updated the Docker image to: *demisto/devo:1.0.0.87277*. - -- **Release Note Update:** - - Improved functionality for writing records to Devo tables and lookup tables has been updated. - - The Docker image has been updated to version *demisto/devo:1.0.0.86778*. + - Improved functionality for **devo-write-to-table** and **devo-write-to-lookup-table** commands. + - Updated the Docker image to: *demisto/devo:1.0.0.87725*. These updates enhance the capabilities of the Devo integration by introducing improved methods for writing records to both Devo tables and lookup tables. The new functions provide enhanced flexibility and efficiency in managing and sending data to Devo's platform. Additionally, users can benefit from the latest version of the Docker image, ensuring compatibility and stability with the integration. \ No newline at end of file From f4a487ce7f1722b150377774aa08ab414c34214f Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 26 Feb 2024 11:08:41 +0530 Subject: [PATCH 13/17] reverted changes --- Packs/Devo/ReleaseNotes/1_3_1.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index 9d5939ab472a..ebf637362e98 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -3,5 +3,3 @@ ##### Devo v2 - Improved functionality for **devo-write-to-table** and **devo-write-to-lookup-table** commands. - Updated the Docker image to: *demisto/devo:1.0.0.87725*. - -These updates enhance the capabilities of the Devo integration by introducing improved methods for writing records to both Devo tables and lookup tables. The new functions provide enhanced flexibility and efficiency in managing and sending data to Devo's platform. Additionally, users can benefit from the latest version of the Docker image, ensuring compatibility and stability with the integration. \ No newline at end of file From 0ed392c426832884318d2dfbc6bfd0e662f0f067 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 26 Feb 2024 12:52:08 +0530 Subject: [PATCH 14/17] added additional unit test cases --- .../Devo/Integrations/Devo_v2/Devo_v2_test.py | 82 +++++++++++++++++++ Packs/Devo/Integrations/Devo_v2/README.md | 2 +- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py index b4a007b32132..60c2ba7f9839 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2_test.py @@ -657,6 +657,88 @@ def test_write_lookup_devo_invalid_action( assert "action must be either 'INC' or 'FULL'." in error +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_empty_records(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "Error decoding JSON. Please ensure the records are valid JSON." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_invalid_json(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = MOCK_WRITER_ARGS + try: + write_to_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "Error decoding JSON. Please ensure the records are valid JSON." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +@patch("Devo_v2.Lookup") +def test_write_lookup_devo_invalid_headers_format( + mock_lookup_writer_lookup, mock_lookup_writer_sender, mock_lookup_write_args +): + mock_lookup_write_args.return_value = MOCK_LOOKUP_WRITER_ARGS + mock_lookup_writer_sender.return_value = MOCK_SENDER() + mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() + try: + write_to_lookup_table_command() + except ValueError as exc: + error_msg = str(exc) + assert "Invalid headers format. 'headers' must be a list." in error_msg + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +def test_write_devo_empty_records_param(mock_load_results, mock_write_args): + mock_load_results.return_value.load.return_value = MOCK_LINQ_RETURN + mock_write_args.return_value = {"tag": "test_tag", "tableName": "test_table", "records": "{}"} + try: + write_to_table_command() + except SystemExit: + pass # Handle SystemExit gracefully in tests + + +@patch("Devo_v2.WRITER_RELAY", MOCK_WRITER_RELAY, create=True) +@patch("Devo_v2.WRITER_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True) +@patch("Devo_v2.demisto.args") +@patch("Devo_v2.Sender") +@patch("Devo_v2.Lookup") +def test_write_lookup_missing_args( + mock_lookup_writer_lookup, mock_lookup_writer_sender, mock_lookup_write_args +): + # Ensure that headers and records are properly formatted JSON strings + mock_lookup_write_args.return_value = { + "lookupTableName": "test_table", + "headers": '["header1", "header2"]', + "records": '["record1", "record2"]' + } + mock_lookup_writer_sender.return_value = MOCK_SENDER() + mock_lookup_writer_lookup.return_value = MOCK_LOOKUP() + # Provide all required arguments + try: + write_to_lookup_table_command() + except SystemExit: + pass # Handle SystemExit gracefully in tests + + @patch("Devo_v2.demisto_ISO", return_value="2022-03-15T15:01:23.456Z") def test_alert_to_incident_all_data(mock_demisto_ISO): incident = alert_to_incident(ALERT, USER_PREFIX) diff --git a/Packs/Devo/Integrations/Devo_v2/README.md b/Packs/Devo/Integrations/Devo_v2/README.md index 7244c74260c8..7d9352642a74 100644 --- a/Packs/Devo/Integrations/Devo_v2/README.md +++ b/Packs/Devo/Integrations/Devo_v2/README.md @@ -327,7 +327,7 @@ For more information on the way we write to a table please refer to this documen | **Argument Name** | **Description** | **Required** | |-------------------|-----------------------------------------------------------------|--------------| | tableName | Table name to write to | Required | -| records | Records written to specified Devo table. | Required | +| records | Records written to specified Devo table. | Required | | linqLinkBase | Overrides the global link base so is able to be set at run time | Optional | From e68ec0bccc259e3aa4edeb9eb6e14e15474fdcca Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 4 Mar 2024 11:20:08 +0530 Subject: [PATCH 15/17] Docker image tag updated --- Packs/Devo/Integrations/Devo_v2/Devo_v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml index bf4af80e068a..1c1799ff9e40 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml @@ -233,7 +233,7 @@ script: type: unknown description: Writes lookup table entry records to a specified Devo table. execution: true - dockerimage: demisto/devo:1.0.0.87725 + dockerimage: demisto/devo:1.0.0.88693 isfetch: true subtype: python3 tests: From 5ce08d1765b2a977f86bd7fde79ea86802b54957 Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Mon, 4 Mar 2024 11:44:09 +0530 Subject: [PATCH 16/17] Updated the release note for latest docker image --- Packs/Devo/ReleaseNotes/1_3_1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index ebf637362e98..c5d4eefb25c7 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -1,5 +1,6 @@ #### Integrations ##### Devo v2 +- Updated the Docker image to: *demisto/devo:1.0.0.88693*. - Improved functionality for **devo-write-to-table** and **devo-write-to-lookup-table** commands. - - Updated the Docker image to: *demisto/devo:1.0.0.87725*. + From 8cf0bac9cbbf2c035444cf5bb491738eec71b8bb Mon Sep 17 00:00:00 2001 From: namrata-metron Date: Tue, 5 Mar 2024 11:16:18 +0530 Subject: [PATCH 17/17] Docker Image updated --- Packs/Devo/Integrations/Devo_v2/Devo_v2.yml | 2 +- Packs/Devo/ReleaseNotes/1_3_1.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml index 1c1799ff9e40..b45992a27bdc 100644 --- a/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml +++ b/Packs/Devo/Integrations/Devo_v2/Devo_v2.yml @@ -233,7 +233,7 @@ script: type: unknown description: Writes lookup table entry records to a specified Devo table. execution: true - dockerimage: demisto/devo:1.0.0.88693 + dockerimage: demisto/devo:1.0.0.89201 isfetch: true subtype: python3 tests: diff --git a/Packs/Devo/ReleaseNotes/1_3_1.md b/Packs/Devo/ReleaseNotes/1_3_1.md index c5d4eefb25c7..1adf062ebffb 100644 --- a/Packs/Devo/ReleaseNotes/1_3_1.md +++ b/Packs/Devo/ReleaseNotes/1_3_1.md @@ -1,6 +1,7 @@ #### Integrations ##### Devo v2 -- Updated the Docker image to: *demisto/devo:1.0.0.88693*. +- Updated the Docker image to: *demisto/devo:1.0.0.89201*. + - Improved functionality for **devo-write-to-table** and **devo-write-to-lookup-table** commands.