Abnormal Security

Abnormal Security detects the whole spectrum of email attacks, from vendor email compromise and spear-phishing to unwanted email spam and graymail. To stop these advanced attacks, Abnormal leverages the industry’s most advanced behavioral data science to baseline known good behavior and detects anomalies.

Data Enrichment & Threat Intelligence · Abnormal Security

Details

IDAbnormal Security
ProviderAbnormal Security
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Abnormal Security detects the whole spectrum of email attacks, from vendor email compromise and spear-phishing to unwanted email spam and graymail. To stop these advanced attacks, Abnormal leverages the industry’s most advanced behavioral data science to baseline known good behavior and detects anomalies.
This integration was integrated and tested with version 1.3.0 of Abnormal Security

Configure Abnormal Security in Cortex

Parameter Description Required
Server URL (e.g. https://api.abnormalplatform.com/v1)   True
API Key   True
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents Retrieves incidents based on the customer’s selection from three categories: Threats, Account Takeover Cases, and Abuse Campaigns False
Maximum incidents to fetch. Maximum number of incidents per fetch. The default value is 200. False
Fetch Threats   False
Fetch Abuse Campaigns   False
Fetch Account Takeover Cases   False
First fetch time First alert created date to fetch. e.g., “1 min ago”,”2 weeks ago”,”3 months ago” False
Incident type   False
Incidents Fetch Interval   False
Polling Lag Time (in minutes) Time in minutes to subtract from polling time window for data consistency (Default : 2 mins) False
Maximum incidents pages to fetch Maximum number of pages to fetch for incidents False

Commands

You can execute these commands from the 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.

abnormal-security-check-case-action-status


Check the status of an action requested on a case.

Base Command

abnormal-security-check-case-action-status

Input

Argument Name Description Required
case_id A string representing the email case. Can be retrieved by first running command to list cases. Required
action_id A string representing the email case. Can be retrieved from payload after performing an action on a case. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.ActionStatus.status String Status of the case after an action is performed
AbnormalSecurity.ActionStatus.description String Detailed description of the status

Command Example

!abnormal-security-check-case-action-status case_id=12345 action_id=abcdefgh-1234-5678-ijkl-mnop9qrstuvwx

Context Example

{
    "AbnormalSecurity": {
        "ActionStatus": {
            "description": "The request was completed successfully",
            "status": "acknowledged"
        }
    }
}

Human Readable Output

Results

description status
The request was completed successfully acknowledged

abnormal-security-check-threat-action-status


Check the status of an action requested on a threat.

Base Command

abnormal-security-check-threat-action-status

Input

Argument Name Description Required
threat_id A UUID representing a threat campaign. Full list of threat IDs can be obtained by first running the command to list a threat. Required
action_id A UUID representing the action id for a threat. Can be obtained from payload after performing an action on the threat. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.ActionStatus.status String The status of a threat after performing an action on it
AbnormalSecurity.ActionStatus.description String The description of the status

Command Example

!abnormal-security-check-threat-action-status threat_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba action_id=abcdefgh-1234-5678-ijkl-mnop9qrstuvwx

Context Example

{
    "AbnormalSecurity": {
        "ActionStatus": {
            "description": "The request was completed successfully",
            "status": "acknowledged"
        }
    }
}

Human Readable Output

Results

description status
The request was completed successfully acknowledged

abnormal-security-download-threat-log-csv


Download data from Threat Log in .csv format

Base Command

abnormal-security-download-threat-log-csv

Input

Argument Name Description Required
filter Filter the results based on a filter key. Value must be of the format filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ. Supported keys - [receivedTime]. Optional
mock-data Returns test data if set to True. Optional
source Filters threats based on the source of detection. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

There is no context output for this command.

Command Example

!abnormal-security-download-threat-log-csv filter="receivedTime gte 2020-12-01T01:01:01Z"

Context Example

{
    "File": {
        "EntryID": "2294@2ef16ace-2149-42b9-8b0f-fb7620ba7d44",
        "Extension": "csv",
        "Info": "csv",
        "MD5": "a981545ee72fe115888800725883ca8a",
        "Name": "threat_log.csv",
        "SHA1": "c3cbae11542dc7244e3bf04a0901d7063597d381",
        "SHA256": "296463cad959803d64bfc94fbffa24e30c9438ba58827a100a9e7c219f26b382",
        "SHA512": "21a53f61c7d22b533abd7181b16116bf9017b7a444c10e4d2336803794ef0d9dded56e65179f924252f0bf3231e35fa1b726c8d7723f10b2f08bae0b3bedddd1",
        "SSDeep": "12:dB2XRzmZIm88Rvu8R7b7+I78RQC5+GUHwgfdvvq:dB2XRMrt/C5+GYw",
        "Size": 449,
        "Type": "ASCII text, with CRLF line terminators"
    }
}

abnormal-security-list-abuse-mailbox-campaigns


Get a list of campaigns submitted to Abuse Mailbox

Base Command

abnormal-security-list-abuse-mailbox-campaigns

Input

Argument Name Description Required
filter Value must be of the format filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ. A {FILTER KEY} must be specified, and currently only the key lastReportedTime is supported for /abusecampaigns. At least one of gte/lte must be specified, with a datetime string following the YYYY-MM-DDTHH:MM:SSZ format. Do note that provided filter time is in UTC. Optional
page_size Number of abuse campaigns shown on each page. Each page of data will have at most page_size abuse campaign IDs. Optional
page_number 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified. Optional
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.AbuseCampaign.campaigns.campaignId String An id which maps to an abuse campaign.
AbnormalSecurity.AbuseCampaign.pageNumber Number The current page number.
AbnormalSecurity.AbuseCampaign.nextPageNumber Number The next page number.

Command Example

!abnormal-security-list-abuse-mailbox-campaigns filter="lastReportedTime gte 2020-12-01T01:01:01Z"

Context Example

{
    "AbnormalSecurity": {
        "AbuseCampaign": {
            "campaigns": [
                {
                    "campaignId": "fff51768-c446-34e1-97a8-9802c29c3ebd"
                },
                {
                    "campaignId": "07434ea5-df7b-3ff4-8d07-4a82df0c655d"
                }
            ],
            "pageNumber": 1
        }
    }
}

Human Readable Output

List of Abuse Mailbox Campaigns

Campaign IDs

campaignId
fff51768-c446-34e1-97a8-9802c29c3ebd
07434ea5-df7b-3ff4-8d07-4a82df0c655d

abnormal-security-list-abnormal-cases


Get a list of Abnormal cases identified by Abnormal Security

Base Command

abnormal-security-list-abnormal-cases

Input

Argument Name Description Required
filter Value must be of the format filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ. A {FILTER KEY} must be specified, and currently the only key that is supported for /cases is lastModifiedTime. At least 1 of gte/lte must be specified, with a datetime string following the YYYY-MM-DDTHH:MM:SSZ format. Optional
page_size Number of cases that are on each page. Each page of data will have at most page_size threats. Has no effect if filter is not specified. Optional
page_number 1-indexed page number to get a particular page of cases. Has no effect if filter is not specified. Optional
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.inline_response_200_1.cases.caseId String A unique identifier for this case.
AbnormalSecurity.inline_response_200_1.cases.description String Description of the severity level for this case.
AbnormalSecurity.inline_response_200_1.pageNumber Number The current page number. Will not be be in the response if no filter query meter is passed in via the request.
AbnormalSecurity.inline_response_200_1.nextpageNumber Number The next page number. Will not be included in the response if there are no more pages of data or if no filter query meter is passed in via the request

Command Example

!abnormal-security-list-abnormal-cases filter="lastModifiedTime gte 2020-12-01T01:01:01Z"

Context Example

{
    "AbnormalSecurity": {
        "inline_response_200_1": {
            "cases": [
                {
                    "caseId": 1234,
                    "description": "Potential Account Takeover"
                }
            ],
            "nextPageNumber": 2,
            "pageNumber": 1
        }
    }
}

Human Readable Output

List of Cases

Case IDs

caseId description
1234 Potential Account Takeover

abnormal-security-list-threats


Get a list of threats

Base Command

abnormal-security-list-threats

Input

Argument Name Description Required
filter Value must be of the format filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ. A {FILTER KEY} must be specified, and currently the only key that is supported for /threats is receivedTime. At least 1 of gte/lte must be specified, with a datetime string following the YYYY-MM-DDTHH:MM:SSZ format. Optional
page_size Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified. Optional
page_number 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified. Optional
mock-data Returns test data if set to True. Optional
source Filters threats based on the source of detection. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.inline_response_200.threats.threatId String An id which maps to a threat campaign. A threat campaign might be received by multiple users.
AbnormalSecurity.inline_response_200.pageNumber Number The current page number. Will not be be in the response if no filter query meter is passed in via the request.
AbnormalSecurity.inline_response_200.nextpageNumber Number The next page number. Will not be included in the response if there are no more pages of data or if no filter query meter is passed in via the request

Command Example

!abnormal-security-list-threats filter="receivedTime gte 2020-12-01T01:01:01Z"

Context Example

{
    "AbnormalSecurity": {
        "inline_response_200": {
            "nextPageNumber": 2,
            "pageNumber": 1,
            "threats": [
                {
                    "threatId": "184712ab-6d8b-47b3-89d3-a314efef79e2"
                }
            ]
        }
    }
}

Human Readable Output

List of Threats

Threat IDs

threatId
184712ab-6d8b-47b3-89d3-a314efef79e2

abnormal-security-get-threat


Get details of a threat

Base Command

abnormal-security-get-threat

Input

Argument Name Description Required
threat_id A UUID representing a threat campaign. Full list of threat IDs can be obtained by first running the command to list a threat. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional
page_size Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified. Optional
page_number 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified. Optional

Context Output

Path Type Description
AbnormalSecurity.ThreatDetails.threatId String An id which maps to a threat campaign.
AbnormalSecurity.ThreatDetails.messages.abxMessageId Number A unique identifier for an individual message within a threat (i.e email campaign).
AbnormalSecurity.ThreatDetails.messages.abxPortalUrl String The URL at which the specific message details are viewable.
AbnormalSecurity.ThreatDetails.messages.attachmentCount Number The number of attachments in the email.
AbnormalSecurity.ThreatDetails.messages.attachmentNames Array List of the names of attachments in the email.
AbnormalSecurity.ThreatDetails.messages.attackStrategy String The attack strategy used in the threat.
AbnormalSecurity.ThreatDetails.messages.attackType String The type of threat the message represents.
AbnormalSecurity.ThreatDetails.messages.attackVector String The medium used for the attack.
AbnormalSecurity.ThreatDetails.messages.attackedParty String The party that was targeted by the attack.
AbnormalSecurity.ThreatDetails.messages.autoRemediated Boolean Whether the threat was automatically remediated.
AbnormalSecurity.ThreatDetails.messages.fromAddress String The email address of the sender.
AbnormalSecurity.ThreatDetails.messages.fromName String The display name of the sender.
AbnormalSecurity.ThreatDetails.messages.impersonatedParty String The party, if any, that was impersonated in the attack.
AbnormalSecurity.ThreatDetails.messages.internetMessageId String The Internet Message ID, per RFC 822.
AbnormalSecurity.ThreatDetails.messages.isRead Boolean Whether the email has been read.
AbnormalSecurity.ThreatDetails.messages.postRemediated Boolean Whether the threat was remediated after landing in the user’s mailbox.
AbnormalSecurity.ThreatDetails.messages.receivedTime String The timestamp at which this message arrived.
AbnormalSecurity.ThreatDetails.messages.recipientAddress String The email address of the user who actually received the message.
AbnormalSecurity.ThreatDetails.messages.remediationStatus String The status of remediation action.
AbnormalSecurity.ThreatDetails.messages.remediationTimestamp String The timestamp at which the message was remediated.
AbnormalSecurity.ThreatDetails.messages.sentTime String The timestamp at which this message was sent.
AbnormalSecurity.ThreatDetails.messages.subject String The subject of the email.
AbnormalSecurity.ThreatDetails.messages.threatId String An id which maps to a threat campaign.
AbnormalSecurity.ThreatDetails.messages.toAddresses Array All the email addresses to which the message was sent.
AbnormalSecurity.ThreatDetails.messages.ccEmails Array All the email addresses in CC.
AbnormalSecurity.ThreatDetails.messages.replyToEmails Array All the email addresses in the “Reply To” field.
AbnormalSecurity.ThreatDetails.messages.returnPath String The path where information is returned to the attacker.
AbnormalSecurity.ThreatDetails.messages.senderDomain String The domain of the sender.
AbnormalSecurity.ThreatDetails.messages.senderIpAddress String The IP address of the sender.
AbnormalSecurity.ThreatDetails.messages.summaryInsights Array Summary insights into the threat’s characteristics.
AbnormalSecurity.ThreatDetails.messages.urlCount Number The number of URLs contained in the email.
AbnormalSecurity.ThreatDetails.messages.urls Array List of all URLs contained in the email.

Command Example

!abnormal-security-get-threat threat_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba

Context Example

{
    "AbnormalSecurity": {
        "ThreatDetails": {
            "messages": [
                {
                    "abxMessageId": 4551618356913732000,
                    "abxPortalUrl": "https://portal.abnormalsecurity.com/home/threat-center/remediation-history/4551618356913732076",
                    "attachmentCount": null,
                    "attachmentNames": ["attachment.pdf"],
                    "attackStrategy": "Name Impersonation",
                    "attackType": "Extortion",
                    "attackVector": "Text",
                    "attackedParty": "VIP",
                    "autoRemediated": true,
                    "ccEmails": ["cc@example.com"],
                    "fromAddress": "support@secure-reply.org",
                    "fromName": "",
                    "impersonatedParty": "None / Others",
                    "internetMessageId": "<5edfca1c.1c69fb81.4b055.8fd5@mx.google.com>",
                    "isRead": true,
                    "postRemediated": true,
                    "receivedTime": "2020-06-09T17:42:59Z",
                    "recipientAddress": "example@example.com",
                    "remediationTimestamp": "2020-06-09T17:42:59Z",
                    "replyToEmails": ["reply-to@example.com"],
                    "returnPath": "support@secure-reply.org",
                    "senderDomain": "",
                    "senderIpAddress": "100.101.102.103",
                    "sentTime": "2020-06-09T17:42:59Z",
                    "subject": "Phishing Email",
                    "summaryInsights": ["Bitcoin Topics", "Personal Information Theft", "Unusual Sender"],
                    "threatId": "184712ab-6d8b-47b3-89d3-a314efef79e2",
                    "toAddresses": "example@example.com, another@example.com",
                    "urlCount": 0,
                    "urls": ["https://www.google.com/"]
                }
            ],
            "threatId": "184712ab-6d8b-47b3-89d3-a314efef79e2"
        }
    }
}

Human Readable Output

Messages in Threat 184712ab-6d8b-47b3-89d3-a314efef79e2

subject fromAddress toAddresses recipientAddress receivedTime attackType attackStrategy returnPath
Phishing Email support@secure-reply.org example@example.com, another@example.com example@example.com 2020-06-09T17:42:59Z Extortion Name Impersonation support@secure-reply.org etc

abnormal-security-get-abnormal-case


Get details of an Abnormal case

Base Command

abnormal-security-get-abnormal-case

Input

Argument Name Description Required
case_id A string representing the email case. Can be retrieved by first running command to list cases. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.AbnormalCaseDetails.caseId String A unique identifier for this case.
AbnormalSecurity.AbnormalCaseDetails.severity String Description of the severity level for this case.
AbnormalSecurity.AbnormalCaseDetails.affectedEmployee String Which employee this case pertains to.
AbnormalSecurity.AbnormalCaseDetails.firstObserved String First time suspicious behavior was observed.
AbnormalSecurity.AbnormalCaseDetails.genai_summary String Gen AI summary for this case

Command Example

!abnormal-security-get-abnormal-case case_id=12805

Context Example

{
    "AbnormalSecurity": {
        "AbnormalCaseDetails": {
            "affectedEmployee": "FirstName LastName",
            "analysis": "Mail Sent",
            "caseId": 1234,
            "case_status": "Action Required",
            "firstObserved": "2020-06-09T17:42:59Z",
            "remediation_status": "Not remediated",
            "severity": "Potential Account Takeover",
            "threatIds": ["184712ab-6d8b-47b3-89d3-a314efef79e2"],
            "genai_summary": "Observed 2 lateral phishing emails sent internally from the user's account"
        }
    }
}

Human Readable Output

Details of Case 1234

caseId severity affectedEmployee firstObserved threatIds
1234 Potential Account Takeover FirstName LastName 2020-06-09T17:42:59Z 184712ab-6d8b-47b3-89d3-a314efef79e2

abnormal-security-get-abuse-mailbox-campaign


Get details of an Abuse Mailbox campaign

Base Command

abnormal-security-get-abuse-mailbox-campaign

Input

Argument Name Description Required
campaign_id A UUID representing the abuse campaign id. Can be Can be retrieved by first running command to list abuse mailbox campaigns. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.AbuseCampaign.campaignId String An id which maps to an abuse campaign.
AbnormalSecurity.AbuseCampaign.firstReported String Date abuse campaign was first reported.
AbnormalSecurity.AbuseCampaign.lastReported String Date abuse campaign was last reported.
AbnormalSecurity.AbuseCampaign.messageId String A unique identifier for the first message in the abuse campaign.
AbnormalSecurity.AbuseCampaign.subject String Subject of the first email in the abuse campaign.
AbnormalSecurity.AbuseCampaign.fromName String The display name of the sender.
AbnormalSecurity.AbuseCampaign.fromAddress String The email address of the sender.
AbnormalSecurity.AbuseCampaign.recipientName String The email address of the recipient.
AbnormalSecurity.AbuseCampaign.recipientAddress String The email address of the recipient.
AbnormalSecurity.AbuseCampaign.judgementStatus String Judgement status of message.
AbnormalSecurity.AbuseCampaign.overallStatus String Overall status of message.
AbnormalSecurity.AbuseCampaign.attackType String The type of threat the message represents.

Command Example

!abnormal-security-get-abuse-mailbox-campaign campaign_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba

Context Example

{
    "AbnormalSecurity": {
        "AbuseCampaign": {
            "campaigns": {
                "attackType": "Attack Type: Spam",
                "campaignId": "fff51768-c446-34e1-97a8-9802c29c3ebd",
                "firstReported": "2020-11-11T13:11:40-08:00",
                "fromAddress": "example@example.com",
                "fromName": "Tom Dinkley",
                "judgementStatus": "Malicious",
                "lastReported": "2020-11-11T13:11:40-08:00",
                "messageId": "12345678910",
                "overallStatus": "Move attempted",
                "recipientAddress": "example_phisher@example.com",
                "recipientName": "Booker",
                "subject": "Fwd: This is spam"
            }
        }
    }
}

Human Readable Output

Results

attackType campaignId firstReported fromAddress fromName judgementStatus lastReported messageId overallStatus recipientAddress recipientName subject
Attack Type: Spam fff51768-c446-34e1-97a8-9802c29c3ebd 2020-11-11T13:11:40-08:00 example@example.com Tom Dinkley Malicious 2020-11-11T13:11:40-08:00 12345678910 Move attempted example_phisher@example.com Booker Fwd: This is spam

abnormal-security-get-employee-identity-analysis


Get employee identity analysis (Genome) data

Base Command

abnormal-security-get-employee-identity-analysis

Input

Argument Name Description Required
email_address Email address of the employee you want to retrieve data for. Required
mock-data Returns test data if set to True. Optional

Context Output

Path Type Description
AbnormalSecurity.Employee.email String Employee email
AbnormalSecurity.Employee.histograms.key String Genome key name
AbnormalSecurity.Employee.histograms.name String Genome title
AbnormalSecurity.Employee.histograms.description String Description of genome object
AbnormalSecurity.Employee.histograms.values.value String Category value
AbnormalSecurity.Employee.histograms.values.percentage Number Ratio of this category relative to others
AbnormalSecurity.Employee.histograms.values.total_count Number Number of occurences for this category

Command Example

!abnormal-security-get-employee-identity-analysis email_address="test@test.com"

Context Example

{
    "AbnormalSecurity": {
        "Employee": {
            "email": "test@test.com",
            "histograms": [
                {
                    "description": "Common IP addresses for user logins",
                    "key": "ip_address",
                    "name": "Common IP Addresses",
                    "values": [
                        {
                            "ratio": 0.25,
                            "raw_count": 12,
                            "text": "ip-address-0"
                        },
                        {
                            "ratio": 0.25,
                            "raw_count": 12,
                            "text": "ip-address-1"
                        },
                        {
                            "ratio": 0.25,
                            "raw_count": 12,
                            "text": "ip-address-2"
                        },
                        {
                            "ratio": 0.25,
                            "raw_count": 12,
                            "text": "ip-address-3"
                        }
                    ]
                }
            ]
        }
    }
}

Human Readable Output

Analysis of test@test.com

description key name values
Common IP addresses for user logins ip_address Common IP Addresses {‘text’: ‘ip-address-0’, ‘ratio’: 0.25, ‘raw_count’: 12},
{‘text’: ‘ip-address-1’, ‘ratio’: 0.25, ‘raw_count’: 12},
{‘text’: ‘ip-address-2’, ‘ratio’: 0.25, ‘raw_count’: 12},
{‘text’: ‘ip-address-3’, ‘ratio’: 0.25, ‘raw_count’: 12}

abnormal-security-get-employee-information


Get employee information

Base Command

abnormal-security-get-employee-information

Input

Argument Name Description Required
email_address Email address of the employee you want to retrieve data for. Required
mock-data Returns test data if set to True. Optional

Context Output

Path Type Description
AbnormalSecurity.Employee.name String Name of the employee.
AbnormalSecurity.Employee.email String Email of the employee.
AbnormalSecurity.Employee.title String Job title of the employee.
AbnormalSecurity.Employee.manager String Email address of the employee’s manager

Command Example

!abnormal-security-get-employee-information email_address="test@test.com"

Context Example

{
    "AbnormalSecurity": {
        "Employee": {
            "email": "testemail@email.com",
            "manager": "testmanageremail@email.net",
            "name": "test_name",
            "title": "Test Operator"
        }
    }
}

Human Readable Output

Results

email manager name title
testemail@email.com testmanageremail@email.net test_name Test Operator

abnormal-security-get-employee-last-30-days-login-csv


Get employee login information for last 30 days in csv format

Base Command

abnormal-security-get-employee-last-30-days-login-csv

Input

Argument Name Description Required
email_address Email address of the employee you want to retrieve data for. Required
mock-data Returns test data if set to True. Optional

Context Output

There is no context output for this command.

Command Example

!abnormal-security-get-employee-last-30-days-login-csv email_address="test@test.com"

Context Example

{
    "File": {
        "EntryID": "2338@2ef16ace-2149-42b9-8b0f-fb7620ba7d44",
        "Extension": "csv",
        "Info": "csv",
        "MD5": "11afb4879c5026e25bd868dfcf23e811",
        "Name": "employee_login_info_30_days.csv",
        "SHA1": "345ea1d24b52c96baf6b0e4d892d13d4efcf666d",
        "SHA256": "12620e0f576f4d74603b1f542919a3e5199e61435ffd99bcd68c26e02ed9c693",
        "SHA512": "f0e788981ce70d9668100ae3f93d1f28660f0d8a9dfda02284a70f08ac14ca5a356872284f460d8fb7970791e314e0db4a6c84b0032c35046efce62368a00da5",
        "SSDeep": "12:uR2xCC56aHoW2IY3zg05Eg05ng05Eg05V:uROjHn2IY3v5i5T5i5V",
        "Size": 484,
        "Type": "ASCII text, with CRLF line terminators"
    }
}

abnormal-security-get-latest-threat-intel-feed


DEPRECATED. Get the latest threat intel feed.

Base Command

abnormal-security-get-latest-threat-intel-feed

Input

Argument Name Description Required
mock-data Returns test data if set to True. Optional

Context Output

There is no context output for this command.

Command Example

!abnormal-security-get-latest-threat-intel-feed

Context Example

{
    "File": {
        "EntryID": "2314@2ef16ace-2149-42b9-8b0f-fb7620ba7d44",
        "Extension": "json",
        "Info": "application/json",
        "MD5": "a00e919efc9e28f77b8f7b7523b1ffe8",
        "Name": "threat_intel_feed.json",
        "SHA1": "53bf3e6075f407b53c95d5dd2197b9be0dfa5ced",
        "SHA256": "f842e7f6795fba081f2046617fce662c050b5a3c64cac9501f23fa7576788429",
        "SHA512": "27af66eefb1ed7227b4f8ec1c663ac8ef47660bb34ffd1d5853a7a58e25caec68615c2e66bfbd66577749faa889894e792f53cab70a826818b4d627ad02bbb04",
        "SSDeep": "49152:dY0GiMq58ZVhOH+sZwFp+h/s0pH6VRRxIGFe7V3dCLtJ/W7H8nsIdL0E:u",
        "Size": 8007799,
        "Type": "ASCII text"
    }
}

abnormal-security-manage-threat


Manage a Threat identified by Abnormal Security

Base Command

abnormal-security-manage-threat

Input

Argument Name Description Required
threat_id A UUID representing a threat campaign. Full list of threat IDs can be obtained by first running the command to list a threat. Required
action Action to perform on threat. Required
mock-data Returns test data if set to True. Optional

Context Output

Path Type Description
AbnormalSecurity.ThreatManageResults.action_id String ID of the action taken
AbnormalSecurity.ThreatManageResults.status_url String URL of the status of the action

Command Example

!abnormal-security-manage-threat threat_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba action=remediate

Context Example

{
    "AbnormalSecurity": {
        "ThreatManageResults": {
            "action_id": "a33a212a-89ff-461f-be34-ea52aff44a73",
            "status_url": "https://api.abnormalplatform.com/v1/threats/184712ab-6d8b-47b3-89d3-a314efef79e2/actions/a33a212a-89ff-461f-be34-ea52aff44a73"
        }
    }
}

Human Readable Output

Results

action_id status_url
a33a212a-89ff-461f-be34-ea52aff44a73 https://api.abnormalplatform.com/v1/threats/184712ab-6d8b-47b3-89d3-a314efef79e2/actions/a33a212a-89ff-461f-be34-ea52aff44a73

abnormal-security-manage-abnormal-case


Manage an Abnormal Case.

Base Command

abnormal-security-manage-abnormal-case

Input

Argument Name Description Required
case_id A string representing the email case. Can be retrieved by first running command to list cases. Required
action Action to perform on case. Required
mock-data Returns test data if set to True. Optional

Context Output

Path Type Description
AbnormalSecurity.CaseManageResults.action_id String ID of the action taken
AbnormalSecurity.CaseManageResults.status_url String URL of the status of the action

Command Example

!abnormal-security-manage-abnormal-case case_id=12805 action=action_required

Context Example

{
    "AbnormalSecurity": {
        "CaseManageResults": {
            "action_id": "61e76395-40d3-4d78-b6a8-8b17634d0f5b",
            "status_url": "https://api.abnormalplatform.com/v1/cases/1234/actions/61e76395-40d3-4d78-b6a8-8b17634d0f5b"
        }
    }
}

Human Readable Output

Results

action_id status_url
61e76395-40d3-4d78-b6a8-8b17634d0f5b https://api.abnormalplatform.com/v1/cases/1234/actions/61e76395-40d3-4d78-b6a8-8b17634d0f5b

abnormal-security-get-case-analysis-and-timeline


Provides the analysis and timeline details of a case

Base Command

abnormal-security-get-case-analysis-and-timeline

Input

Argument Name Description Required
case_id A string representing the email case. Can be retrieved by first running command to list cases. Required
mock-data Returns test data if set to True. Optional
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.CaseAnalysis.insights.signal String Insight signal or highlight of a case
AbnormalSecurity.CaseAnalysis.insights.description String Description of insight signal or highlight
AbnormalSecurity.CaseAnalysis.eventTimeline.event_timestamp String Time when event occurred
AbnormalSecurity.CaseAnalysis.eventTimeline.category String Type of event
AbnormalSecurity.CaseAnalysis.eventTimeline.title String Title of the event
AbnormalSecurity.CaseAnalysis.eventTimeline.ip_address String IP Address where user accessed mail from
AbnormalSecurity.CaseAnalysis.eventTimeline.field_labels Unknown Analysis labels associated with the fields in the timeline event

Command Example

!abnormal-security-get-case-analysis-and-timeline case_id=12345

Context Example

{
    "AbnormalSecurity": {
        "CaseAnalysis": {
            "eventTimeline": [
                {
                    "category": "Risk Event",
                    "description": "Impossible Travel Event was observed for test@lamronba.com.",
                    "event_timestamp": "2021-07-14T22:41:54Z",
                    "ip_address": "127.0.0.1",
                    "location": {
                        "city": "Aldie",
                        "country": "US",
                        "state": "Virginia"
                    },
                    "prev_location": {
                        "city": "Atherton",
                        "country": "US",
                        "state": "California"
                    },
                    "title": "Impossible Travel"
                },
                {
                    "category": "Mail Rule",
                    "condition": "hasNoCondition",
                    "event_timestamp": "2021-07-14T22:41:54Z",
                    "flagging_detectors": "DELETE_ALL",
                    "rule_name": "Swag Voice Note",
                    "title": "Mail Rule Change"
                },
                {
                    "category": "Mail Sent",
                    "event_timestamp": "2021-07-14T22:41:54Z",
                    "recipient": "Recipient Name",
                    "sender": "test@lamronba.com",
                    "subject": "Spoof email subject",
                    "title": "Unusual Correspondence"
                },
                {
                    "application": "Microsoft Office 365 Portal",
                    "browser": "Chrome 79.0.3453",
                    "category": "Sign In",
                    "description": "Suspicious Failed Sign In Attempt for test@lamronba.com",
                    "device_trust_type": "None",
                    "event_timestamp": "2021-07-14T22:41:54Z",
                    "field_labels": {
                        "ip_address": ["rare", "proxy"],
                        "operating_system": ["legacy"]
                    },
                    "ip_address": "127.0.0.1",
                    "isp": "NGCOM",
                    "location": {
                        "country": "Ireland"
                    },
                    "operating_system": "Windows XP",
                    "protocol": "Browser",
                    "title": "Suspicious Failed Sign In Attempt"
                }
            ],
            "insights": [
                {
                    "description": "There was a signin into test@lamronba.com from a location frequently used to launch attacks.",
                    "signal": "Risky Location"
                }
            ]
        }
    }
}

Human Readable Output

Insights for 12345

signal description
Risky Location There was a signin into test@lamronba.com from a location frequently used to launch attacks.

Event Timeline for

event_timestamp category title field_labels ip_address description location sender subject title rule_name
2021-07-14T22:41:54Z Risk Event Impossible Travel   127.0.0.1 Impossible Travel Event was observed for test@lamronba.com. city: Aldie
state: Virginia
country: US
    Impossible Travel  
2021-07-14T22:41:54Z Mail Rule Mail Rule Change             Mail Rule Change Swag Voice Note
2021-07-14T22:41:54Z Mail Sent Unusual Correspondence         test@lamronba.com Spoof email subject Unusual Correspondence  
2021-07-14T22:41:54Z Sign In Suspicious Failed Sign In Attempt ip_address: rare,
proxy
operating_system: legacy
127.0.0.1 Suspicious Failed Sign In Attempt for test@lamronba.com country: Ireland     Suspicious Failed Sign In Attempt  

[Deprecated] abnormal-security-submit-inquiry-to-request-a-report-on-misjudgement


Submit an Inquiry to request a report on misjudgement by Abnormal Security

Base Command

abnormal-security-submit-inquiry-to-request-a-report-on-misjudgement

Input

Argument Name Description Required
mock-data Returns test data if set to True. Optional
reporter Email of the reporter. Required
report_type Type of misjudgement reported. Required

Context Output

Path Type Description
AbnormalSecurity.SubmitInquiry.detail String Confirmation of inquiry sent

Command Example

!abnormal-security-submit-inquiry-to-request-a-report-on-misjudgement reporter=abc@def.com report_type=false-positive

Context Example

{
    "AbnormalSecurity": {
        "SubmitInquiry": {
            "detail": "Thank you for your feedback! We have sent your inquiry to our support staff."
        }
    }
}

Human Readable Output

Results

detail
Thank you for your feedback! We have sent your inquiry to our support staff.

abnormal-security-submit-false-negative-report


Submit a False Negative Report

Base Command

abnormal-security-submit-false-negative-report

Input

Argument Name Description Required
sender_email Email address of the sender. Required
recipient_email Email address of the recipient. Required
subject Email subject. Required

Command Example

!abnormal-security-submit-false-negative-report recipient_email=abc@def.com sender_email=def@def.com subject=hello

Human Readable Output

Results

detail
Thank you for your feedback! We have sent your inquiry to our support staff.

abnormal-security-submit-false-positive-report


Submit a False Positive Report

Base Command

abnormal-security-submit-false-positive-report

Input

Argument Name Description Required
portal_link URL link of threat log in abnormal security portal Required

Command Example

!abnormal-security-submit-false-positive-report portal_link=https://portal.abnormalsecurity.com/home/threat-center/remediation-history/123455667

Human Readable Output

Results

detail
Thank you for your feedback! We have sent your inquiry to our support staff.

abnormal-security-list-vendors


Get a list of vendors

Base Command

abnormal-security-list-vendors

Input

Argument Name Description Required
page_size Number of vendors that are on each page. Each page of data will have at most page_size vendors. Has no effect if filter is not specified. Optional
page_number 1-indexed page number to get a particular page of vendors. Has no effect if filter is not specified. Optional

Context Output

Path Type Description
AbnormalSecurity.VendorsList Unknown List of vendors.
AbnormalSecurity.VendorsList.vendorDomain String The domain of the vendor.

Command Example

!abnormal-security-list-vendors

Context Example

{
    "AbnormalSecurity": {
        "VendorsList": [
            {
                "vendorDomain": "test-domain-1.com"
            },
            {
                "vendorDomain": "test-domain-2.com"
            },
            {
                "vendorDomain": "test-domain-2.com"
            }
        ]
    }
}

Human Readable Output

List of Vendors

Vendor Domains

vendorDomain
test-domain-1.com

abnormal-security-get-vendor-details


Get details of a vendor

Base Command

abnormal-security-get-vendor-details

Input

Argument Name Description Required
vendor_domain The domain name of the vendor in question. It should be formatted as a fully qualified domain name (e.g., example.com). Required

Context Output

Path Type Description
AbnormalSecurity.VendorDetails.vendorDomain String The domain name of the vendor
AbnormalSecurity.VendorDetails.riskLevel String The risk level associated with the vendor.
AbnormalSecurity.VendorDetails.vendorContacts Unknown List of contacts related to the vendor.
AbnormalSecurity.VendorDetails.companyContacts Unknown List of contacts related to the company.
AbnormalSecurity.VendorDetails.vendorCountries Unknown List of countries associated with the vendor.
AbnormalSecurity.VendorDetails.analysis Unknown List of analyses associated with the vendor.
AbnormalSecurity.VendorDetails.vendorIpAddresses Unknown List of IP addresses associated with the vendor.

Command Example

!abnormal-security-get-vendor-details vendor_domain="test-domain-1.com"

Context Example

{
    "AbnormalSecurity": {
        "VendorDetails": {
            "vendorDomain": "test-domain-1.com",
            "riskLevel": "High",
            "vendorContacts": ["john.doe@test-domain-1.com"],
            "companyContacts": ["john.doe@test-domain-2.com", "jane.doe@test-domain-2.com"],
            "vendorCountries": ["USA"],
            "analysis": ["Vendor Compromise Seen in Abnormal Community"],
            "vendorIpAddresses": ["192.158. 1.38"]
        }
    }
}

Human Readable Output

Vendor Domain Details

vendorDomain riskLevel vendorContacts companyContacts vendorCountries analysis vendorIpAddresses
test-domain-1.com High john.doe@test-domain-1.com john.doe@test-domain-2.com, jane.doe@test-domain-2.com USA Vendor Compromise Seen in Abnormal Community 192.158.1.38

abnormal-security-get-vendor-activity


Get details of a vendor

Base Command

abnormal-security-get-vendor-activity

Input

Argument Name Description Required
vendor_domain The domain name of the vendor in question. It should be formatted as a fully qualified domain name (e.g., example.com). Required

Context Output

Path Type Description
AbnormalSecurity.VendorActivity.eventTimeline Unknown Event timeline for the vendor.
AbnormalSecurity.VendorActivity.eventTimeline.eventTimestamp String Timestamp of the event in the vendor’s activity timeline.
AbnormalSecurity.VendorActivity.eventTimeline.eventType String Type of event in the vendor’s activity timeline.
AbnormalSecurity.VendorActivity.eventTimeline.suspiciousDomain String Suspicious domain involved in the event.
AbnormalSecurity.VendorActivity.eventTimeline.domainIp String IP address of the suspicious domain.
AbnormalSecurity.VendorActivity.eventTimeline.ipGeolocation String Geolocation of the IP address.
AbnormalSecurity.VendorActivity.eventTimeline.attackGoal String The goal of the attack.
AbnormalSecurity.VendorActivity.eventTimeline.actionTaken String Action taken in response to the event.
AbnormalSecurity.VendorActivity.eventTimeline.hasEngagement Boolean Indicates whether the event involved any form of engagement.
AbnormalSecurity.VendorActivity.eventTimeline.recipient Unknown The recipient targeted by the event, if applicable.
AbnormalSecurity.VendorActivity.eventTimeline.threatId String Unique identifier for the threat.

Command Example

!abnormal-security-get-vendor-activity vendor_domain="test-domain-1.com"

Context Example

{
    "AbnormalSecurity": {
        "VendorActivity": {
            "eventTimeline": [
                {
                    "eventTimestamp": "2023-07-28T16:20:05Z",
                    "eventType": "Federated Signal",
                    "suspiciousDomain": "test@test-domain.com",
                    "domainIp": "192.158.1.38",
                    "ipGeolocation": null,
                    "attackGoal": "Spam",
                    "actionTaken": "Remediation Triggered",
                    "hasEngagement": false,
                    "recipient": "jane@doe.com",
                    "threatId": "184712ab-6d8b-47b3-89d3-a314efef79e2"
                }
            ]
        }
    }
}

Human Readable Output

Vendor Activity

eventTimestamp eventType suspiciousDomain domainIp ipGeolocation attackGoal actionTaken hasEngagement recipient threatId
2023-07-28T16:20:05Z Federated Signal Signal test@test-domain.com 192.158.1.38 null Spam Remediation Triggered false jane@doe.com 184712ab-6d8b-47b3-89d3-a314efef79e2

abnormal-security-list-vendor-cases


Get a list of vendor cases

Base Command

!abnormal-security-list-vendor-cases

Input

Argument Name Description Required
filter Value must be of the format filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ. A {FILTER KEY} must be specified, and currently the only keys that are supported are firstObservedTime and lastModifiedTime. At least 1 of gte/lte must be specified, with a datetime string following the YYYY-MM-DDTHH:MM:SSZ format. Optional
page_size Number of cases that are on each page. Optional
page_number 1-indexed page number to get a particular page of cases. Optional

Context Output

Path Type Description
AbnormalSecurity.VendorCases Unknown List of vendor cases.
AbnormalSecurity.VendorCases.vendorCaseId Number The identifier of the vendor case.

Command Example

!abnormal-security-list-vendor-cases filter="lastModifiedTime gte 2020-12-01T01:01:01Z"

Context Example

{
    "AbnormalSecurity": {
        "VendorCases": [
            {
                "vendorCaseId": 123
            },
            {
                "vendorCaseId": 456
            },
            {
                "vendorCaseId": 789
            }
        ]
    }
}

Human Readable Output

List of Cases

Vendor Case IDs

vendorCaseId
123

abnormal-security-get-vendor-case-details


Get details of a vendor case

Base Command

!abnormal-security-get-vendor-case-details

Input

Argument Name Description Required
case_id A string representing the email case. Can be retrieved by first running command to list cases. Required
subtenant Subtenant of the user (if applicable). Optional

Context Output

Path Type Description
AbnormalSecurity.VendorCaseDetails.vendorCaseId String The identifier of the vendor case.
AbnormalSecurity.VendorCaseDetails.vendorDomain String The vendor domain associated with the case.
AbnormalSecurity.VendorCaseDetails.firstObservedTime String The time the vendor case was first observed.
AbnormalSecurity.VendorCaseDetails.lastModifiedTime String The last time the vendor case was modified.
AbnormalSecurity.VendorCaseDetails.insights Unknown List of insights related to the vendor case.
AbnormalSecurity.VendorCaseDetails.timeline Unknown Timeline of events related to the vendor case.

Command Example

!abnormal-security-get-vendor-case-details case_id=123

Context Example

{
    "AbnormalSecurity": {
        "AbnormalCaseDetails": {
            "vendorCaseId": 123,
            "vendorDomain": "some-domain.com",
            "firstObservedTime": "2022-04-04T21:12:14Z",
            "lastModifiedTime": "2022-04-05T14:40:11Z",
            "insights": [
                {
                    "highlight": "Inconsistent Sender Domain Registrars",
                    "description": "The suspicious sending domain, \"some-domain.com\", was registered in \"City, United States\" to \"unknown\" on 2022-02-07 with registrar \"ABCD\". The legitimate domain for \"some-domain.com\", was registered through \"Test, LLC\" in \"City, United States\" on 1999-12-02."
                },
                {
                    "highlight": "Look-a-like Sender Domain",
                    "description": "The sending domain of this message, \"some-domain.com\", is attempting to impersonate the legitimate domain of \"some-domain.com\"."
                },
                {
                    "highlight": "Young Sender Domain",
                    "description": "The sender domain \"some-domain.com\" was 65 days old when the first engagement in this case was observed, a suspicious signal for a financial email conversation."
                }
            ],
            "timeline": [
                {
                    "eventTimestamp": "2022-04-04T21:12:14Z",
                    "senderAddress": "john-doe@some-domain.com",
                    "recipientAddress": "jane.doe@some-other-domain.com",
                    "subject": "Important Notice",
                    "markedAs": "Malicious",
                    "threatId": 1234
                },
                {
                    "eventTimestamp": "2022-04-04T21:12:14Z",
                    "senderAddress": "jand-doe@some-domain.com",
                    "recipientAddress": "john@some-other-domain.com",
                    "subject": "Important Notice",
                    "markedAs": "Malicious",
                    "threatId": 12345
                }
            ]
        }
    }
}

Human Readable Output

Case Details

vendorCaseId vendorDomain firstObservedTime lastModifiedTime insights timeline
123 some-domain.com 2022-04-04T21:12:14Z 2022-04-05T14:40:11Z {“highlight”: “Inconsistent Sender Domain Registrars”,”description”: “The suspicious sending domain, "some-domain.com", was registered in "City, United States" to "unknown" on 2022-02-07 with registrar "ABCD". The legitimate domain for "some-domain.com", was registered through "Test, LLC" in "City, United States" on 1999-12-02.”}… {“eventTimestamp”: “2022-04-04T21:12:14Z”,”senderAddress”: “john-doe@some-domain.com”,”recipientAddress”: “jane.doe@some-other-domain.com”,”subject”: “Important Notice”,”markedAs”: “Malicious”,”threatId”: 123}..

abnormal-security-search-messages


Search for messages using filters across abnormal or quarantine sources.

Base Command

abnormal-security-search-messages

Input

Argument Name Description Required
source Message source (abnormal or quarantine). Possible values are: abnormal, quarantine. Required
tenant_ids Comma-separated list of tenant IDs. Optional
start_time Start time for the search in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Required
end_time End time for the search in ISO 8601 format (e.g., 2024-01-31T23:59:59Z). Required
subject Filter by email subject. Optional
sender_email Filter by sender email address. Optional
sender_name Filter by sender name. Optional
recipient_email Filter by recipient email address. Optional
recipient_name Filter by recipient name. Optional
attachment_name Filter by attachment name. Optional
attachment_md5_hash Filter by attachment MD5 hash. Optional
internet_message_id Filter by internet message ID. Optional
body_link Filter by body link/URL. Optional
sender_ip Filter by sender IP address. Optional
judgement Filter by judgement status. Possible values are: attack, borderline, spam, graymail, safe. Optional
use_sender_regex Use regex for sender filtering (default false). Possible values are: true, false. Optional
use_recipient_regex Use regex for recipient filtering (default false). Possible values are: true, false. Optional
show_graymail Show graymail messages (default false). Possible values are: true, false. Optional
page_number Page number (default 1). Optional
page_size Page size (default 100, max 1000). Optional

Context Output

Path Type Description
AbnormalSecurity.MessageSearch.results Unknown List of messages matching the search criteria.
AbnormalSecurity.MessageSearch.results.abnormal_message_id String Abnormal message identifier.
AbnormalSecurity.MessageSearch.results.subject String Email subject.
AbnormalSecurity.MessageSearch.results.sender String Sender email address.
AbnormalSecurity.MessageSearch.results.mailbox_name String Mailbox name where the message was received.
AbnormalSecurity.MessageSearch.results.received_time String Time when message was received.
AbnormalSecurity.MessageSearch.results.decision_category String Decision category (malicious, spam, safe, graymail).
AbnormalSecurity.MessageSearch.results.judgement String Judgement status (attack, borderline, spam, graymail, safe).
AbnormalSecurity.MessageSearch.total Number Total number of messages found.
AbnormalSecurity.MessageSearch.pageNumber Number Current page number.
AbnormalSecurity.MessageSearch.nextPageNumber Number Next page number.

abnormal-security-remediate-messages


Remediate messages by performing actions like delete, move, or submit to Detection360.

Base Command

abnormal-security-remediate-messages

Input

Argument Name Description Required
action Action to perform on messages. Possible values are: delete, move_to_inbox, submit_to_d360, reclassify. Required
tenant_ids Comma-separated list of tenant IDs. Optional
source Message source (abnormal or quarantine). Possible values are: abnormal, quarantine. Required
remediation_reason Reason for remediation. Possible values are: false_negative, misdirected, unsolicited, other, groups_remediation, quarantine_release. Required
messages JSON string containing array of message objects to remediate. Required if remediate_all is false. Optional
remediate_all Whether to remediate all messages matching search filters (default false). Possible values are: true, false. Optional
target_folder Target folder for move actions (e.g., “Deleted Items”). Optional
submit_d360_case Whether to submit a Detection360 case (default false). Possible values are: true, false. Optional
start_time Start time for search filters when remediate_all is true (ISO 8601 format). Optional
end_time End time for search filters when remediate_all is true (ISO 8601 format). Optional
subject Subject filter when remediate_all is true. Optional
sender_email Sender email filter when remediate_all is true. Optional
sender_name Sender name filter when remediate_all is true. Optional
recipient_email Recipient email filter when remediate_all is true. Optional
recipient_name Recipient name filter when remediate_all is true. Optional
attachment_name Attachment name filter when remediate_all is true. Optional
attachment_md5_hash Attachment MD5 hash filter when remediate_all is true. Optional
internet_message_id Internet message ID filter when remediate_all is true. Optional
body_link Body link/URL filter when remediate_all is true. Optional
sender_ip Sender IP address filter when remediate_all is true. Optional
judgement Judgement filter when remediate_all is true. Possible values are: attack, borderline, spam, graymail, safe. Optional
use_sender_regex Use regex for sender filtering when remediate_all is true (default false). Possible values are: true, false. Optional
use_recipient_regex Use regex for recipient filtering when remediate_all is true (default false). Possible values are: true, false. Optional
show_graymail Show graymail messages when remediate_all is true (default false). Possible values are: true, false. Optional

Context Output

Path Type Description
AbnormalSecurity.MessageRemediation.activity_log_id Number Activity log ID for tracking the remediation request.
AbnormalSecurity.MessageRemediation.metadata Unknown Metadata about the request.

abnormal-security-list-activities


Get a list of activity logs for message search and remediation operations.

Base Command

abnormal-security-list-activities

Input

Argument Name Description Required
tenant_ids Comma-separated list of tenant IDs (will be sent as query parameters). Optional
action Filter by action type. Possible values are: search, remediation, csv_export. Optional
page_number Page number (default 1). Optional
page_size Page size (default 100, max 1000). Optional

Context Output

Path Type Description
AbnormalSecurity.Activities.activities Unknown List of activity logs.
AbnormalSecurity.Activities.activities.activity_id Number Activity log ID.
AbnormalSecurity.Activities.activities.action String Action type (search, remediate, csv_export).
AbnormalSecurity.Activities.activities.status String Activity status.
AbnormalSecurity.Activities.activities.performed_by String User who performed the action.
AbnormalSecurity.Activities.activities.timestamp String Timestamp of the activity.
AbnormalSecurity.Activities.activities.result_count Number Number of results from the activity.
AbnormalSecurity.Activities.total Number Total number of activities.
AbnormalSecurity.Activities.page Number Current page number.
AbnormalSecurity.Activities.size Number Page size.

abnormal-security-get-activity-status


Get the status and details of a specific activity log entry.

Base Command

abnormal-security-get-activity-status

Input

Argument Name Description Required
activity_log_id Activity log ID to get status for. Required
page Page number (default 1). Optional
size Page size (default 100, max 1000). Optional

Context Output

Path Type Description
AbnormalSecurity.ActivityStatus.activity_id Number Activity log ID.
AbnormalSecurity.ActivityStatus.action String Action type.
AbnormalSecurity.ActivityStatus.status String Activity status.
AbnormalSecurity.ActivityStatus.performed_by String User who performed the action.
AbnormalSecurity.ActivityStatus.timestamp String Timestamp of the activity.
AbnormalSecurity.ActivityStatus.result_count Number Number of results.
AbnormalSecurity.ActivityStatus.remediation_details Unknown Detailed remediation information for each message.
AbnormalSecurity.ActivityStatus.total Number Total number of remediation details.

abnormal-security-download-message-attachment


Download an attachment from a message found in search results. All required parameters can be obtained from the abnormal-security-search-messages command output.

Base Command

abnormal-security-download-message-attachment

Input

Argument Name Description Required
message_id Abnormal message ID (can be negative). Obtained from message search results. Required
attachment_name Name of the attachment to download. Obtained from message search results attachments field. Required
tenant_id Tenant ID for the message. Obtained from message search results. Required
raw_message_id Cloud provider message ID (O365/GSuite). Obtained from message search results as raw_message_id. Required
native_user_id Cloud provider user ID. Obtained from message search results as native_user_id. Required
recipient_mailbox Mailbox email address. Obtained from message search results as mailbox_name. Required

Context Output

There is no context output for this command.

abnormal-security-download-message-eml


Download a message in RFC822 EML format for forensic analysis. For quarantine messages, both quarantine_identity and recipient_mailbox parameters are required.

Base Command

abnormal-security-download-message-eml

Input

Argument Name Description Required
cloud_message_id The cloud_message_id from search results (format abx:CloudMessage:…). Use the cloud_message_id field from message search results. Required
quarantine_identity Quarantine identifier (required only for quarantine messages). Obtained from quarantine_info.identity field in search results. Optional
recipient_mailbox Recipient email address (required only for quarantine messages). Obtained from mailbox_name field in search results. Optional

Context Output

There is no context output for this command.

abnormal-security-list-unanalyzed-abuse-mailbox-campaigns


Get a list of unanalyzed Abuse Mailbox campaigns

Base Command

abnormal-security-list-unanalyzed-abuse-mailbox-campaigns

Input

Argument Name Description Required
start The start time for retrieving the list of unanalyzed abuse mailbox campaigns.. Optional
end The end time for retrieving the list of unanalyzed abuse mailbox campaigns. Optional

Context Output

Path Type Description
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.abx_message_id Number An id which maps to an abuse campaign.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.recipient.name String The name of the recipient.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.recipient.email String The email address of the recipient.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.reported_datetime String The datetime the report was made.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.reporter.email String The email address of the reporter.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.reporter.name String The name of the reporter.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.subject String The subject of the message.
AbnormalSecurity.UnanalyzedAbuseCampaigns.results.not_analyzed_reason String The reason the message was not analyzed.

Command Example

!abnormal-security-list-unanalyzed-abuse-mailbox-campaigns

Context Example

{
    "AbnormalSecurity": {
        "AbuseCampaign": {
            "results": [
                {
                    "abx_message_id": 123456789,
                    "recipient": {
                        "name": "John Doe",
                        "email": "john.doe@some-domain.com"
                    },
                    "reported_datetime": "2023-06-15T00:17:31Z",
                    "reporter": {
                        "email": "info@some-domain.com",
                        "name": "Support"
                    },
                    "subject": "URGENT",
                    "not_analyzed_reason": "INVALID_SUBMISSION"
                },
                {
                    "abx_message_id": 987654321,
                    "recipient": {
                        "name": "Jane Doe",
                        "email": "jane.doe@some-domain.com"
                    },
                    "reported_datetime": "2023-06-14T06:23:31Z",
                    "reporter": {
                        "email": "info@some-domain.com",
                        "name": "support"
                    },
                    "subject": "Hello",
                    "not_analyzed_reason": "INVALID_SUBMISSION"
                }
            ]
        }
    }
}

Human Readable Output

Unanalyzed Abuse Mailbox Campaigns

abx_message_id recipient reported_datetime reporter subject not_analyzed_reason
123456789 name: John Doe email: john.doe@some-domain.com 2023-06-15T00:17:31Z email : info@some-domain.com name: Support URGENT INVALID_SUBMISSION

Configuration parameters

  • url — Server URL (e.g. https://api.abnormalplatform.com/v1) (required)
  • api_key — API Key (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • max_fetch — Maximum incidents to fetch.
  • fetch_threats — Fetch Threats
  • fetch_abuse_campaigns — Fetch Abuse Campaigns
  • fetch_account_takeover_cases — Fetch Account Takeover Cases
  • first_fetch — First fetch time
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • polling_lag — Polling Lag Time (in minutes)
  • max_page_number — Maximum incidents pages to fetch

Commands (31)

  • abnormal-security-check-case-action-status

    Check the status of an action requested on a case.

  • abnormal-security-check-threat-action-status

    Check the status of an action requested on a threat.

  • abnormal-security-download-message-attachment

    Download an attachment from a message found in search results. All required parameters can be obtained from the abnormal-security-search-messages command output.

  • abnormal-security-download-message-eml

    Download a message in RFC822 EML format for forensic analysis. For quarantine messages, both quarantine_identity and recipient_mailbox parameters are required.

  • abnormal-security-download-threat-log-csv

    Download data from Threat Log in .csv format.

  • abnormal-security-get-abnormal-case

    Get details of an Abnormal case.

  • abnormal-security-get-abuse-mailbox-campaign

    Get details of an Abuse Mailbox campaign.

  • abnormal-security-get-activity-status

    Get the status and details of a specific activity log entry.

  • abnormal-security-get-case-analysis-and-timeline

    Provides the analysis and timeline details of a case.

  • abnormal-security-get-employee-identity-analysis

    Get employee identity analysis (Genome) data.

  • abnormal-security-get-employee-information

    Get employee information.

  • abnormal-security-get-employee-last-30-days-login-csv

    Get employee login information for last 30 days in csv format.

  • abnormal-security-get-latest-threat-intel-feed Deprecated

    Get the latest threat intel feed.

  • abnormal-security-get-threat

    Get details of a threat.

  • abnormal-security-get-vendor-activity

    Get the activity for a specific vendor.

  • abnormal-security-get-vendor-case-details

    Get the details of a vendor case.

  • abnormal-security-get-vendor-details

    Get the details of a specific vendor.

  • abnormal-security-list-abnormal-cases

    Get a list of Abnormal cases identified by Abnormal Security.

  • abnormal-security-list-abuse-mailbox-campaigns

    Get a list of campaigns submitted to Abuse Mailbox.

  • abnormal-security-list-activities

    Get a list of activity logs for message search and remediation operations.

  • abnormal-security-list-threats

    Get a list of threats.

  • abnormal-security-list-unanalyzed-abuse-mailbox-campaigns

    Get a list of unanalyzed abuse mailbox campaigns.

  • abnormal-security-list-vendor-cases

    Get a list of vendor cases.

  • abnormal-security-list-vendors

    Get a list of vendors.

  • abnormal-security-manage-abnormal-case

    Manage an Abnormal Case.

  • abnormal-security-manage-threat

    Manage a Threat identified by Abnormal Security.

  • abnormal-security-remediate-messages

    Remediate messages by performing actions like delete, move, or submit to Detection360.

  • abnormal-security-search-messages

    Search for messages using filters across abnormal or quarantine sources.

  • abnormal-security-submit-false-negative-report

    Submit a False Negative Report.

  • abnormal-security-submit-false-positive-report

    Submit a False Positive Report.

  • abnormal-security-submit-inquiry-to-request-a-report-on-misjudgement

    Submit an Inquiry to request a report on misjudgement by Abnormal Security.

import logging
from datetime import datetime, timedelta
from typing import Any

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401

urllib3.disable_warnings()


DEFAULT_INTERVAL = 30
DEFAULT_TIMEOUT = 600
FETCH_LIMIT = 200
MAX_PAGE_SIZE = 100


XSOAR_SEVERITY_BY_AMP_SEVERITY = {
    "Low": IncidentSeverity.LOW,
    "Medium": IncidentSeverity.MEDIUM,
    "High": IncidentSeverity.HIGH,
    "Critical": IncidentSeverity.CRITICAL,
}

ISO_8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
TIME_FORMAT_WITHMS = "%Y-%m-%dT%H:%M:%S.%fZ"

# 4xx status codes that indicate systemic issues and should NOT be skipped
NON_SKIPPABLE_STATUS_CODES = {401, 403, 429}


def _is_skippable_error(e: DemistoException) -> bool:
    """Check if a DemistoException from an API call is a 4xx error that can be safely skipped.

    Skippable errors are client errors (4xx) that are specific to a single entity
    (e.g., 404 Not Found, 410 Gone). Non-skippable errors indicate systemic issues
    (401 Unauthorized, 403 Forbidden, 429 Rate Limit) and should be raised.

    Args:
        e: The DemistoException raised by _http_request.

    Returns:
        True if the error can be safely skipped, False otherwise.
    """
    status_code = None
    if e.res is not None and hasattr(e.res, "status_code"):
        status_code = e.res.status_code
    if status_code is None:
        return False
    return 400 <= status_code < 500 and status_code not in NON_SKIPPABLE_STATUS_CODES


def try_str_to_datetime(time: str) -> datetime:
    """
    Try to convert a string to a datetime object.
    """
    try:
        return datetime.strptime(time, ISO_8601_FORMAT).astimezone(timezone.utc)
    except Exception as _:
        pass
    return datetime.strptime((time[:26] + "Z") if len(time) > 26 else time, TIME_FORMAT_WITHMS).astimezone(timezone.utc)


def get_current_datetime() -> datetime:
    return datetime.utcnow().astimezone(timezone.utc)


class FetchIncidentsError(Exception):
    """Raised when there's an error in fetching incidents."""


class Client(BaseClient):
    CASES = "cases"
    ABUSE_CAMPAIGNS = "abusecampaigns"
    THREATS = "threats"

    def __init__(self, server_url, verify, proxy, headers, auth):
        super().__init__(base_url=server_url, verify=verify, proxy=proxy, headers=headers, auth=auth, timeout=2400)

    def check_the_status_of_an_action_requested_on_a_case_request(self, case_id, action_id, subtenant):
        params = assign_params(subtenant)
        headers = self._headers

        response = self._http_request("get", f"cases/{case_id}/actions/{action_id}", params=params, headers=headers)

        return response

    def check_the_status_of_an_action_requested_on_a_threat_request(self, threat_id, action_id, subtenant):
        params = assign_params(subtenant)
        headers = self._headers

        response = self._http_request("get", f"threats/{threat_id}/actions/{action_id}", params=params, headers=headers)

        return response

    def download_data_from_threat_log_in_csv_format_request(self, filter_, source, subtenant):
        params = assign_params(filter=filter_, source=source, subtenant=subtenant)

        headers = self._headers

        response = self._http_request("get", "threats_export/csv", params=params, headers=headers, resp_type="response")
        return response

    def get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
        self, filter_="", page_size=None, page_number=None, subtenant=None
    ):
        params = assign_params(filter=filter_, pageSize=page_size, pageNumber=page_number, subtenant=subtenant)

        headers = self._headers

        response = self._http_request("get", "cases", params=params, headers=headers)

        return response

    def get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
        self,
        filter_="",
        page_size=None,
        page_number=None,
        subtenant=None,
        subject=None,
        sender=None,
        recipient=None,
        reporter=None,
        attackType=None,
        threatType=None,
    ):
        params = assign_params(
            filter=filter_,
            pageSize=page_size,
            pageNumber=page_number,
            subtenant=subtenant,
            subject=subject,
            sender=sender,
            recipient=recipient,
            reporter=reporter,
            attackType=attackType,
            threatType=threatType,
        )

        headers = self._headers

        response = self._http_request("get", "abusecampaigns", params=params, headers=headers)

        return response

    def get_a_list_of_threats_request(
        self,
        filter_="",
        page_size=None,
        page_number=None,
        source=None,
        subtenant=None,
        subject=None,
        sender=None,
        recipient=None,
        topic=None,
        attackType=None,
        attackVector=None,
    ):
        params = assign_params(
            filter=filter_,
            pageSize=page_size,
            pageNumber=page_number,
            source=source,
            subtenant=subtenant,
            subject=subject,
            sender=sender,
            recipient=recipient,
            topic=topic,
            attackType=attackType,
            attackVector=attackVector,
        )

        headers = self._headers

        response = self._http_request("get", "threats", params=params, headers=headers)

        return response

    def get_page_number_and_max_iterations(self, max_incidents_to_fetch):
        page_size = min(max_incidents_to_fetch, 100)
        max_iterations = (max_incidents_to_fetch // page_size) + 1
        return page_size, max_iterations

    def get_paginated_cases_list(self, filter_="", max_incidents_to_fetch=FETCH_LIMIT):
        cases_response: dict[str, list[dict]] = {"cases": []}
        if max_incidents_to_fetch < 1:
            return cases_response

        page_number, current_iteration = 1, 1
        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)

        while page_number is not None:
            response = self.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
                filter_=filter_, page_size=page_size, page_number=page_number
            )
            cases_response["cases"].extend(response.get("cases", []))
            page_number = response.get("nextPageNumber", None)
            current_iteration += 1
            if current_iteration > max_iterations:
                break
        return cases_response

    def get_paginated_threats_list(self, filter_="", max_incidents_to_fetch=FETCH_LIMIT):
        threats_response: dict[str, list[dict]] = {"threats": []}
        if max_incidents_to_fetch < 1:
            return threats_response

        page_number, current_iteration = 1, 1
        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)

        while page_number is not None:
            response = self.get_a_list_of_threats_request(filter_=filter_, page_size=page_size, page_number=page_number)
            threats_response["threats"].extend(response.get("threats", []))
            page_number = response.get("nextPageNumber", None)
            current_iteration += 1
            if current_iteration > max_iterations:
                break
        return threats_response

    def get_paginated_abusecampaigns_list(self, filter_="", max_incidents_to_fetch=FETCH_LIMIT):
        campaigns_response: dict[str, list[dict]] = {"campaigns": []}
        if max_incidents_to_fetch < 1:
            return campaigns_response

        page_number, current_iteration = 1, 1
        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)

        while page_number is not None:
            response = self.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
                filter_=filter_, page_size=page_size, page_number=page_number
            )
            campaigns_response["campaigns"].extend(response.get("campaigns", []))
            page_number = response.get("nextPageNumber", None)
            current_iteration += 1
            if current_iteration > max_iterations:
                break
        return campaigns_response

    def get_details_of_a_threat_request(self, threat_id, subtenant=None, page_size=None, page_number=None):
        """
        Get details of a specific threat with pagination support.

        Args:
            threat_id (str): The ID of the threat to get details for
            subtenant (str, optional): The subtenant ID
            page_size (int, optional): The number of items per page
            page_number (int, optional): The page number (zero-based)

        Returns:
            dict: The threat details with pagination
        """
        headers = self._headers
        params = assign_params(subtenant=subtenant, pageSize=page_size, pageNumber=page_number)

        response = self._http_request("get", f"threats/{threat_id}", params=params, headers=headers)

        return response

    def get_details_of_an_abnormal_case_request(self, case_id, subtenant=None):
        headers = self._headers
        params = assign_params(subtenant=subtenant)

        response = self._http_request("get", f"cases/{case_id}", params=params, headers=headers)

        return response

    def get_details_of_an_abuse_mailbox_campaign_request(self, campaign_id, subtenant=None):
        headers = self._headers
        params = assign_params(subtenant=subtenant)

        response = self._http_request("get", f"abusecampaigns/{campaign_id}", params=params, headers=headers)

        return response

    def get_employee_identity_analysis_genome_data_request(self, email_address):
        headers = self._headers

        response = self._http_request("get", f"employee/{email_address}/identity", headers=headers)

        return response

    def get_employee_information_request(self, email_address):
        headers = self._headers

        response = self._http_request("get", f"employee/{email_address}", headers=headers)

        return response

    def get_employee_login_information_for_last_30_days_in_csv_format_request(self, email_address):
        headers = self._headers

        response = self._http_request("get", f"employee/{email_address}/logins", headers=headers, resp_type="response")

        return response

    def get_the_latest_threat_intel_feed_request(self):
        headers = self._headers
        response = self._http_request("get", "threat-intel", headers=headers, timeout=120, resp_type="response")

        return response

    def manage_a_threat_identified_by_abnormal_security_request(self, threat_id, action):
        headers = self._headers
        json_data = {"action": action}

        response = self._http_request("post", f"threats/{threat_id}", json_data=json_data, headers=headers)

        return response

    def manage_an_abnormal_case_request(self, case_id, action):
        headers = self._headers
        json_data = {"action": action}

        response = self._http_request("post", f"cases/{case_id}", json_data=json_data, headers=headers)

        return response

    def provides_the_analysis_and_timeline_details_of_a_case_request(self, case_id, subtenant):
        params = assign_params(subtenant=subtenant)
        headers = self._headers

        response = self._http_request("get", f"cases/{case_id}/analysis", params=params, headers=headers)

        return response

    def submit_an_inquiry_to_request_a_report_on_misjudgement_by_abnormal_security_request(self, reporter, report_type):
        headers = self._headers
        json_data = {
            "reporter": reporter,
            "report_type": report_type,
        }
        response = self._http_request("post", "inquiry", json_data=json_data, headers=headers)

        return response

    def submit_false_negative_report_request(self, recipient_email, sender_email, subject):
        headers = self._headers
        json_data = {
            "report_type": "false-negative",
            "recipient_email": recipient_email,
            "sender_email": sender_email,
            "subject": subject,
        }
        response = self._http_request("post", "detection360/reports", json_data=json_data, headers=headers)

        return response

    def submit_false_positive_report_request(self, portal_link):
        headers = self._headers
        json_data = {
            "report_type": "false-positive",
            "portal_link": portal_link,
        }
        response = self._http_request("post", "detection360/reports", json_data=json_data, headers=headers)

        return response

    def get_a_list_of_vendors_request(self, page_size, page_number):
        params = assign_params(pageSize=page_size, pageNumber=page_number)

        headers = self._headers

        response = self._http_request("get", "vendors", params=params, headers=headers)

        response = self._remove_keys_from_response(response, ["pageNumber", "nextPageNumber"])

        return response["vendors"]

    def get_the_details_of_a_specific_vendor_request(self, vendorDomain):
        headers = self._headers

        response = self._http_request("get", f"vendors/{vendorDomain}/details", headers=headers)

        return response

    def get_the_activity_of_a_specific_vendor_request(self, vendorDomain):
        headers = self._headers

        response = self._http_request("get", f"vendors/{vendorDomain}/activity", headers=headers)

        return response

    def get_a_list_of_vendor_cases_request(self, filter_, page_size, page_number):
        params = assign_params(filter=filter_, pageSize=page_size, pageNumber=page_number)

        headers = self._headers

        response = self._http_request("get", "vendor-cases", params=params, headers=headers)

        response = self._remove_keys_from_response(response, ["pageNumber", "nextPageNumber"])

        return response["vendorCases"]

    def get_the_details_of_a_vendor_case_request(self, caseId):
        headers = self._headers

        response = self._http_request("get", f"vendor-cases/{caseId}", headers=headers)

        return response

    def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_request(self, start, end):
        params = assign_params(start=start, end=end)

        headers = self._headers

        response = self._http_request("get", "abuse_mailbox/not_analyzed", params=params, headers=headers)

        return response

    def search_messages_request(self, source, tenant_ids, filters, page_number=None, page_size=None):
        """
        Search for messages using the SOAR Message Search API.

        Args:
            source (str): Message source (abnormal|quarantine)
            tenant_ids (list): List of tenant IDs
            filters (dict): Search filters
            page_number (int, optional): Page number (default 1)
            page_size (int, optional): Page size (default 100, max 1000)

        Returns:
            dict: Search results with messages, pagination, and metadata
        """
        params = assign_params(pageNumber=page_number, pageSize=page_size)
        headers = self._headers

        json_data = {
            "source": source,
            "tenant_ids": tenant_ids,
            "filters": filters,
        }

        response = self._http_request("post", "search", params=params, json_data=json_data, headers=headers)

        return response

    def remediate_messages_request(
        self, action, tenant_ids, source, remediation_reason, messages=None, remediate_all=False, search_filters=None, **kwargs
    ):
        """
        Remediate messages using the SOAR Message Remediation API.

        Args:
            action (str): Action to perform (delete|move_to_inbox|submit_to_d360|reclassify)
            tenant_ids (list): List of tenant IDs
            source (str): Message source (abnormal|quarantine)
            remediation_reason (str): Reason for remediation
            messages (list, optional): List of message objects to remediate
            remediate_all (bool, optional): Whether to remediate all matching messages
            search_filters (dict, optional): Search filters when remediate_all=True
            **kwargs: Additional optional parameters (target_folder, submit_d360_case)

        Returns:
            dict: Remediation response with activity_log_id and metadata
        """
        headers = self._headers

        json_data = {
            "action": action,
            "tenant_ids": tenant_ids,
            "source": source,
            "remediation_reason": remediation_reason,
            "remediate_all": remediate_all,
        }

        if messages:
            json_data["messages"] = messages
        if search_filters:
            json_data["search_filters"] = search_filters

        # Add optional parameters
        if "target_folder" in kwargs:
            json_data["target_folder"] = kwargs["target_folder"]
        if "submit_d360_case" in kwargs:
            json_data["submit_d360_case"] = kwargs["submit_d360_case"]

        response = self._http_request("post", "search/remediate", json_data=json_data, headers=headers)

        return response

    def get_activities_list_request(self, tenant_ids, action=None, page_number=None, page_size=None):
        """
        Get list of activity logs using the SOAR Activity Logs API.

        Args:
            tenant_ids (list): List of tenant IDs (passed as query parameters)
            action (str, optional): Filter by action (search|remediation|csv_export)
            page_number (int, optional): Page number (default 1)
            page_size (int, optional): Page size (default 100, max 1000)

        Returns:
            dict: Activity logs with pagination and metadata
        """
        params = assign_params(action=action, pageNumber=page_number, pageSize=page_size, tenant_ids=tenant_ids)
        headers = self._headers

        response = self._http_request("get", "search/activities", params=params, headers=headers)

        return response

    def get_activity_status_request(self, activity_log_id, page=None, size=None):
        """
        Get status of a specific activity using the SOAR Activity Status API.

        Args:
            activity_log_id (str): Activity log ID
            page (int, optional): Page number (default 1)
            size (int, optional): Page size (default 100, max 1000)

        Returns:
            dict: Activity status with remediation details and metadata
        """
        params = assign_params(page=page, size=size)
        headers = self._headers

        response = self._http_request("get", f"search/activities/{activity_log_id}/status", params=params, headers=headers)

        return response

    def download_message_attachment_request(
        self, message_id, attachment_name, tenant_id, raw_message_id, native_user_id, recipient_mailbox
    ):
        """
        Download a message attachment using the SOAR Attachment Download API.

        Args:
            message_id (str): Abnormal message ID (can be negative)
            attachment_name (str): Name of the attachment to download
            tenant_id (int): Tenant ID for the message
            raw_message_id (str): Cloud provider message ID (O365/GSuite)
            native_user_id (str): Cloud provider user ID
            recipient_mailbox (str): Mailbox email address

        Returns:
            Response: HTTP response object containing the attachment file
        """
        params = assign_params(
            message_id=message_id,
            attachment_name=attachment_name,
            tenant_id=tenant_id,
            raw_message_id=raw_message_id,
            native_user_id=native_user_id,
            recipient_mailbox=recipient_mailbox,
        )
        headers = self._headers

        response = self._http_request(
            "get", "search/messages/attachments/download", params=params, headers=headers, resp_type="response"
        )

        return response

    def download_message_eml_request(self, cloud_message_id, quarantine_identity=None, recipient_mailbox=None):
        """
        Download a message in EML format using the SOAR EML Download API.

        Args:
            cloud_message_id (str): The cloud_message_id from search results (format: abx:CloudMessage:...)
            quarantine_identity (str, optional): Quarantine identifier (required for quarantine messages)
            recipient_mailbox (str, optional): Recipient email address (required for quarantine messages)

        Returns:
            Response: HTTP response object containing the EML file (RFC822 format)
        """
        params = assign_params(quarantineIdentity=quarantine_identity, recipientMailbox=recipient_mailbox)
        headers = self._headers

        response = self._http_request(
            "get", f"search/messages/{cloud_message_id}/eml", params=params, headers=headers, resp_type="response"
        )

        return response

    def _remove_keys_from_response(self, response, keys_to_remove):
        """Removes specified keys from the response."""
        for key in keys_to_remove:
            response.pop(key, None)
        return response


