Devo_v2

Use the Devo v2 integration to query Devo for alerts, lookup tables, with support of pagination, and to write to lookup tables.

Analytics & SIEM · Devo

Details

IDDevo_v2
ProviderDevo Technology
CategoryAnalytics & SIEM
From Version5.0.0
Docker Imagedemisto/devo:1.0.0.9285614
Supported ModulesAgentix XSIAM

README

Overview


Use the Devo v2 integration to query Devo for alerts, lookup tables, with support of pagination, and to write to lookup tables.\
This integration was integrated and tested with version 6.0+ Devo.\
Devo is a generic log management
solution which can also act as an advanced SIEM. Users are able to query petabytes of data in a fraction
of the time that other traditional time series databases can’t.

Use Cases


  • Ingest all user defined alerts from Devo into Cortex XSOAR
  • Query any data source available on the Devo.
  • Run needle in haystack multi-table queries for threat hunting incidents.
  • Write results back to Devo as searchable records or alerts.
  • Write new entries into lookup tables to be used in synthesis tables (ALPHA)

Prerequisites


  • Active Devo account and domain.
  • OAuth token with the *.** permissions.
  • Writer TLS Certificate, Key, and Chain if writing back to Devo.

Get your Cortex XSOAR OAuth Token

  1. Login to your Devo domain with a user with the ability to create security credentials.
  2. Navigate to Administration > Credentials > Authentication Tokens.
  3. If a token for Cortex XSOAR has not already been created, Click CREATE NEW TOKEN

    • Create the Token with *.** table permissions as an apiv2 token.
  4. Note the generated Token

Get your Cortex XSOAR Writer Credentials

  1. Login to your Devo domain with a user with the ability to create security credentials.
  2. Navigate to Administration > Credentials > X.509 Certificates.
  3. Click NEW CERTIFICATE if you do not already have a set of keys for Cortex XSOAR.
  4. Download the following files:
    • Certificate
    • Private Key
    • CHAIN CA

