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)