def check_the_status_of_an_action_requested_on_a_case_command(client, args):
    case_id = str(args.get("case_id", ""))
    action_id = str(args.get("action_id", ""))
    subtenant = args.get("subtenant", None)

    response = client.check_the_status_of_an_action_requested_on_a_case_request(case_id, action_id, subtenant)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.ActionStatus", outputs_key_field="", outputs=response, raw_response=response
    )

    return command_results


def check_the_status_of_an_action_requested_on_a_threat_command(client, args):
    threat_id = str(args.get("threat_id", ""))
    action_id = str(args.get("action_id", ""))
    subtenant = args.get("subtenant", None)

    response = client.check_the_status_of_an_action_requested_on_a_threat_request(threat_id, action_id, subtenant)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.ActionStatus", outputs_key_field="", outputs=response, raw_response=response
    )

    return command_results


def download_data_from_threat_log_in_csv_format_command(client, args):
    filter_ = str(args.get("filter", ""))
    source = str(args.get("source", ""))
    subtenant = args.get("subtenant", None)

    response = client.download_data_from_threat_log_in_csv_format_request(filter_, source, subtenant)
    filename = "threat_log.csv"
    file_content = response.text

    results = fileResult(filename, file_content)

    return results


def get_a_list_of_abnormal_cases_identified_by_abnormal_security_command(client, args):
    filter_ = str(args.get("filter", ""))
    page_size = args.get("page_size", None)
    page_number = args.get("page_number", None)
    subtenant = args.get("subtenant", None)

    response = client.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
        filter_, page_size, page_number, subtenant
    )
    markdown = tableToMarkdown("Case IDs", response.get("cases", []), headers=["caseId", "description"], removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.inline_response_200_1",
        outputs_key_field="",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_a_list_of_campaigns_submitted_to_abuse_mailbox_command(client, args):
    filter_ = str(args.get("filter", ""))
    page_size = args.get("page_size", None)
    page_number = args.get("page_number", None)
    subtenant = args.get("subtenant", None)
    subject = args.get("subject", None)
    sender = args.get("sender", None)
    recipient = args.get("recipient", None)
    reporter = args.get("reporter", None)
    attackType = args.get("attackType", None)
    threatType = args.get("threatType", None)

    response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
        filter_, page_size, page_number, subtenant, subject, sender, recipient, reporter, attackType, threatType
    )
    markdown = tableToMarkdown("Campaign IDs", response.get("campaigns", []), headers=["campaignId"], removeNull=True)

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.AbuseCampaign",
        outputs_key_field="campaignId",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_a_list_of_threats_command(client, args):
    filter_ = str(args.get("filter", ""))
    page_size = args.get("page_size", None)
    page_number = args.get("page_number", None)
    source = str(args.get("source", ""))
    subtenant = args.get("subtenant", None)
    subject = args.get("subject", None)
    sender = args.get("sender", None)
    recipient = args.get("recipient", None)
    topic = args.get("topic", None)
    attackType = args.get("attackType", None)
    attackVector = args.get("attackVector", None)

    response = client.get_a_list_of_threats_request(
        filter_, page_size, page_number, source, subtenant, subject, sender, recipient, topic, attackType, attackVector
    )
    markdown = tableToMarkdown("Threat IDs", response.get("threats"), headers=["threatId"], removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.inline_response_200",
        outputs_key_field="",
        outputs=response,
        raw_response=response,
    )
    return command_results