Configure Devo v2 on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Devo v2.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Required
    Query Server Endpoint (e.g. https://apiv2-us.devo.com/search/query) True
    Port (e.g. 443) False
    OAuth Token (Preferred method) True
    Writer relay to connect to (e.g. us.elb.relay.logtrust.net) False
    Writer JSON credentials False
    Devo base domain False
    Fetch incidents False
    Incident type False
    Custom Alert Table name(if not provided, ‘siem.logtrust.alert.info’ will be used) False
    Custom Alert Table prefix (provide prefix if custom table name provided) False
    Fetch incidents alert filter (Same filters for get-alerts) False
    Deduplication parameters JSON if required False
    Fetch Incident Limit(must be between 10 and 100; advisable 50 for better performance.) False
    Incidents Fetch Interval False
    Global query default timeout in seconds False
    Trust any certificate (not secure) False
    Use system proxy settings False
    Fetch Incidents Lookback Time (in seconds). Must be between 3600 (1 hour; default) to 86400 (24 hours). False
    Fetch Incident Time Frame (in seconds) False
  4. Click Test to validate the URLs, token, and connection.

Configuration Details

  • Writer JSON credentials Optional

      {
          "key": "string",
          "crt": "string",
          "chain": "string"
      }
    

    The JSON should be given in one line, and new lines should be replaced with \n, for example:

      {"key": "-----BEGIN RSA PRIVATE KEY-----\n\n...\n-----END RSA PRIVATE KEY-----", "crt": "-----BEGIN CERTIFICATE-----\n\n...\n-----END CERTIFICATE-----", "chain": "-----BEGIN CERTIFICATE-----\n\n...\n-----END CERTIFICATE-----"}
    
  • Fetch incidents alert filter (Same filters for get-alerts) Optional

      {
          "type": <"AND" | "OR">,
          "filters" : [
              {"key": "<String Devo Column Name>", "operator": "<Devo Linq Operator>", "value": "<string>"},
              {"key": "<String Devo Column Name>", "operator": "<Devo Linq Operator>", "value": "<string>"},
              ...
              {"key": "<String Devo Column Name>", "operator": "<Devo Linq Operator>", "value": "<string>"}
          ]
      }
    

Note: single table query and multi table query can take long hours to complete runing and xsoar only allows commands to run for 5 minutes.
To override that follow the below steps:

  • Login to xsoar.
  • Go to settings.
  • Go to about > troubleshooting.
  • In server configurations add the following:
    • key = .devo-run-query.timeout, value = 1440
    • key = .devo-multi-table-query.timeout, value = 1440
  • Click save.

Fetched Incidents Data

Fetched incidents data will resemble closely to that of the data you get back from the devo-get-alerts command.\
The format is as follows. The keyN in the main body will be the columns that you used to define your alert in Devo.

{
  "devo.metadata.alert": {
    "eventdate": "string",
    "alertHost": "string",
    "domain": "string",
    "priority": "string",
    "context": "string",
    "category": "string",
    "status": "string",
    "alertId": "string",
    "srcIp": "string",
    "srcPort": "string",
    "srcHost": "string",
    "dstIp": "string",
    "dstPort": "string",
    "dstHost": "string",
    "application": "string",
    "engine":  "string"
  },
  <key0>: <value0>,
  <key1>: <value1>,
  ...
  <keyN>: <valueN>
}

Currently the only data that is fetchable in Devo are the alerts that users have defined in the platform.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

  1. devo-run-query
  2. devo-get-alerts
  3. devo-multi-table-query
  4. devo-write-to-table
  5. devo-write-to-lookup-table

1. devo-run-query


Queries Devo based on the specified LINQ query.

Please refer to to the Devo documentation for building a query with LINQ
HERE.

Required Permissions

A Cortex XSOAR instance configured with the correct OAuth token that has permission to query the target tables

Base Command

devo-run-query

Input

Argument Name Description Required
query A LINQ query to run in Devo, with pagination support. Required
from Start datetime for the specified query. This argument supports natural language (e.g., 2 day, 3 week), Unix timestamps, Python datetime objects, and string datetimes. Required
to End datetime for specified query. If provided must be in same format as “from” argument. This argument is ignored in a date range. Optional
items_per_page Enter the per page value you want to set. Default is 50. Optional
queryTimeout Timeout in seconds for this query to run against Devo to override the minute default in the platform. Default is 60. Optional
writeToContext Whether to write results to context. Can be “true” or “false”. Default is true. Optional
linqLinkBase Overrides the global Devo base domain for linq linking. Optional
filtered_columns The subset of fields (separated by a comma) that you want to display from the query result. Use this if you want to filter out unwanted columns in your result. Context data is eventually modified by this parameter. Optional
ip_as_string Flag to return IP as string. Optional

Time Format for from and to Arguments

This integration supports the following time formats for the from and to arguments:

  • Date ranges such as “1 day”, “30 minutes”, etc. If a date range is provided for from, the to parameter is not needed and will be ignored.
  • Unix timestamps in milliseconds and seconds.
  • Datetime strings in the format ‘%Y-%m-%dT%H:%M:%S’.
  • Python datetime objects.

Please ensure that the from and to times are provided in the same format.\
Using unsupported formats will result in an error.

Context Output

Path Type Description
Devo.QueryResults unknown List of dictionary alerts from the specified time range.
Devo.QueryLink unknown The link to the Devo table for executed query.

Command Example

!devo-run-query query="from siem.logtrust.web.activity select *" from=1576845233.193244 to=1576845293.193244 items_per_page=1000

Human Readable Output

Devo run query results

eventdate level domain userid username sessionid correlationId srcHost srcPort serverHost serverPort type method url headers params referer userAgent locale contentLength responseLength responseTime result resourceInfo errorInfo country region city isp org
2019-10-23T17:18:29.784000 INFO helloworld 988409ce-3955-44a8-bcbb-b613bc8d9f8e john.doe@devo.com 22671FE384D9FDF20E9BFFD7F4469971   1.2.3.4 45590 us.devo.com 8080   GET https://us.devo.com/alerts/alertsGlobe.json   {origin:app.custom.tsAnomalyDetectionDev,serialNumber:ad475065-b0ef-4bbe-a620-a6dcd0874629,} https://us.devo.com/welcome Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36 en_US 0 124 7 OK     US NJ Secaucus Ppman Services Srl M247 Ltd New Jersey
2019-10-23T17:18:29.800000 INFO helloworld 988409ce-3955-44a8-bcbb-b613bc8d9f8e john.doe@devo.com 22671FE384D9FDF20E9BFFD7F4469971   1.2.3.4 45588 us.devo.com 8080   GET https://us.devo.com/domain/notification.json   {origin:app.custom.tsAnomalyDetectionDev,serialNumber:ad475065-b0ef-4bbe-a620-a6dcd0874629,} https://us.devo.com/welcome Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36 en_US 0 119 24 OK     US NJ Secaucus Ppman Services Srl M247 Ltd New Jersey
2019-10-23T17:18:59.780000 INFO helloworld 988409ce-3955-44a8-bcbb-b613bc8d9f8e john.doe@devo.com 22671FE384D9FDF20E9BFFD7F4469971   1.2.3.4 45816 us.devo.com 8080   GET https://us.devo.com/alerts/alertsGlobe.json   {origin:app.custom.tsAnomalyDetectionDev,serialNumber:ad475065-b0ef-4bbe-a620-a6dcd0874629,} https://us.devo.com/welcome Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36 en_US 0 124 7 OK     US NJ Secaucus Ppman Services Srl M247 Ltd New Jersey
DevoTableLink
Devo Direct Link

2. devo-get-alerts


Queries alerts in the specified timeframe.

Alerts are based off the table siem.logtrust.alert.info found in your Devo account.\
Please refer to this table
for a list of columns you can filter off of. Also please refer back to the LINQ documentation for operations
that are allowed.

Required Permissions

Requires a Devo OAuth token that has read permission on siem.logtrust.alert.info table.

Base Command

devo-get-alerts

Input

Argument Name Description Required
table_name name of alert table to fetch alerts from a table. If not provided ‘siem.logtrust.alert.info’ will be used. Optional
prefix Prefix to use for the column names. Optional
from Start datetime for alerts to fetch. Required
to End datetime for alerts to fetch. Optional
items_per_page Enter the per page value you want to set. Default is 50. Optional
filters Key value filter to apply to retrieve the specified alerts. For more information, see the Devo documentation. Optional
queryTimeout Timeout in seconds for this query to run against Devo to override the minute default in the platform. Default is 60. Optional
writeToContext Whether to write results to context. Can be “true” or “false”. Default is true. Optional
linqLinkBase Overrides the global Devo base domain for linq linking. Optional
filtered_columns The subset of fields (separated by a comma) that you want to display from the query result. Use this if you want to filter out unwanted columns in your result. Context data is eventually modified by this parameter. Optional

Time Format for from and to Arguments

This integration supports the following time formats for the from and to arguments:

  • Date ranges such as “1 day”, “30 minutes”, etc. If a date range is provided for from, the to parameter is not needed and will be ignored.
  • Unix timestamps in milliseconds and seconds.
  • Datetime strings in the format ‘%Y-%m-%dT%H:%M:%S’.
  • Python datetime objects.

Please ensure that the from and to times are provided in the same format.\
Using unsupported formats will result in an error.

Context Output

Path Type Description
Devo.AlertsResults unknown List of dictionary alerts from the specified time range.
Devo.QueryLink unknown The link to the Devo table for the executed query.

Command Example

!devo-get-alerts from=1576845233.193244 to=1576845293.193244

Human Readable Output

Devo get alerts results

eventdate alertHost domain priority context category status alertId srcIp srcPort srcHost dstIp dstPort dstHost protocol username application engine extraData
2019-10-23T18:18:07.320000 backoffice helloworld 5.0 my.alert.helloworld.simultaneous_login my.context 4 6715552                   pilot.my.alert.helloworld.simultaneous_login duration_seconds: 30.142
cluster: -
prev_timestamp: 2019-10-23+18:17:29.652
instance: -
distance: 294.76
level: info
city: Secaucus
srcHost: 1.2.3.4
prev_city: Waltham
format: output_qs9n126lnvh
prev_geolocation: 42°23’49.925537109375”N+71°14’36.2420654296875”W
message:0.0.0.4Waltham0°0’0.00”N+0°0’0.0”N+0°0’0.00”W0.0.0«br/>eventdate: 2019-10-23+18:18:02.087
prev_srcHost: 50.204.142.130
duration: 0.008372777777777778
indices: 0,9,31,49,69,77,123,136,149,156,204,217,231
payload: 0.0.0.4Waltham0°0’0.00”N+0°0’0.0”N+0°0’0.00”W0.0.0«br/>state: ANOMALOUS
category: modelserverdev
facility: user
username: john.doe@devo.com
geolocation: 0°0’0.00”N+0°0’0.0”W
timestamp: 2019-10-23+18:17:59.794
DevoTableLink
Devo Direct Link

3. devo-multi-table-query


Queries multiple tables for a given token and returns relevant results.

This method is used for when you do not know which columns a specified search token will show up in (Needle in a haystack search)
Thus querying all columns for the search token and returning a union of the given tables.

Required Permissions

A Cortex XSOAR instance configured with the correct OAuth token that has permission to query the target tables

Base Command

devo-multi-table-query

Input

Argument Name Description Required
tables A list of table names to check for the searchToken. Required
searchToken A string to search for in the specified tables (in any column). Required
from Start datetime for the specified query. This argument supports natural language (e.g., 2 day, 3 week), Unix timestamps, Python datetime objects, and string datetimes. Required
to End datetime for specified query. If provided must be in same format as “from” argument. This argument is ignored in a date range. Optional
limit Limit of results to return to context. 0 for no limit. Default is 50. Optional
queryTimeout Timeout in seconds for this query to run against Devo to override the minute default in the platform. Default is 60. Optional
writeToContext Whether to write results to context. Can be “true” or “false”. Default is true. Optional
items_per_page Enter the per page value you want to set. Default is 50. Optional
filtered_columns The subset of fields (separated by a comma) that you want to display from the query result. Use this if you want to filter out unwanted columns in your result. Context data is eventually modified by this parameter. Optional

Time Format for from and to Arguments

This integration supports the following time formats for the from and to arguments:

  • Date ranges such as “1 day”, “30 minutes”, etc. If a date range is provided for from, the to parameter is not needed and will be ignored.
  • Unix timestamps in milliseconds and seconds.
  • Datetime strings in the format ‘%Y-%m-%dT%H:%M:%S’.
  • Python datetime objects.

Please ensure that the from and to times are provided in the same format.\
Using unsupported formats will result in an error.

Context Output

Path Type Description
Devo.MultiResults unknown A list of dictionary results.
Command Example
!devo-multi-table-query tables="[siem.logtrust.alert.info, siem.logtrust.web.navigation]" searchToken="parag@metronlabs.com" from=1707416980 to=1707805927

Human Readable Output

Devo multi-query results

isp serverPort srcPort responseTime headers eventdate correlationId userEmail responseLength message result method type url userid level referer username region userAgent sessionid resourceInfo contentLength org domain srcHost city params serverHost errorInfo section action origin country locale
Amazon.com 8080 33522     2019-09-18T07:58:39.691000   john@doe.com         0 https://us.devo.com/alerts/view.json 400d338d-c9a6-4930-90a5-357937f3e735   https://us.devo.com/welcome   VA Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4 Safari/537.36 8723DEE4B38F1056BC738760B5E79FD3     Amazon.com helloworld 1.2.3.4 Ashburn   us.devo.com   alert index undefined US  
Amazon.com 8080 33574     2019-09-18T07:58:41.685000   john@doe.com   UserDomain: UserDomain[id: 2942, domain: 6ab72601-e982-4694-8ce6-3d526047f8a5/helloworld, roles: null, logged: 2019-09-18 04:32:58.0, status: 0, creation date: 2018-11-05 14:23:44.0, update date: 2019-09-18 04:32:58.0]|     0 https://us.devo.com/lxcWidgets/lxcWidget.json 400d338d-c9a6-4930-90a5-357937f3e735   https://us.devo.com/welcome   VA Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4 Safari/537.36 8723DEE4B38F1056BC738760B5E79FD3     Amazon.com helloworld 1.2.3.4 Ashburn   us.devo.com   lxc_widgets index undefined US  
Comcast Cable 8080 37094 45   2019-09-18T08:08:21.593000     124   OK GET   https://us.devo.com/alerts/alertsGlobe.json 400d338d-c9a6-4930-90a5-357937f3e735 INFO https://us.devo.com/welcome john@doe.com CA Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4 Safari/537.36 8723DEE4B38F1056BC738760B5E79FD3   0 Comcast Cable helloworld 1.2.3.4 San Francisco {origin:menu.alerts,serialNumber:b181cf08-14e0-49c2-826b-e4ff36afaa84,} us.devo.com         US en_US
Comcast Cable 8080 37092 78   2019-09-18T08:08:21.625000     119   OK GET   https://us.devo.com/domain/notification.json 400d338d-c9a6-4930-90a5-357937f3e735 INFO https://us.devo.com/welcome john@doe.com CA Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4 Safari/537.36 8723DEE4B38F1056BC738760B5E79FD3   0 Comcast Cable helloworld 1.2.3.4 San Francisco {origin:menu.alerts,serialNumber:b181cf08-14e0-49c2-826b-e4ff36afaa84,} us.devo.com         US en_US

4. devo-write-to-table


Writes records to a specified Devo table.

The records written to the table should all be of the same JSON format and to the same table. We currently do not support
writing to multiple tables in a single operation.

For more information on the way we write to a table please refer to this documentation found HERE

Required Permissions

A Cortex XSOAR instance configured with the correct write JSON credentials

Base Command

devo-write-to-table

Input

Argument Name Description Required
tableName Table name to write to 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

Context Output

Path Type Description
Devo.RecordsWritten unknown Records written to specified Devo table.
Devo.LinqQuery unknown The LINQ query to use to see your data in Devo.
Devo.QueryLink unknown The link to the Devo table for the executed query.
Command Example
!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

Total Records Sent: 8.
Total Bytes Sent: 196.

Entries to load into Devo
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
DevoTableLink
Devo Direct Link

5. devo-write-to-lookup-table


Writes lookup table entry records to a specified Devo table.

For more information on lookup tables please refer to documentation found HERE.
We can add extra records with incremental lookup additions.\
Please refer to our Python SDK for more information on how we are
adding in extra lookup information found HERE

Required Permissions

A Cortex XSOAR instance configured with the correct write JSON credentials.

Base Command

devo-write-to-lookup-table

Input

Argument Name Description Required
lookupTableName The lookup table name to write to. Required
headers Headers for lookup table control. Required
records Records to write to the specified table. Required

Context Output

Path Type Description
Devo.RecordsWritten unknown Lookup records written to the lookup table.
Command Example
!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

Lookup Table Name: lookup123.
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.

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.

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)