def get_details_of_a_threat_command(client, args):
    threat_id = str(args.get("threat_id", ""))
    subtenant = args.get("subtenant", None)
    page_size = args.get("page_size", None)
    page_number = args.get("page_number", None)

    response = client.get_details_of_a_threat_request(threat_id, subtenant, page_size, page_number)
    headers = [
        "subject",
        "fromAddress",
        "fromName",
        "toAddresses",
        "recipientAddress",
        "receivedTime",
        "attackType",
        "attackStrategy",
        "abxMessageId",
        "abxPortalUrl",
        "attachmentCount",
        "attachmentNames",
        "attackVector",
        "attackedParty",
        "autoRemediated",
        "impersonatedParty",
        "internetMessageId",
        "isRead",
        "postRemediated",
        "remediationStatus",
        "remediationTimestamp",
        "sentTime",
        "threatId",
        "ccEmails",
        "replyToEmails",
        "returnPath",
        "senderDomain",
        "senderIpAddress",
        "summaryInsights",
        "urlCounturls",
    ]
    markdown = tableToMarkdown(
        f"Messages in Threat {response.get('threatId', '')}", response.get("messages", []), headers=headers, removeNull=True
    )

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.ThreatDetails",
        outputs_key_field="threatId",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_details_of_an_abnormal_case_command(client, args):
    case_id = str(args.get("case_id", ""))
    subtenant = args.get("subtenant", None)
    response = client.get_details_of_an_abnormal_case_request(case_id, subtenant)
    headers = ["caseId", "severity", "affectedEmployee", "firstObserved", "threatIds", "genai_summary"]
    markdown = tableToMarkdown(f"Details of Case {response.get('caseId', '')}", response, headers=headers, removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.AbnormalCaseDetails",
        outputs_key_field="",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_details_of_an_abuse_mailbox_campaign_command(client, args):
    campaign_id = str(args.get("campaign_id", ""))
    subtenant = args.get("subtenant", None)

    response = client.get_details_of_an_abuse_mailbox_campaign_request(campaign_id, subtenant)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.AbuseCampaign", outputs_key_field="campaignId", outputs=response, raw_response=response
    )

    return command_results


def get_employee_identity_analysis_genome_data_command(client, args):
    email_address = str(args.get("email_address", ""))

    response = client.get_employee_identity_analysis_genome_data_request(email_address)

    headers = ["description", "key", "name", "values"]

    markdown = tableToMarkdown(f"Analysis of {email_address}", response.get("data", []), headers=headers, removeNull=True)

    response["email"] = email_address
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.Employee",
        outputs_key_field="email",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_employee_information_command(client, args):
    email_address = str(args.get("email_address", ""))

    response = client.get_employee_information_request(email_address)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.Employee", outputs_key_field="email", outputs=response, raw_response=response
    )

    return command_results


def get_employee_login_information_for_last_30_days_in_csv_format_command(client, args):
    email_address = str(args.get("email_address", ""))

    response = client.get_employee_login_information_for_last_30_days_in_csv_format_request(email_address)
    filename = "employee_login_info_30_days.csv"
    file_content = response.text

    results = fileResult(filename, file_content)

    return results


def get_the_latest_threat_intel_feed_command(client, args=None):
    response = client.get_the_latest_threat_intel_feed_request()
    filename = "threat_intel_feed.json"
    file_content = response.text
    results = fileResult(filename, file_content)

    return results


def manage_a_threat_identified_by_abnormal_security_command(client, args):
    threat_id = str(args.get("threat_id", ""))
    action = str(args.get("action", ""))

    response = client.manage_a_threat_identified_by_abnormal_security_request(threat_id, action)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.ThreatManageResults", outputs_key_field="", outputs=response, raw_response=response
    )

    return command_results