Known Limitations

  • Currently the lookup table functionality is in Alpha. Please use at your own risk as behavior is still not fully stable.
  • It is up to the user to make sure your Cortex XSOAR instance can handle the amount of data returned by a query.

Configuration parameters

  • reader_endpoint — Query Server Endpoint (e.g. https://apiv2-us.devo.com/search/query) (required)
  • port — Port (e.g. 443)
  • reader_oauth_token — OAuth Token (Preferred method) (required)
  • writer_relay — Writer relay to connect to (e.g. us.elb.relay.logtrust.net)
  • writer_credentials — Writer JSON credentials
  • linq_link_base — Devo base domain
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • table_name — Custom Alert Table name(if not provided, 'siem.logtrust.alert.info' will be used)
  • prefix — Custom Alert Table prefix (provide prefix if custom table name provided)
  • fetch_incidents_filters — Fetch incidents alert filter (Same filters for get-alerts)
  • fetch_incidents_deduplication — Deduplication parameters JSON if required
  • fetch_incidents_limit — Fetch Incident Limit(must be between 10 and 100; advisable 50 for better performance.)
  • incidentFetchInterval — Incidents Fetch Interval
  • timeout — Global query default timeout in seconds
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • fetch_incidents_lookback_seconds — Fetch Incidents Lookback Time (in seconds). Must be between 3600 (1 hour; default) to 86400 (24 hours).
  • fetch_incidents_window — Fetch Incident Time Frame (in seconds)

Commands (5)

  • devo-get-alerts

    Queries alerts in the specified timeframe.

  • devo-multi-table-query

    Queries multiple tables for a given token and returns relevant results.

  • devo-run-query

    Queries Devo based on the specified LINQ query.

  • devo-write-to-lookup-table

    Writes lookup table entry records to a specified Devo table.

  • devo-write-to-table

    Writes records to a specified Devo table.

import json
import time
import copy
from unittest.mock import MagicMock, patch
from datetime import datetime, UTC
import pytest
import re
from freezegun import freeze_time
import pandas as pd

from Devo_v2 import (
    alert_to_incident,
    fetch_incidents,
    run_query_command,
    get_alerts_command,
    multi_table_query_command,
    write_to_table_command,
    write_to_lookup_table_command,
    check_configuration,
    get_time_range,
    _to_unix,
)

MOCK_READER_ENDPOINT = "https://fake.devo.com/query"
MOCK_LINQ_LINK_BASE = "https://devo.com"
MOCK_READER_OAUTH_TOKEN = "123"
MOCK_WRITER_RELAY = "eu.whatever.devo.com"
MOCK_LINQ_RETURN = "from whatever.table"
MOCK_WRITER_CREDENTIALS = {"key": "fake", "crt": "fake", "chain": "fake"}
MOCK_FETCH_INCIDENTS_FILTER = {
    "type": "OR",
    "filters": [
        {"key": "foo", "operator": "->", "value": "baz"},
        {"key": "baz", "operator": "or", "value": "bar"},
    ],
}


MOCK_FETCH_INCIDENTS_LIMIT_INCORRECT = 1000

MOCK_FETCH_INCIDENTS_DEDUPE = {"cooldown": 120}
MOCK_HIGH_CPU_ALERT = {
    "eventdate": time.time() - 20,
    "alertHost": "backoffice",
    "domain": "dsteam",
    "priority": 5.0,
    "context": "CPU_Usage_Alert",
    "category": "my.context",
    "status": 4,
    "alertId": "6294258",
    "srcIp": None,
    "srcPort": None,
    "srcHost": None,
    "dstIp": None,
    "dstPort": None,
    "dstHost": None,
    "protocol": None,
    "username": None,
    "application": None,
    "engine": "CPU_Usage_Alert",
    "extraData": '{"cluster":"-","anomaly_score":"100","indices":'
    '"0%2","_message":"CPU+Usage+Anomaly","instance":"-","payload":'
    '"2019-09-20+08997","pred":"52.52","message":'
    '"097",'
    '"eventdate":"2019-09-20+08%3A52%3A14.096","timestamp":"2019-09-20+08%3A52%3A14"}',
}
MOCK_HIGH_CPU_ALERT_1 = {
    "eventdate": time.time() - 20,
    "alertHost": "backoffice",
    "domain": "dsteam",
    "priority": 5.0,
    "context": "CPU_Usage_Alert",
    "category": "my.context",
    "status": 4,
    "alertId": "6294258",
    "srcIp": 2130706433,
    "srcPort": None,
    "srcHost": None,
    "dstIp": 2130706234,
    "dstPort": None,
    "dstHost": None,
    "protocol": None,
    "username": None,
    "application": None,
    "engine": "CPU_Usage_Alert",
    "extraData": '{"cluster":"-","anomaly_score":"100","indices":'
    '"0%2","_message":"CPU+Usage+Anomaly","instance":"-","payload":'
    '"2019-09-20+08997","pred":"52.52","message":'
    '"097",'
    '"eventdate":"2019-09-20+08%3A52%3A14.096","timestamp":"2019-09-20+08%3A52%3A14"}',
}
MOCK_SIMULTANEOUS_LOGIN_ALERT = {
    "eventdate": time.time() - 45,
    "alertHost": "backoffice",
    "domain": "dsteam",
    "priority": 5.0,
    "context": "simultaneous_login",
    "category": "my.context",
    "status": 4,
    "alertId": "6306076",
    "srcIp": None,
    "srcPort": None,
    "srcHost": None,
    "dstIp": None,
    "dstPort": None,
    "dstHost": None,
    "protocol": None,
    "username": None,
    "application": None,
    "bar": None,
    "baz": None,
    "engine": "simultaneous_login",
    "extraData": '{"duration_seconds":"null","cluster":"-","prev_timestamp":"null","instance":'
    '"-","distance":"null","level":"info","city":"Natick","srcHost":"blahip","prev_city":"None","format":'
    '"output_aaa","prev_geolocation":"None","message":'
    '"0%2ENEW+RECORD'
    "test%40test.comNoneNone550.239."
    '225.14NoneNoneNoneNone","eventdate":"2019-09-20+20%3A41%3A39.688","prev_srcHost":"None","duration":"None",'
    '"indices":"0%2C1C133","payload":'
    '"NEW+RECORDtest%40test.comNoneNoneNatic31.'
    '335.14NoneNoneNoneNone","state":"NEW+RECORD","category":"modelserverdev",'
    '"facility":"user","username":"test%40test.com","geolocation":"421%'
    'C2W","timestamp":"2019-09-20+20%3A41%3A37.395"}',
}
MOCK_SIMULTANEOUS_LOGIN_ALERT_1 = {
    "eventdate": time.time() - 45,
    "alertHost": "backoffice",
    "domain": "dsteam",
    "priority": 5.0,
    "context": "simultaneous_login",
    "category": "my.context",
    "status": 4,
    "alertId": "6306076",
    "srcIp": 2130706234,
    "srcPort": None,
    "srcHost": None,
    "dstIp": 2130706456,
    "dstPort": None,
    "dstHost": None,
    "protocol": None,
    "username": None,
    "application": None,
    "bar": None,
    "baz": None,
    "engine": "simultaneous_login",
    "extraData": '{"duration_seconds":"null","cluster":"-","prev_timestamp":"null","instance":'
    '"-","distance":"null","level":"info","city":"Natick","srcHost":"blahip","prev_city":"None","format":'
    '"output_aaa","prev_geolocation":"None","message":'
    '"0%2ENEW+RECORD'
    "test%40test.comNoneNone550.239."
    '225.14NoneNoneNoneNone","eventdate":"2019-09-20+20%3A41%3A39.688","prev_srcHost":"None","duration":"None",'
    '"indices":"0%2C1C133","payload":'
    '"NEW+RECORDtest%40test.comNoneNoneNatic31.'
    '335.14NoneNoneNoneNone","state":"NEW+RECORD","category":"modelserverdev",'
    '"facility":"user","username":"test%40test.com","geolocation":"421%'
    'C2W","timestamp":"2019-09-20+20%3A41%3A37.395"}',
}
# Create a dictionary containing the list of alerts
mock_query_result = {"object": [MOCK_HIGH_CPU_ALERT, MOCK_SIMULTANEOUS_LOGIN_ALERT], "status": 0}

# Convert the dictionary to a JSON string
MOCK_QUERY_RESULTS = json.dumps(mock_query_result)
MOCK_QUERY_RESULTS_1 = json.dumps({"object": [MOCK_HIGH_CPU_ALERT_1, MOCK_SIMULTANEOUS_LOGIN_ALERT_1], "status": 0})
MOCK_LAST_RUN = {"from_time": time.time() - 60}
MOCK_QUERY_ARGS = {
    "query": "from whatever",
    "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_QUERY_ARGS_FALSE_IP_AS_STRING = {
    "query": "from whatever",
    "from": time.time() - 60,
    "to": time.time(),
    "writeToContext": "true",
    "ip_as_string": "false",
}
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"],
    "searchToken": "searching",
    "from": time.time() - 60,
    "to": time.time(),
    "writeToContext": "true",
    "filtered_columns": "alertId,extraData,context",
}
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"}]',
}
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"}]',
}
MOCK_LOOKUP_WRITER_ARGS = {
    "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_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
IP_AS_STRING = True

ALERT_WITH_MISSING_DATA = {
    "user_prefixcontext": "sample.context.value",
    "user_prefixalertId": "alert123",
    "eventdate": 1646895689000,
    "user_prefixextraData": {
        "alertPriority": "HIGH",
        "alertName": "null",
        "alertDescription": "This is a sample alert",
    },
    "sample_key1": "sample_value1",
    "sample_key2": "sample_value2",
}

ALERT = {
    "user_prefixcontext": "sample.context.value",
    "user_prefixalertId": "alert123",
    "eventdate": 1646895689000,
    "user_prefixextraData": {
        "alertPriority": "HIGH",
        "alertName": "Sample Alert",
        "alertDescription": "This is a sample alert",
    },
    "sample_key1": "sample_value1",
    "sample_key2": "sample_value2",
}
USER_PREFIX = "user_prefix"
EXPECTED_LABELS_WITH_NULL = [
    {"type": "devo.metadata.alert.user_prefixcontext", "value": "sample.context.value"},
    {"type": "devo.metadata.alert.user_prefixalertId", "value": "alert123"},
    {"type": "devo.metadata.alert.eventdate", "value": "1646895689000"},
    {"type": "devo.metadata.alert.sample_key1", "value": "sample_value1"},
    {"type": "devo.metadata.alert.sample_key2", "value": "sample_value2"},
    {"type": "alertPriority", "value": "HIGH"},
    {"type": "alertName", "value": "null"},
    {"type": "alertDescription", "value": "This is a sample alert"},
]

EXPECTED_LABELS = [
    {"type": "devo.metadata.alert.user_prefixcontext", "value": "sample.context.value"},
    {"type": "devo.metadata.alert.user_prefixalertId", "value": "alert123"},
    {"type": "devo.metadata.alert.eventdate", "value": "1646895689000"},
    {"type": "devo.metadata.alert.sample_key1", "value": "sample_value1"},
    {"type": "devo.metadata.alert.sample_key2", "value": "sample_value2"},
    {"type": "alertPriority", "value": "HIGH"},
    {"type": "alertName", "value": "Sample Alert"},
    {"type": "alertDescription", "value": "This is a sample alert"},
]
LAST_RUN_DATA = {"from_time": 1691307869.0, "last_fetch_events": [{"123": 1691307869.0}]}

EVENTS = [
    {
        "alertId": "123",
        "extraData": {"key1": "value1", "key2": "value2"},
        "eventdate": 1691307869000,
        "context": "value1",
    },
    {
        "alertId": "456",
        "extraData": {"key1": "value3", "key2": "value4"},
        "eventdate": 1691394269000,
        "context": "value2",
    },
    {
        "alertId": "789",
        "extraData": {"key1": "value3", "key2": "value4"},
        "eventdate": 1691480669000,
        "context": "value3",
    },
]
MOCK_EVENTS = json.dumps({"object": EVENTS, "status": 0})

EXPECTED_LAST_RUN_DATA = {"from_time": 1691480669.0, "last_fetch_events": [{"456": 1691394269.0}, {"789": 1691480669.0}]}


class MOCK_LOOKUP:
    def send_headers(*args, **kw):
        pass

    def send_control(*args, **kw):
        pass

    def send_data_line(*args, **kw):
        pass


class MOCK_SOCKET:
    def shutdown(*args, **kw):
        pass


class MOCK_SENDER:
    socket = MOCK_SOCKET()

    def flush_buffer(*args, **kw):
        pass


class MOCK_READER:
    pass


@freeze_time("2024-01-01T00:00", tz_offset=0)
def test_time_range():
    tolerance: float = 0.001
    time_from = time.time() - 60
    time_to = time.time()
    time_from_string = "2020-01-10T01:30:30"
    time_from_string_ts = 1578619830
    time_to_string = "2020-01-10T02:30:30"
    time_to_string_ts = 1578623430
    future_timestamp = 2862390524
    # Test Unix timestamp input
    assert get_time_range(time_from, None)[0] == time_from
    assert get_time_range(time_from, time_to)[1] == time_to
    assert get_time_range(str(time_from), None)[0] == time_from
    assert get_time_range(str(time_from), str(time_to))[1] == time_to
    # Test natural language input
    assert get_time_range("1 minute", None)[0] - time_from < abs(tolerance)
    assert get_time_range("2 minute", None)[0] <= time_from
    # Test string datetime input
    assert get_time_range(time_from_string, None)[0] - time_from_string_ts < abs(tolerance)
    assert get_time_range(time_from_string, time_to_string)[1] - time_to_string_ts < abs(tolerance)
    # Test Python datetime object input
    dt_from = datetime.fromtimestamp(time_from)
    datetime.fromtimestamp(time_to)
    # Convert Python datetime object timestamps to milliseconds
    result_timestamp_ms = get_time_range(dt_from, None)[0] * 1000
    expected_from = round(dt_from.timestamp() * 1000)
    assert abs(result_timestamp_ms - expected_from) < tolerance
    # Additional test for Python datetime object input
    dt_additional = datetime.now()
    assert get_time_range(dt_additional, None)[0] == dt_additional.timestamp()
    # Negative test for future timestamp
    try:
        get_time_range(future_timestamp, None)[0]
    except ValueError as exc:
        error_msg = str(exc)
        assert "Date should not be greater than current time" in error_msg


@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.WRITER_RELAY", MOCK_WRITER_RELAY, create=True)
@patch("Devo_v2.WRITE_CREDENTIALS", MOCK_WRITER_CREDENTIALS, create=True)
@patch("Devo_v2.FETCH_INCIDENTS_FILTER", MOCK_FETCH_INCIDENTS_FILTER, create=True)
@patch("Devo_v2.FETCH_INCIDENTS_DEDUPE", MOCK_FETCH_INCIDENTS_DEDUPE, create=True)
@patch("Devo_v2.Sender")
@patch("Devo_v2.Client.query")
def test_command(mock_query_results, mock_write_args):
    mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS)
    mock_write_args.return_value = MOCK_WRITER_ARGS
    assert check_configuration()