def manage_an_abnormal_case_command(client, args):
    case_id = str(args.get("case_id", ""))
    action = str(args.get("action", ""))

    response = client.manage_an_abnormal_case_request(case_id, action)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.CaseManageResults", outputs_key_field="", outputs=response, raw_response=response
    )

    return command_results


def provides_the_analysis_and_timeline_details_of_a_case_command(client, args):
    case_id = str(args.get("case_id", ""))
    subtenant = args.get("subtenant", None)
    response = client.provides_the_analysis_and_timeline_details_of_a_case_request(case_id, subtenant)
    insight_headers = ["signal", "description"]
    markdown = tableToMarkdown(f"Insights for {case_id}", response.get("insights", []), headers=insight_headers, removeNull=True)

    timeline_headers = [
        "event_timestamp",
        "category",
        "title",
        "field_labels",
        "ip_address",
        "description",
        "location",
        "sender",
        "subject",
        "title",
        "flagging detectors",
        "rule_name",
    ]

    markdown += tableToMarkdown(
        f"Event Timeline for {response.get('caseId', '')}",
        response.get("eventTimeline", []),
        headers=timeline_headers,
        removeNull=True,
    )

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.CaseAnalysis",
        outputs_key_field="caseId",
        outputs=response,
        raw_response=response,
    )

    return command_results


def submit_an_inquiry_to_request_a_report_on_misjudgement_by_abnormal_security_command(client, args):
    reporter = str(args.get("reporter", ""))
    report_type = str(args.get("report_type", ""))
    response = client.submit_an_inquiry_to_request_a_report_on_misjudgement_by_abnormal_security_request(reporter, report_type)
    command_results = CommandResults(
        outputs_prefix="AbnormalSecurity.SubmitInquiry", outputs_key_field="", outputs=response, raw_response=response
    )

    return command_results


def submit_false_negative_report_command(client, args):
    recipient_email = str(args.get("recipient_email", ""))
    sender_email = str(args.get("sender_email", ""))
    subject = str(args.get("subject", ""))
    response = client.submit_false_negative_report_request(recipient_email, sender_email, subject)
    command_results = CommandResults(readable_output=response, raw_response=response)

    return command_results


def submit_false_positive_report_command(client, args):
    portal_link = str(args.get("portal_link", ""))
    response = client.submit_false_positive_report_request(portal_link)
    command_results = CommandResults(readable_output=response, raw_response=response)

    return command_results


def get_a_list_of_vendors_command(client, args):
    page_size = str(args.get("page_size", ""))
    page_number = str(args.get("page_number", ""))
    response = client.get_a_list_of_vendors_request(page_size, page_number)
    markdown = tableToMarkdown("Vendor Domains", response, headers=["vendorDomain"], removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.VendorsList",
        outputs_key_field="vendorDomain",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_the_details_of_a_specific_vendor_command(client, args):
    vendor_domain: str = args["vendor_domain"]
    response = client.get_the_details_of_a_specific_vendor_request(vendor_domain)
    markdown = tableToMarkdown("Vendor Domain", response, removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.VendorDetails",
        outputs_key_field="vendorDomain",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_the_activity_of_a_specific_vendor_command(client, args):
    vendor_domain: str = args["vendor_domain"]
    response = client.get_the_activity_of_a_specific_vendor_request(vendor_domain)
    markdown = tableToMarkdown("Vendor Activity", response.get("eventTimeline"), removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.VendorActivity",
        outputs_key_field="",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_a_list_of_vendor_cases_command(client, args):
    filter_ = str(args.get("filter", ""))
    page_size = str(args.get("page_size", ""))
    page_number = str(args.get("page_number", ""))

    response = client.get_a_list_of_vendor_cases_request(filter_, page_size, page_number)
    markdown = tableToMarkdown("Vendor Case IDs", response, removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.VendorCases",
        outputs_key_field="vendorCaseId",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_the_details_of_a_vendor_case_command(client, args):
    case_id: str = args["case_id"]
    response = client.get_the_details_of_a_vendor_case_request(case_id)
    markdown = tableToMarkdown("Case Details", response, removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.VendorCaseDetails",
        outputs_key_field="vendorCaseId",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
    start = str(args.get("start", ""))
    end = str(args.get("end", ""))

    response = client.get_a_list_of_unanalyzed_abuse_mailbox_campaigns_request(start, end)
    markdown = tableToMarkdown("Unanalyzed Abuse Mailbox Campaigns", response.get("results", []), removeNull=True)
    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.UnanalyzedAbuseCampaigns",
        outputs_key_field="abx_message_id",
        outputs=response,
        raw_response=response,
    )

    return command_results


def search_messages_command(client, args):  # pragma: no cover
    """
    Search for messages using the SOAR Message Search API.
    """
    source = str(args.get("source", ""))
    tenant_ids = argToList(args.get("tenant_ids", []))
    page_number = arg_to_number(args.get("page_number"))
    page_size = arg_to_number(args.get("page_size"))

    # Build filters dictionary
    filters = {}
    if args.get("start_time"):
        filters["start_time"] = str(args.get("start_time"))
    if args.get("end_time"):
        filters["end_time"] = str(args.get("end_time"))
    if args.get("subject"):
        filters["subject"] = str(args.get("subject"))
    if args.get("sender_email"):
        filters["sender_email"] = str(args.get("sender_email"))
    if args.get("sender_name"):
        filters["sender_name"] = str(args.get("sender_name"))
    if args.get("recipient_email"):
        filters["recipient_email"] = str(args.get("recipient_email"))
    if args.get("recipient_name"):
        filters["recipient_name"] = str(args.get("recipient_name"))
    if args.get("attachment_name"):
        filters["attachment_name"] = str(args.get("attachment_name"))
    if args.get("attachment_md5_hash"):
        filters["attachment_md5_hash"] = str(args.get("attachment_md5_hash"))
    if args.get("internet_message_id"):
        filters["internet_message_id"] = str(args.get("internet_message_id"))
    if args.get("body_link"):
        filters["body_link"] = str(args.get("body_link"))
    if args.get("sender_ip"):
        filters["sender_ip"] = str(args.get("sender_ip"))
    if args.get("judgement"):
        filters["judgement"] = str(args.get("judgement"))
    if args.get("use_sender_regex") is not None:
        filters["use_sender_regex"] = argToBoolean(args.get("use_sender_regex"))
    if args.get("use_recipient_regex") is not None:
        filters["use_recipient_regex"] = argToBoolean(args.get("use_recipient_regex"))
    if args.get("show_graymail") is not None:
        filters["show_graymail"] = argToBoolean(args.get("show_graymail"))

    response = client.search_messages_request(source, tenant_ids, filters, page_number, page_size)

    headers = [
        "abnormal_message_id",
        "subject",
        "sender",
        "mailbox_name",
        "received_time",
        "decision_category",
        "judgement",
    ]
    markdown = tableToMarkdown(
        f"Message Search Results (Total: {response.get('total', 0)})",
        response.get("results", []),
        headers=headers,
        removeNull=True,
    )

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.MessageSearch",
        outputs_key_field="abnormal_message_id",
        outputs=response,
        raw_response=response,
    )

    return command_results


def remediate_messages_command(client, args):  # pragma: no cover
    """
    Remediate messages using the SOAR Message Remediation API.
    """
    action = str(args.get("action", ""))
    tenant_ids = argToList(args.get("tenant_ids", []))
    source = str(args.get("source", ""))
    remediation_reason = str(args.get("remediation_reason", ""))
    remediate_all = argToBoolean(args.get("remediate_all", False))

    # Optional parameters
    kwargs = {}
    if args.get("target_folder"):
        kwargs["target_folder"] = str(args.get("target_folder"))
    if args.get("submit_d360_case") is not None:
        kwargs["submit_d360_case"] = argToBoolean(args.get("submit_d360_case"))

    # Handle messages or search_filters
    messages = None
    search_filters = None

    if remediate_all:
        # Build search filters for remediate_all
        search_filters = {}
        if args.get("start_time"):
            search_filters["start_time"] = str(args.get("start_time"))
        if args.get("end_time"):
            search_filters["end_time"] = str(args.get("end_time"))
        if args.get("subject"):
            search_filters["subject"] = str(args.get("subject"))
        if args.get("sender_email"):
            search_filters["sender_email"] = str(args.get("sender_email"))
        if args.get("sender_name"):
            search_filters["sender_name"] = str(args.get("sender_name"))
        if args.get("recipient_email"):
            search_filters["recipient_email"] = str(args.get("recipient_email"))
        if args.get("recipient_name"):
            search_filters["recipient_name"] = str(args.get("recipient_name"))
        if args.get("attachment_name"):
            search_filters["attachment_name"] = str(args.get("attachment_name"))
        if args.get("attachment_md5_hash"):
            search_filters["attachment_md5_hash"] = str(args.get("attachment_md5_hash"))
        if args.get("internet_message_id"):
            search_filters["internet_message_id"] = str(args.get("internet_message_id"))
        if args.get("body_link"):
            search_filters["body_link"] = str(args.get("body_link"))
        if args.get("sender_ip"):
            search_filters["sender_ip"] = str(args.get("sender_ip"))
        if args.get("judgement"):
            search_filters["judgement"] = str(args.get("judgement"))
        if args.get("use_sender_regex") is not None:
            search_filters["use_sender_regex"] = argToBoolean(args.get("use_sender_regex"))
        if args.get("use_recipient_regex") is not None:
            search_filters["use_recipient_regex"] = argToBoolean(args.get("use_recipient_regex"))
        if args.get("show_graymail") is not None:
            search_filters["show_graymail"] = argToBoolean(args.get("show_graymail"))
    else:
        # Parse messages JSON
        messages_json = args.get("messages")
        if messages_json:
            try:
                messages = json.loads(messages_json) if isinstance(messages_json, str) else messages_json
            except json.JSONDecodeError as e:
                raise ValueError(f"Invalid JSON format for messages: {e}")

    response = client.remediate_messages_request(
        action, tenant_ids, source, remediation_reason, messages, remediate_all, search_filters, **kwargs
    )

    markdown = f"## Message Remediation Initiated\n\n**Activity Log ID:** {response.get('activity_log_id', 'N/A')}"

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.MessageRemediation",
        outputs_key_field="activity_log_id",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_activities_list_command(client, args):
    """
    Get list of activity logs using the SOAR Activity Logs API.
    """
    tenant_ids = argToList(args.get("tenant_ids", []))
    action = args.get("action")
    page_number = arg_to_number(args.get("page_number"))
    page_size = arg_to_number(args.get("page_size"))

    response = client.get_activities_list_request(tenant_ids, action, page_number, page_size)

    headers = ["activity_id", "action", "status", "performed_by", "timestamp", "result_count"]
    markdown = tableToMarkdown(
        f"Activity Logs (Total: {response.get('total', 0)})", response.get("activities", []), headers=headers, removeNull=True
    )

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.Activities",
        outputs_key_field="activity_id",
        outputs=response,
        raw_response=response,
    )

    return command_results


def get_activity_status_command(client, args):
    """
    Get status of a specific activity using the SOAR Activity Status API.
    """
    activity_log_id = str(args.get("activity_log_id", ""))
    page = arg_to_number(args.get("page"))
    size = arg_to_number(args.get("size"))

    response = client.get_activity_status_request(activity_log_id, page, size)

    # Create markdown for activity summary
    summary_headers = ["activity_id", "action", "status", "performed_by", "timestamp", "result_count"]
    summary_data = {
        "activity_id": response.get("activity_id"),
        "action": response.get("action"),
        "status": response.get("status") or "In Progress",
        "performed_by": response.get("performed_by") or "N/A",
        "timestamp": response.get("timestamp") or "N/A",
        "result_count": response.get("result_count") if response.get("result_count") is not None else "N/A",
    }
    markdown = tableToMarkdown("Activity Status", [summary_data], headers=summary_headers)

    # Add metadata information if available
    if response.get("metadata"):
        metadata = response.get("metadata")
        markdown += f"\n**Trace ID:** {metadata.get('trace_id', 'N/A')}"
        markdown += f"\n**Response Time:** {metadata.get('response_time', 'N/A')}"

    # Add remediation details table if available
    if response.get("remediation_details"):
        detail_headers = [
            "tenant_id",
            "subject",
            "sender",
            "mailbox_name",
            "status",
            "date_remediated",
        ]
        markdown += "\n\n" + tableToMarkdown(
            f"Remediation Details (Total: {response.get('total', 0)})",
            response.get("remediation_details", []),
            headers=detail_headers,
            removeNull=True,
        )
    elif response.get("status") is None or response.get("result_count") is None:
        # Activity is likely still in progress
        markdown += "\n\n**Note:** Activity is in progress. Details will be available once the activity completes."

    command_results = CommandResults(
        readable_output=markdown,
        outputs_prefix="AbnormalSecurity.ActivityStatus",
        outputs_key_field="activity_id",
        outputs=response,
        raw_response=response,
    )

    return command_results


def download_message_attachment_command(client, args):
    """
    Download a message attachment using the SOAR Attachment Download API.
    """
    message_id = str(args.get("message_id", ""))
    attachment_name = str(args.get("attachment_name", ""))
    tenant_id = arg_to_number(args.get("tenant_id"))
    raw_message_id = str(args.get("raw_message_id", ""))
    native_user_id = str(args.get("native_user_id", ""))
    recipient_mailbox = str(args.get("recipient_mailbox", ""))

    response = client.download_message_attachment_request(
        message_id, attachment_name, tenant_id, raw_message_id, native_user_id, recipient_mailbox
    )

    # Return the file to XSOAR
    file_content = response.content
    results = fileResult(attachment_name, file_content)

    return results


def download_message_eml_command(client, args):
    """
    Download a message in EML format using the SOAR EML Download API.
    """
    cloud_message_id = str(args.get("cloud_message_id", ""))
    quarantine_identity = args.get("quarantine_identity")
    recipient_mailbox = args.get("recipient_mailbox")

    response = client.download_message_eml_request(cloud_message_id, quarantine_identity, recipient_mailbox)

    # Generate filename from cloud_message_id
    # Replace special characters to create a valid filename
    safe_filename = cloud_message_id.replace(":", "_").replace("/", "_")
    filename = f"{safe_filename}.eml"

    # Return the EML file to XSOAR
    file_content = response.content
    results = fileResult(filename, file_content)

    return results


def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
    incidents = []
    for threat in threats:
        page_number = 1
        all_messages, all_filtered_messages = [], []
        threat_details = None
        try:
            while page_number is not None:
                threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
                for message in threat_details["messages"]:
                    all_messages.append(message)
                    remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
                    if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
                        all_filtered_messages.append(message)
                    if remediation_datetime and remediation_datetime < start_datetime:
                        break
                page_number = threat_details.get("nextPageNumber", None)
                if page_number is not None and page_number > max_page_number:
                    break
        except DemistoException as e:
            if _is_skippable_error(e):
                demisto.debug(f"Threat {threat['threatId']} returned a skippable error, skipping: {e}")
                continue
            raise

        # Skip if we didn't get any threat details (shouldn't happen but defensive)
        if threat_details is None:
            continue

        received_time = ""
        threat_details["messages"] = all_filtered_messages or all_messages
        if threat_details.get("messages", []):
            received_time = threat_details["messages"][0].get("receivedTime")

        incident = {
            "dbotMirrorId": str(threat["threatId"]),
            "name": "Threat",
            "occurred": received_time[:26] if len(received_time) > 26 else received_time,
            "details": "Threat",
            "rawJSON": json.dumps(threat_details) if threat_details else {},
        }
        incidents.append(incident)
    return incidents


def generate_abuse_campaign_incidents(client, campaigns):
    incidents = []
    for campaign in campaigns:
        try:
            campaign_details = client.get_details_of_an_abuse_mailbox_campaign_request(campaign["campaignId"])
        except DemistoException as e:
            if _is_skippable_error(e):
                demisto.debug(f"Campaign {campaign['campaignId']} returned a skippable error, skipping: {e}")
                continue
            raise
        first_reported = campaign_details.get("firstReported", "")
        incident = {
            "dbotMirrorId": str(campaign.get("campaignId", "")),
            "name": "Abuse Campaign",
            "occurred": first_reported[:26] if len(first_reported) > 26 else first_reported,
            "details": "Abuse Campaign",
            "rawJSON": json.dumps(campaign_details) if campaign_details else {},
        }
        incidents.append(incident)
    return incidents


def generate_account_takeover_cases_incidents(client, cases):
    incidents = []
    for case in cases:
        try:
            case_details = client.get_details_of_an_abnormal_case_request(case["caseId"])
        except DemistoException as e:
            if _is_skippable_error(e):
                demisto.debug(f"Case {case['caseId']} returned a skippable error, skipping: {e}")
                continue
            raise
        incident = {
            "dbotMirrorId": str(case["caseId"]),
            "name": "Account Takeover Case",
            "occurred": case_details["firstObserved"],
            "details": case["description"],
            "genaiSummary": case_details["genai_summary"],
            "rawJSON": json.dumps(case_details) if case_details else {},
        }
        incidents.append(incident)
    return incidents


def fetch_incidents(
    client: Client,
    last_run: dict[str, Any],
    first_fetch_time: str,
    fetch_threats: bool,
    fetch_abuse_campaigns: bool,
    fetch_account_takeover_cases: bool,
    max_page_number: int = 8,
    max_incidents_to_fetch: int = FETCH_LIMIT,
    polling_lag: timedelta = timedelta(minutes=0),
):
    """
    Fetch incidents from various sources (threats, abuse campaigns, and account takeovers).

    Parameters:
    - client (Client): Client object to interact with the API.
    - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
    - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
    - max_page_number (int): Maximum number of pages to fetch for incidents.
    - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.

    Returns:
    - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
    """
    try:
        last_fetch = last_run.get("last_fetch", first_fetch_time)
        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)

        current_datetime = get_current_datetime()
        start_time = last_fetch + timedelta(milliseconds=1)  # Not to overlap with previous polling window
        end_time = get_current_datetime()

        if polling_lag is not None:
            start_time = start_time - polling_lag
            end_time = end_time - polling_lag

        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
        end_timestamp = end_time.strftime(ISO_8601_FORMAT)

        all_incidents = []
        current_pending_incidents_to_fetch = max_incidents_to_fetch
        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []

        if fetch_threats and current_pending_incidents_to_fetch > 0:
            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
            threats_response = client.get_paginated_threats_list(
                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch
            )
            threat_incidents = generate_threat_incidents(
                client, threats_response.get("threats", []), max_page_number, start_time, end_time
            )
        current_pending_incidents_to_fetch -= len(threat_incidents)

        if fetch_abuse_campaigns and current_pending_incidents_to_fetch > 0:
            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp} and lastReportedTime lte {end_timestamp}"
            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch
            )
            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get("campaigns", []))
        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)

        if fetch_account_takeover_cases and current_pending_incidents_to_fetch > 0:
            account_takeover_cases_filter = f"lastModifiedTime gte {start_timestamp} and lastModifiedTime lte {end_timestamp}"
            account_takeover_cases_response = client.get_paginated_cases_list(
                filter_=account_takeover_cases_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch
            )
            account_takeover_cases_incidents = generate_account_takeover_cases_incidents(
                client, account_takeover_cases_response.get("cases", [])
            )

        all_incidents = threat_incidents + abuse_campaign_incidents + account_takeover_cases_incidents
    except Exception as e:
        logging.error(f"Failed fetching incidents: {e}")
        raise FetchIncidentsError(f"Error while fetching incidents: {e}")

    next_run = {"last_fetch": current_datetime.strftime(ISO_8601_FORMAT)}

    return next_run, all_incidents[:max_incidents_to_fetch]