@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.WRITER_RELAY", MOCK_WRITER_RELAY, create=True)
@patch("Devo_v2.WRITE_CREDENTIALS", {"key": "fake", "chain": "fake"}, create=True)
@patch("Devo_v2.FETCH_INCIDENTS_FILTER", MOCK_FETCH_INCIDENTS_FILTER, create=True)
@patch("Devo_v2.FETCH_INCIDENTS_DEDUPE", MOCK_FETCH_INCIDENTS_DEDUPE, create=True)
@patch("Devo_v2.Sender")
@patch("Devo_v2.Client.query")
def test_command_new(mock_query_results, mock_write_args):
    mock_query_results.return_value = json.dumps({"success": False})
    mock_write_args.return_value = MOCK_WRITER_ARGS
    assert check_configuration() is False


@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.FETCH_INCIDENTS_FILTER", MOCK_FETCH_INCIDENTS_FILTER, create=True)
@patch("Devo_v2.Client.query")
def test_first_fetch_incidents(mock_query_results):
    mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS)
    incidents = fetch_incidents()
    assert len(incidents) == 2
    assert json.loads(incidents[0]["rawJSON"])["devo.metadata.alert"]["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.FETCH_INCIDENTS_FILTER", MOCK_FETCH_INCIDENTS_FILTER, create=True)
@patch("Devo_v2.demisto.getLastRun")
@patch("Devo_v2.Client.query")
def test_next_fetch(mock_query_results, mock_last_run):
    mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS)
    mock_last_run.return_value = MOCK_LAST_RUN
    incidents = fetch_incidents()
    assert len(incidents) == 2
    assert json.loads(incidents[1]["rawJSON"])["devo.metadata.alert"]["context"] == "simultaneous_login"


@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.Client.query")
def test_get_alerts(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"


@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.Client.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.Client.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.Client.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")
@patch("Devo_v2.Client.query")
def test_run_query(mock_query_results, mock_args_results):
    mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS)
    mock_args_results.return_value = MOCK_QUERY_ARGS
    results = run_query_command(OFFSET, ITEMS_PER_PAGE, IP_AS_STRING)
    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.Client.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, IP_AS_STRING)


@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.Client.query")
def test_run_query_with_ip_as_string_false(mock_query_results, mock_args_results):
    mock_query_results.return_value = copy.deepcopy(MOCK_QUERY_RESULTS_1)
    mock_args_results.return_value = MOCK_QUERY_ARGS_FALSE_IP_AS_STRING
    IP_AS_STRING = False
    results = run_query_command(OFFSET, ITEMS_PER_PAGE, IP_AS_STRING)
    assert (results[1]["HumanReadable"]).find("Devo Direct Link") != -1
    assert len(results) == 2
    assert results[0]["Contents"][0]["context"] == "CPU_Usage_Alert"
    assert results[0]["Contents"][0]["srcIp"] == 2130706433
    assert results[0]["Contents"][1]["srcIp"] == 2130706234