def test_module(client):
    # Run a sample request to retrieve mock data
    client.get_a_list_of_threats_request(None, None, None, None)
    demisto.results("ok")


def main():  # pragma: nocover
    params = demisto.params()
    args = demisto.args()
    url = params.get("url")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    is_fetch = params.get("isFetch")
    headers = {}
    mock_data = str(args.get("mock-data", ""))
    if mock_data.lower() == "true":
        headers["Mock-Data"] = "True"
    headers["Authorization"] = f'Bearer {params["api_key"]}'
    headers["Soar-Integration-Origin"] = "Cortex XSOAR"
    command = demisto.command()
    demisto.debug(f"Command being called is {command}")

    try:
        client = Client(urljoin(url, ""), verify_certificate, proxy, headers=headers, auth=None)

        commands = {
            # Threat commands
            "abnormal-security-list-threats": get_a_list_of_threats_command,
            "abnormal-security-get-threat": get_details_of_a_threat_command,
            "abnormal-security-manage-threat": manage_a_threat_identified_by_abnormal_security_command,
            "abnormal-security-check-threat-action-status": check_the_status_of_an_action_requested_on_a_threat_command,
            "abnormal-security-download-threat-log-csv": download_data_from_threat_log_in_csv_format_command,
            # Case commands
            "abnormal-security-list-abnormal-cases": get_a_list_of_abnormal_cases_identified_by_abnormal_security_command,
            "abnormal-security-get-abnormal-case": get_details_of_an_abnormal_case_command,
            "abnormal-security-manage-abnormal-case": manage_an_abnormal_case_command,
            "abnormal-security-check-case-action-status": check_the_status_of_an_action_requested_on_a_case_command,
            "abnormal-security-get-case-analysis-and-timeline": provides_the_analysis_and_timeline_details_of_a_case_command,
            # Threat Intel commands
            "abnormal-security-get-latest-threat-intel-feed": get_the_latest_threat_intel_feed_command,
            # Abuse Mailbox commands
            "abnormal-security-list-abuse-mailbox-campaigns": get_a_list_of_campaigns_submitted_to_abuse_mailbox_command,
            "abnormal-security-get-abuse-mailbox-campaign": get_details_of_an_abuse_mailbox_campaign_command,
            "abnormal-security-list-unanalyzed-abuse-mailbox-campaigns": get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command,
            # Employee commands
            "abnormal-security-get-employee-identity-analysis": get_employee_identity_analysis_genome_data_command,
            "abnormal-security-get-employee-information": get_employee_information_command,
            "abnormal-security-get-employee-last-30-days-login-csv":  # noqa: E501
            get_employee_login_information_for_last_30_days_in_csv_format_command,
            # Detection 360 commands
            "abnormal-security-submit-inquiry-to-request-a-report-on-misjudgement":  # noqa: E501
            submit_an_inquiry_to_request_a_report_on_misjudgement_by_abnormal_security_command,
            "abnormal-security-submit-false-negative-report": submit_false_negative_report_command,
            "abnormal-security-submit-false-positive-report": submit_false_positive_report_command,
            # Vendor commands
            "abnormal-security-list-vendors": get_a_list_of_vendors_command,
            "abnormal-security-get-vendor-details": get_the_details_of_a_specific_vendor_command,
            "abnormal-security-get-vendor-activity": get_the_activity_of_a_specific_vendor_command,
            # Vendor case commands
            "abnormal-security-list-vendor-cases": get_a_list_of_vendor_cases_command,
            "abnormal-security-get-vendor-case-details": get_the_details_of_a_vendor_case_command,
            # SOAR Message Search and Respond commands
            "abnormal-security-search-messages": search_messages_command,
            "abnormal-security-remediate-messages": remediate_messages_command,
            "abnormal-security-list-activities": get_activities_list_command,
            "abnormal-security-get-activity-status": get_activity_status_command,
            "abnormal-security-download-message-attachment": download_message_attachment_command,
            "abnormal-security-download-message-eml": download_message_eml_command,
        }

        if command == "test-module":  # pragma: no cover
            headers["Mock-Data"] = "True"
            test_client = Client(urljoin(url, ""), verify_certificate, proxy, headers=headers, auth=None)
            test_module(test_client)
        elif command == "fetch-incidents" and is_fetch:  # pragma: no cover
            max_incidents_to_fetch = arg_to_number(params.get("max_fetch", FETCH_LIMIT))
            fetch_threats = params.get("fetch_threats", False)
            # Get the polling lag time parameter
            polling_lag_minutes = int(params.get("polling_lag", 2))
            max_page_number = int(params.get("max_page_number", 8))
            polling_lag_delta = timedelta(minutes=polling_lag_minutes)
            fetch_abuse_campaigns = params.get("fetch_abuse_campaigns", False)
            fetch_account_takeover_cases = params.get("fetch_account_takeover_cases", False)
            first_fetch_datetime = arg_to_datetime(arg=params.get("first_fetch"), arg_name="First fetch time", required=True)
            if first_fetch_datetime:
                first_fetch_time = first_fetch_datetime.strftime(ISO_8601_FORMAT)
            else:
                first_fetch_time = datetime.now().strftime(ISO_8601_FORMAT)
            next_run, incidents = fetch_incidents(
                client=client,
                last_run=demisto.getLastRun(),
                first_fetch_time=first_fetch_time,
                max_incidents_to_fetch=max_incidents_to_fetch or FETCH_LIMIT,
                fetch_threats=fetch_threats,
                fetch_abuse_campaigns=fetch_abuse_campaigns,
                fetch_account_takeover_cases=fetch_account_takeover_cases,
                max_page_number=max_page_number,
                polling_lag=polling_lag_delta,
            )
            demisto.setLastRun(next_run)
            demisto.incidents(incidents)
        elif command in commands:
            return_results(commands[command](client, args))  # type: ignore
        else:
            raise NotImplementedError(f"{command} command is not implemented.")

    except Exception as e:
        return_error(str(e))


if __name__ in ["__main__", "builtin", "builtins"]:
    main()