@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.Client.query")
@patch("Devo_v2.Client")
@patch("Devo_v2.get_types")
def test_multi_query(
    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_ARGS
    mock_submit_results.return_value = None
    mock_wait_results.return_value = (None, None)
    results = multi_table_query_command(OFFSET, ITEMS_PER_PAGE)
    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.Client.query")
@patch("Devo_v2.Client")
@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")
@patch("Devo_v2.Sender")
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) == 2  # We expect two entries in the results list
    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")
@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


@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:
        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")
@patch("Devo_v2.Sender")
@patch("Devo_v2.Lookup")
def test_write_lookup_devo(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()
    results = write_to_lookup_table_command()
    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.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.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)
    assert incident["name"] == "Sample Alert"
    assert incident["severity"] == 3
    assert incident["details"] == "alert123"
    assert incident["description"] == "This is a sample alert"
    assert incident["occurred"] == "2022-03-15T15:01:23.456Z"
    assert incident["labels"] == EXPECTED_LABELS
    assert "devo.metadata.alert" in json.loads(incident["rawJSON"])
    assert "sample_key1" in json.loads(incident["rawJSON"])["devo.metadata.alert"]
    assert "sample_key2" in json.loads(incident["rawJSON"])["devo.metadata.alert"]
    assert "alertPriority" in json.loads(incident["rawJSON"])
    assert "alertName" in json.loads(incident["rawJSON"])
    assert "alertDescription" in json.loads(incident["rawJSON"])
    assert mock_demisto_ISO.called


@patch("Devo_v2.demisto_ISO", return_value="2022-03-15T15:01:23.456Z")
def test_alert_to_incident_missing_data(mock_demisto_ISO):
    incident = alert_to_incident(ALERT_WITH_MISSING_DATA, USER_PREFIX)
    assert incident["name"] == "value"
    assert incident["severity"] == 3
    assert incident["details"] == "alert123"
    assert incident["description"] == "This is a sample alert"
    assert incident["occurred"] == "2022-03-15T15:01:23.456Z"
    assert incident["labels"] == EXPECTED_LABELS_WITH_NULL
    assert "devo.metadata.alert" in json.loads(incident["rawJSON"])
    assert "sample_key1" in json.loads(incident["rawJSON"])["devo.metadata.alert"]
    assert "sample_key2" in json.loads(incident["rawJSON"])["devo.metadata.alert"]
    assert "alertPriority" in json.loads(incident["rawJSON"])
    assert "alertName" in json.loads(incident["rawJSON"])
    assert "alertDescription" in json.loads(incident["rawJSON"])
    assert mock_demisto_ISO.called


@patch("Devo_v2.demisto.getLastRun")
@patch("Devo_v2.Client")
@patch("Devo_v2.demisto.setLastRun")
@patch("Devo_v2.demisto.incidents")
def test_fetch_incidents(
    mock_incidents: MagicMock,
    mock_setLastRun: MagicMock,
    mock_Reader: MagicMock,
    mock_getLastRun: MagicMock,
):
    mock_getLastRun.return_value = LAST_RUN_DATA

    mock_Reader.return_value.query.return_value = MOCK_EVENTS
    # Call the function
    fetch_incidents()
    # Check that setLastRun was called with the expected argument

    mock_setLastRun.assert_called_once_with(EXPECTED_LAST_RUN_DATA)
    mock_incidents.assert_called_once()


@patch("Devo_v2.FETCH_INCIDENTS_LIMIT", MOCK_FETCH_INCIDENTS_LIMIT_INCORRECT, create=True)
def fetch_incidents_limit_out_of_range():
    with pytest.raises(ValueError) as e:
        fetch_incidents()
    assert "Fetch incidents limit should be greater than or equal to 10 and smaller than or equal to 100" in str(e.value)


# Test case for converting current time to Unix timestamp
def test_to_unix_current_time():
    unix_timestamp = _to_unix("now")
    assert isinstance(unix_timestamp, int)


# Test case for converting datetime object to Unix timestamp


def test_to_unix_datetime_object():
    dt = datetime(2024, 3, 23, 12, 0, 0, tzinfo=UTC)  # Ensure timezone is UTC
    unix_timestamp = _to_unix(dt)
    assert unix_timestamp == 1711195200


# Test case for converting pandas.Timestamp object to Unix timestamp


def test_to_unix_pandas_timestamp():
    ts = pd.Timestamp("2024-03-23 12:00:00", tz="UTC")  # Ensure timezone is UTC
    unix_timestamp = _to_unix(ts)
    assert unix_timestamp == 1711195200


# Test case for converting string to Unix timestamp


def test_to_unix_string():
    unix_timestamp = _to_unix("2024-03-23 12:00:00")
    assert unix_timestamp == 1711195200


# Test case for converting integer timestamp to Unix timestamp


def test_to_unix_integer():
    unix_timestamp = _to_unix(1740604800)
    assert unix_timestamp == 1740604800


# Test case for converting float timestamp to Unix timestamp


def test_to_unix_float():
    unix_timestamp = _to_unix(1740604800.0)
    assert unix_timestamp == 1740604800


# Test case for converting None to Unix timestamp


def test_to_unix_none():
    unix_timestamp = _to_unix(None)
    assert unix_timestamp is None


# Test case for converting with milliseconds option set to True


def test_to_unix_milliseconds():
    unix_timestamp = _to_unix("2024-03-23 12:00:00", milliseconds=True)
    assert unix_timestamp == 1711195200000