Rapid7 Nexpose

Vulnerability management solution to help reduce threat exposure.

Vulnerability Management · Rapid7 InsightVM

Details

IDRapid7 Nexpose
ProviderRapid7
CategoryVulnerability Management
From Version5.0.0
Docker Imagedemisto/auth-utils:1.0.0.10133006
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security Exposure Management

README

Vulnerability management solution to help reduce threat exposure.
This integration was integrated and tested with version 6.6.103 of Rapid7 Nexpose.

Configure Rapid7 InsightVM on Cortex XSOAR

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

    Parameter Required
    Server URL (e.g., https://192.0.2.0:8080) True
    Username True
    Password True
    2FA Token False
    Trust any certificate (not secure) False
    Use system proxy settings False
  4. Click Test to validate the URLs, token, and connection.

Commands

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

nexpose-get-asset


Returns the specified asset.

Base Command

nexpose-get-asset

Input

Argument Name Description Required
id Asset ID. Required

Context Output

Path Type Description
Nexpose.Asset.Addresses unknown All addresses discovered on the asset.
Nexpose.Asset.AssetId number Id of the asset.
Nexpose.Asset.Hardware string The primary Media Access Control (MAC) address of the asset. The format is six groups of two hexadecimal digits separated by colons.
Nexpose.Asset.Aliases unknown All host names or aliases discovered on the asset.
Nexpose.Asset.HostType string The type of asset. Valid values are unknown, guest, hypervisor, physical, mobile
Nexpose.Asset.Site string Asset site name.
Nexpose.Asset.OperatingSystem string Operating system of the asset.
Nexpose.Asset.Vulnerabilities number The total number of vulnerabilities on the asset.
Nexpose.Asset.CPE string The Common Platform Enumeration (CPE) of the operating system.
Nexpose.Asset.LastScanDate date Last scan date of the asset.
Nexpose.Asset.LastScanId number ID of the asset’s last scan.
Nexpose.Asset.RiskScore number The risk score (with criticality adjustments) of the asset.
Nexpose.Asset.Software.Software string The description of the software.
Nexpose.Asset.Software.Version string The version of the software.
Nexpose.Asset.Services.Name string The name of the service.
Nexpose.Asset.Services.Port number The port of the service.
Nexpose.Asset.Services.Product string The product running the service.
Nexpose.Asset.Services.protocol string The protocol of the service, valid values are ip, icmp, igmp, ggp, tcp, pup, udp, idp, esp, nd, raw
Nexpose.Asset.Users.FullName string The full name of the user account.
Nexpose.Asset.Users.Name string The name of the user account.
Nexpose.Asset.Users.UserId number The identifier of the user account.
Nexpose.Asset.Vulnerability.Id number The identifier of the vulnerability.
Nexpose.Asset.Vulnerability.Instances number The number of vulnerable occurrences of the vulnerability. This does not include invulnerable instances.
Nexpose.Asset.Vulnerability.Title string The title (summary) of the vulnerability.
Nexpose.Asset.Vulnerability.Malware number The malware kits that are known to be used to exploit the vulnerability.
Nexpose.Asset.Vulnerability.Exploit number The exploits that can be used to exploit a vulnerability.
Nexpose.Asset.Vulnerability.CVSS string The CVSS exploit score.
Nexpose.Asset.Vulnerability.Risk number The risk score of the vulnerability, rounded to a maximum of to digits of precision. If using the default Rapid7 Real Risk™ model, this value ranges from 0-1000.
Nexpose.Asset.Vulnerability.PublishedOn date The date the vulnerability was first published or announced. The format is an ISO 8601 date, YYYY-MM-DD.
Nexpose.Asset.Vulnerability.ModifiedOn date The last date the vulnerability was modified. The format is an ISO 8601 date, YYYY-MM-DD.
Nexpose.Asset.Vulnerability.Severity string The severity of the vulnerability, one of: “Moderate”, “Severe”, “Critical”.
Endpoint.IP string Endpoint IP address.
Endpoint.HostName string Endpoint host name.
Endpoint.OS string Endpoint operating system.
CVE.ID string Common Vulnerabilities and Exposures IDs.

nexpose-get-asset-tags


Returns the specified tags for an asset.

Base Command

nexpose-get-asset-tags

Input

Argument Name Description Required
asset_id Asset ID. Required

Context Output

Path Type Description
Nexpose.AssetTag.Type string Type of asset tag.
Nexpose.AssetTag.Name string The value of the tag.
Nexpose.AssetTag.CreatedTime string Timestamp of when the tag was created.
Nexpose.AssetTag.RiskModifier string The risk modifier value associated with criticality tag type.

Command example

!nexpose-get-asset-tags asset_id=1

Context Example

{
    "Nexpose":{
        "AssetTag":[
                {
                    "CreatedTime": "2023-00-00T00:00:00.000Z",
                    "Name": "Low",
                    "RiskModifier": "0.75",
                    "Type": "criticality"
                },
                {
                    "CreatedTime": "2023-00-00T00:00:00.000Z",
                    "Name": "FAKELOCATION",
                    "RiskModifier": null,
                    "Type": "location"
                },
                {
                    "CreatedTime": "2023-00-00T00:00:00.000Z",
                    "Name": "FAKEOWNER",
                    "RiskModifier": null,
                    "Type": "owner"
                },
                {
                    "CreatedTime": "2023-00-00T00:00:00.000Z",
                    "Name": "AWS",
                    "RiskModifier": null,
                    "Type": "custom"
                }
            ]
    }
}

Human Readable Output

Nexpose Asset Tags for Asset 1

Type Name Risk Modifier Created Time
criticality Low 0.75 2023-00-00T00:00:00.000Z
location FAKELOCATION   2023-00-00T00:00:00.000Z
owner FAKEOWNER   2023-00-00T00:00:00.000Z
custom AWS   2023-00-00T00:00:00.000Z

nexpose-get-assets


Returns all assets for which you have access.

Base Command

nexpose-get-assets

Input

Argument Name Description Required
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;”. For example: “riskScore,DESC;hostName,ASC”. Optional
limit A number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.Asset.AssetId number The identifier of the asset.
Nexpose.Asset.Address string The primary IPv4 or IPv6 address of the asset.
Nexpose.Asset.Name string The primary host name (local or FQDN) of the asset.
Nexpose.Asset.Site string Asset site name.
Nexpose.Asset.Exploits number The number of distinct exploits that can exploit any of the vulnerabilities on the asset.
Nexpose.Asset.Malware number The number of distinct malware kits that vulnerabilities on the asset are susceptible to.
Nexpose.Asset.OperatingSystem string Operating system of the asset.
Nexpose.Asset.Vulnerabilities number The total number of vulnerabilities.
Nexpose.Asset.RiskScore number The risk score (with criticality adjustments) of the asset.
Nexpose.Asset.Assessed boolean Whether the asset has been assessed for vulnerabilities at least once.
Nexpose.Asset.LastScanDate date Last scan date of the asset.
Nexpose.Asset.LastScanId number Id of the asset’s last scan.
Endpoint.IP string Endpoint IP address.
Endpoint.HostName string Endpoint host name.
Endpoint.OS string Endpoint operating system.

Command example

!nexpose-get-assets limit=3

Context Example

{
    "Endpoint": [
        {
            "Hostname": "pool-96-252-18-158.bstnma.fios.verizon.net",
            "ID": 9,
            "IPAddress": "192.0.2.1",
            "Vendor": "Rapid7 Nexpose"
        },
        {
            "Hostname": "angular.testsparker.com",
            "ID": 11,
            "IPAddress": "192.0.2.2",
            "OS": "Ubuntu Linux",
            "Vendor": "Rapid7 Nexpose"
        },
        {
            "ID": 12,
            "IPAddress": "192.0.2.3",
            "OS": "Microsoft Windows",
            "Vendor": "Rapid7 Nexpose"
        }
    ],
    "Nexpose": {
        "Asset": [
            {
                "Address": "192.0.2.1",
                "Assessed": true,
                "AssetId": 9,
                "Exploits": 0,
                "LastScanDate": "2020-10-01T22:37:33.710Z",
                "LastScanId": 650,
                "Malware": 0,
                "Name": "pool-96-252-18-158.bstnma.fios.verizon.net",
                "OperatingSystem": null,
                "RiskScore": 0,
                "Site": "PANW",
                "Vulnerabilities": 0
            },
            {
                "Address": "192.0.2.2",
                "Assessed": true,
                "AssetId": 11,
                "Exploits": 2,
                "LastScanDate": "2022-11-02T14:54:19.055Z",
                "LastScanId": "-",
                "Malware": 0,
                "Name": "angular.testsparker.com",
                "OperatingSystem": "Ubuntu Linux",
                "RiskScore": 7718.4091796875,
                "Site": "PANW",
                "Vulnerabilities": 26
            },
            {
                "Address": "192.0.2.3",
                "Assessed": true,
                "AssetId": 12,
                "Exploits": 4,
                "LastScanDate": "2049-03-01T04:31:56Z",
                "LastScanId": "-",
                "Malware": 0,
                "Name": null,
                "OperatingSystem": "Microsoft Windows",
                "RiskScore": 18819.919921875,
                "Site": "PANW",
                "Vulnerabilities": 45
            }
        ]
    }
}

Human Readable Output

Nexpose Asset 12

AssetId Address Site Exploits Malware OperatingSystem Vulnerabilities RiskScore Assessed LastScanDate LastScanId
12 192.0.2.3 PANW 4 0 Microsoft Windows 45 18819.919921875 true 2049-03-01T04:31:56Z -

nexpose-search-assets


Search and return all assets matching specific filters. Returns only assets the user has access to.

Base Command

nexpose-search-assets

Input

Argument Name Description Required
query Queries to use as a filter, according to the Search Criteria API standard. Multiple queries can be specified, separated by a “;” separator. For example: “ip-address in-range 192.0.2.0,192.0.2.1;host-name is myhost”. For more information regarding Search Criteria, refer to https://help.rapid7.com/insightvm/en-us/api/index.html#section/Overview/Responses. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit A number of records to limit the response to. Default is 10. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;” separator. For example: “riskScore,DESC;hostName,ASC”. Optional
ipAddressIs A specific IP address to search. Optional
hostNameIs A specific host name to search. Optional
riskScoreHigherThan A minimum risk score to use as a filter. Optional
vulnerabilityTitleContains A string to search for in vulnerabilities titles. Optional
siteIdIn Site IDs to filter for. Can be a comma-separated list. Optional
siteNameIn Site names to filter for. Can be a comma-separated list. Optional
match Operator to determine how to match filters. “all” requires that all filters match for an asset to be included. “any” requires only one filter to match for an asset to be included. Possible values are: all, any. Default is all. Optional

Context Output

Path Type Description
Nexpose.Asset.AssetId number The identifier of the asset.
Nexpose.Asset.Address string The primary IPv4 or IPv6 address of the asset.
Nexpose.Asset.Name string The primary host name (local or FQDN) of the asset.
Nexpose.Asset.Site string Asset site name.
Nexpose.Asset.Exploits number The number of distinct exploits that can exploit any of the vulnerabilities on the asset.
Nexpose.Asset.Malware number The number of distinct malware kits that vulnerabilities on the asset are susceptible to.
Nexpose.Asset.OperatingSystem string Operating system of the asset.
Nexpose.Asset.Vulnerabilities number The total number of vulnerabilities.
Nexpose.Asset.RiskScore number The risk score (with criticality adjustments) of the asset.
Nexpose.Asset.Assessed boolean Whether the asset has been assessed for vulnerabilities at least once.
Nexpose.Asset.LastScanDate date Last scan date of the asset.
Nexpose.Asset.LastScanId number Id of the asset’s last scan.
Endpoint.IP string Endpoint IP address.
Endpoint.HostName string Endpoint host name.
Endpoint.OS string Endpoint operating system.

Command example

!nexpose-search-assets match=all riskScoreHigherThan=1000 limit=3

Context Example

{
    "Endpoint": [
        {
            "Hostname": "angular.testsparker.com",
            "ID": 11,
            "IPAddress": "192.0.2.2",
            "OS": "Ubuntu Linux",
            "Vendor": "Rapid7 Nexpose"
        },
        {
            "ID": 12,
            "IPAddress": "192.0.2.3",
            "OS": "Microsoft Windows",
            "Vendor": "Rapid7 Nexpose"
        },
        {
            "Hostname": "57.27.185.35.bc.googleusercontent.com",
            "ID": 13,
            "IPAddress": "192.0.2.4",
            "OS": "Linux 2.6.18",
            "Vendor": "Rapid7 Nexpose"
        }
    ],
    "Nexpose": {
        "Asset": [
            {
                "Address": "192.0.2.2",
                "Assessed": true,
                "AssetId": 11,
                "Exploits": 2,
                "LastScanDate": "2022-11-02T14:54:19.055Z",
                "LastScanId": "-",
                "Malware": 0,
                "Name": "angular.testsparker.com",
                "OperatingSystem": "Ubuntu Linux",
                "RiskScore": 7718.4091796875,
                "Site": "PANW",
                "Vulnerabilities": 26
            },
            {
                "Address": "192.0.2.3",
                "Assessed": true,
                "AssetId": 12,
                "Exploits": 4,
                "LastScanDate": "2049-03-01T04:31:56Z",
                "LastScanId": "-",
                "Malware": 0,
                "Name": null,
                "OperatingSystem": "Microsoft Windows",
                "RiskScore": 18819.919921875,
                "Site": "PANW",
                "Vulnerabilities": 45
            },
            {
                "Address": "192.0.2.4",
                "Assessed": true,
                "AssetId": 13,
                "Exploits": 0,
                "LastScanDate": "2022-11-15T11:53:25.281Z",
                "LastScanId": "-",
                "Malware": 0,
                "Name": "57.27.185.35.bc.googleusercontent.com",
                "OperatingSystem": "Linux 2.6.18",
                "RiskScore": 1323.0916748046875,
                "Site": "PANW",
                "Vulnerabilities": 2
            }
        ]
    }
}

Human Readable Output

Nexpose Asset 13

AssetId Address Name Site Exploits Malware OperatingSystem RiskScore Assessed LastScanDate LastScanId
13 192.0.2.4 57.27.185.35.bc.googleusercontent.com PANW 0 0 Linux 2.6.18 1323.0916748046875 true 2022-11-15T11:53:25.281Z -

nexpose-get-scan


Get a specific scan.

Base Command

nexpose-get-scan

Input

Argument Name Description Required
id ID of a specific scan to retrieve. Can be a comma-separated list. Required

Context Output

Path Type Description
Nexpose.Scan.Id number The identifier of the scan.
Nexpose.Scan.ScanType string The scan type (automated, manual, scheduled).
Nexpose.Scan.StartedBy string The name of the user who started the scan.
Nexpose.Scan.Assets number The number of assets found in the scan
Nexpose.Scan.TotalTime string The duration of the scan in minutes.
Nexpose.Scan.Status string The scan status. Valid values are aborted, unknown, running, finished, stopped, error, paused, dispatched, integrating
Nexpose.Scan.Completed date The end time of the scan in ISO8601 format.
Nexpose.Scan.Vulnerabilities.Critical number The number of critical vulnerabilities.
Nexpose.Scan.Vulnerabilities.Moderate number The number of moderate vulnerabilities.
Nexpose.Scan.Vulnerabilities.Severe number The number of severe vulnerabilities.
Nexpose.Scan.Vulnerabilities.Total number The total number of vulnerabilities.

Command example

!nexpose-get-scan id=1

Context Example

{
    "Nexpose": {
        "Scan": {
            "Assets": 0,
            "Completed": "2019-12-03T20:48:01.368Z",
            "Id": 1,
            "Message": null,
            "ScanName": "Tue 03 Dec 2019 10:47 PM",
            "ScanType": "Manual",
            "StartedBy": null,
            "Status": "finished",
            "TotalTime": "51.316 seconds",
            "Vulnerabilities": {
                "Critical": 0,
                "Moderate": 0,
                "Severe": 0,
                "Total": 0
            }
        }
    }
}

Human Readable Output

Nexpose Scan ID 1

Id ScanType ScanName Assets TotalTime Completed Status
1 Manual Tue 03 Dec 2019 10:47 PM 0 51.316 seconds 2019-12-03T20:48:01.368Z finished

Vulnerabilities

Critical Severe Moderate Total
0 0 0 0

nexpose-get-asset-vulnerability


Returns details and possible remediations for an asset’s vulnerability.

Base Command

nexpose-get-asset-vulnerability

Input

Argument Name Description Required
id ID of an asset to search for the vulnerability. Required
vulnerabilityId ID of a vulnerability to search for. Example: 7-zip-cve-2008-6536. Required

Context Output

Path Type Description
Nexpose.Asset.AssetId number Identifier of the asset.
Nexpose.Asset.Vulnerability.Id number The identifier of the vulnerability.
Nexpose.Asset.Vulnerability.Title string The title (summary) of the vulnerability.
Nexpose.Asset.Vulnerability.Severity string The severity of the vulnerability, one of: “Moderate”, “Severe”, “Critical”.
Nexpose.Asset.Vulnerability.RiskScore number The risk score of the vulnerability, rounded to a maximum of to digits of precision. If using the default Rapid7 Real Risk™ model, this value ranges from 0-1000.
Nexpose.Asset.Vulnerability.CVSS string The CVSS vector(s) for the vulnerability.
Nexpose.Asset.Vulnerability.CVSSV3 string The CVSS v3 vector.
Nexpose.Asset.Vulnerability.Published date The date the vulnerability was first published or announced. The format is an ISO 8601 date, YYYY-MM-DD.
Nexpose.Asset.Vulnerability.Added date The date the vulnerability coverage was added. The format is an ISO 8601 date, YYYY-MM-DD.
Nexpose.Asset.Vulnerability.Modified date The last date the vulnerability was modified. The format is an ISO 8601 date, YYYY-MM-DD.
Nexpose.Asset.Vulnerability.CVSSScore number The CVSS score (ranges from 0-10).
Nexpose.Asset.Vulnerability.CVSSV3Score number The CVSS3 score, which ranges from 0-10.
Nexpose.Asset.Vulnerability.Categories unknown All vulnerability categories assigned to this vulnerability.
Nexpose.Asset.Vulnerability.CVES unknown All CVEs assigned to this vulnerability.
Nexpose.Asset.Vulnerability.Check.Port number The port of the service the result was discovered on.
Nexpose.Asset.Vulnerability.Check.Protocol string The protocol of the service the result was discovered on, valid values ip, icmp, igmp, ggp, tcp, pup, udp, idp, esp, nd, raw
Nexpose.Asset.Vulnerability.Check.Since date The date and time the result was first recorded, in the ISO8601 format. If the result changes status this value is the date and time of the status change.
Nexpose.Asset.Vulnerability.Check.Proof string The proof explaining why the result was found vulnerable.
Nexpose.Asset.Vulnerability.Check.Status string The status of the vulnerability check result. Valid values are, unknown, not-vulnerable, vulnerable, vulnerable-version, vulnerable-potential, vulnerable-with-exception-applied, vulnerable-version-with-exception-applied, vulnerable-potential-with-exception-applied
Nexpose.Asset.Vulnerability.Solution.Type string The type of the solution. One of: “Configuration”, “Rollup patch”, “Patch”.
Nexpose.Asset.Vulnerability.Solution.Summary string The summary of the solution.
Nexpose.Asset.Vulnerability.Solution.Steps string The steps required to remediate the vulnerability.
Nexpose.Asset.Vulnerability.Solution.Estimate string The estimated duration to apply the solution, in minutes.
Nexpose.Asset.Vulnerability.Solution.AdditionalInformation string Additional information or resources that can assist in applying the remediation
CVE.ID string Common Vulnerabilities and Exposures IDs.

Command example

!nexpose-get-asset-vulnerability id=1 vulnerabilityId=apache-httpd-cve-2017-15710

Context Example

{
    "CVE": {
        "CVSS": {
            "Score": 7.5,
            "Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
            "Version": "3"
        },
        "Description": "The affected asset is vulnerable to this vulnerability ONLY if it is running one of the following modules: mod_authnz_ldap. Review your web server configuration for validation. mod_authnz_ldap, if configured with AuthLDAPCharsetConfig, uses the Accept-Language header value to lookup the right charset encoding when verifying the user's credentials. If the header value is not present in the charset conversion table, a fallback mechanism is used to truncate it to a two characters value to allow a quick retry (for example, 'en-US' is truncated to 'en'). A header value of less than two characters forces an out of bound write of one NUL byte to a memory location that is not part of the string. In the worst case, quite unlikely, the process would crash which could be used as a Denial of Service attack. In the more likely case, this memory is already reserved for future use and the issue has no effect at all.",
        "ID": "CVE-2017-15710",
        "Modified": "2020-01-30",
        "Published": "2018-03-26"
    },
    "DBotScore": {
        "Indicator": "CVE-2017-15710",
        "Score": 0,
        "Type": "cve",
        "Vendor": "Rapid7 Nexpose"
    },
    "Nexpose": {
        "Asset": {
            "AssetId": "1",
            "Vulnerability": [
                {
                    "Added": "2018-03-26",
                    "CVES": [
                        "CVE-2017-15710"
                    ],
                    "CVSS": "AV:N/AC:L/Au:N/C:N/I:N/A:P",
                    "CVSSScore": 5,
                    "CVSSV3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
                    "CVSSV3Score": 7.5,
                    "Categories": [
                        "Apache",
                        "Apache HTTP Server",
                        "Denial of Service",
                        "LDAP",
                        "Web"
                    ],
                    "Check": [
                        {
                            "Port": 80,
                            "Proof": "Running HTTP serviceProduct HTTPD exists -- Apache HTTPD 2.4.29Vulnerable version of product HTTPD found -- Apache HTTPD 2.4.29",
                            "Protocol": "tcp",
                            "Since": "2020-10-01T22:40:08.844Z",
                            "Status": "vulnerable-version"
                        },
                        {
                            "Port": 8000,
                            "Proof": "Running HTTP serviceProduct HTTPD exists -- Apache HTTPD 2.4.29Vulnerable version of product HTTPD found -- Apache HTTPD 2.4.29",
                            "Protocol": "tcp",
                            "Since": "2020-10-01T22:40:08.844Z",
                            "Status": "vulnerable-version"
                        }
                    ],
                    "Id": "apache-httpd-cve-2017-15710",
                    "Modified": "2020-01-30",
                    "Published": "2018-03-26",
                    "RiskScore": 175.22,
                    "Severity": "Severe",
                    "Solution": [
                        {
                            "AdditionalInformation": "The latest version of Apache HTTPD is 2.4.48.\n\nMany platforms and distributions provide pre-built binary packages for Apache HTTP server. These pre-built packages are usually customized and optimized for a particular distribution, therefore we recommend that you use the packages if they are available for your operating system.",
                            "Estimate": "2 hours",
                            "Steps": "Download and apply the upgrade from: http://archive.apache.org/dist/httpd/httpd-2.4.48.tar.gz (http://archive.apache.org/dist/httpd/httpd-2.4.48.tar.gz)",
                            "Summary": "Upgrade to the latest version of Apache HTTPD",
                            "Type": "rollup-patch"
                        }
                    ],
                    "Title": "Apache HTTPD: Out of bound write in mod_authnz_ldap when using too small Accept-Language values (CVE-2017-15710)"
                }
            ]
        }
    }
}

Human Readable Output

Vulnerability apache-httpd-cve-2017-15710

Id Title Severity RiskScore CVSS CVSSV3 Published Added Modified CVSSScore CVSSV3Score Categories CVES
apache-httpd-cve-2017-15710 Apache HTTPD: Out of bound write in mod_authnz_ldap when using too small Accept-Language values (CVE-2017-15710) Severe 175.22 AV:N/AC:L/Au:N/C:N/I:N/A:P CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H 2018-03-26 2018-03-26 2020-01-30 5.0 7.5 Apache,
Apache HTTP Server,
Denial of Service,
LDAP,
Web
CVE-2017-15710

Checks

Port Protocol Since Proof Status
80 tcp 2020-10-01T22:40:08.844Z Running HTTP serviceProduct HTTPD exists – Apache HTTPD 2.4.29Vulnerable version of product HTTPD found – Apache HTTPD 2.4.29 vulnerable-version
8000 tcp 2020-10-01T22:40:08.844Z Running HTTP serviceProduct HTTPD exists – Apache HTTPD 2.4.29Vulnerable version of product HTTPD found – Apache HTTPD 2.4.29 vulnerable-version

Solutions

Type Summary Steps Estimate AdditionalInformation
rollup-patch Upgrade to the latest version of Apache HTTPD Download and apply the upgrade from: http://archive.apache.org/dist/httpd/httpd-2.4.48.tar.gz (http://archive.apache.org/dist/httpd/httpd-2.4.48.tar.gz) 2 hours The latest version of Apache HTTPD is 2.4.48.

Many platforms and distributions provide pre-built binary packages for Apache HTTP server. These pre-built packages are usually customized and optimized for a particular distribution, therefore we recommend that you use the packages if they are available for your operating system.

nexpose-create-shared-credential


Create a new shared credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSharedCredential

Base Command

nexpose-create-shared-credential

Input

Argument Name Description Required
name Name of the credential. Required
site_assignment Site assignment configuration for the credential. Assign the shared scan credential either to be available to all sites, or a specific list of sites. Possible values are: All-Sites, Specific-Sites. Required
service Credential service type. Possible values are: AS400, CIFS, CIFSHash, CVS, DB2, FTP, HTTP, MS-SQL, MySQL, Notes, Oracle, POP, PostgresSQL, Remote-Exec, SNMP, SNMPv3, SSH, SSH-Key, Sybase, Telnet. Required
database Database name. Optional
description Description for the credential. Optional
domain Domain address. Optional
host_restriction Hostname or IP address to restrict the credentials to. Optional
http_realm HTTP realm. Optional
notes_id_password Password for the notes account that will be used for authenticating. Optional
ntlm_hash NTLM password hash. Optional
oracle_enumerate_sids Whether the scan engine should attempt to enumerate SIDs from the environment. Possible values are: true, false. Optional
oracle_listener_password Oracle Net Listener password. Used to enumerate SIDs from your environment. Optional
oracle_sid Oracle database name. Optional
password Password for the credential. Optional
port_restriction Further restricts the credential to attempt to authenticate on a specific port. Can be used only if host_restriction is used. Optional
sites List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. Optional
community_name SNMP community for authentication. Optional
authentication_type SNMPv3 authentication type for the credential. Possible values are: No-Authentication, MD5, SHA. Optional
privacy_password SNMPv3 privacy password to use. Optional
privacy_type SNMPv3 Privacy protocol to use. Possible values are: No-Privacy, DES, AES-128, AES-192, AES-192-With-3-DES-Key-Extension, AES-256, AES-256-With-3-DES-Key-Extension. Optional
ssh_key_pem PEM formatted private key. Optional
ssh_permission_elevation Elevation type to use for scans. Possible values are: None, sudo, sudosu, su, pbrun, Privileged-Exec. Optional
ssh_permission_elevation_password Password to use for elevation. Optional
ssh_permission_elevation_username Username to use for elevation. Optional
ssh_private_key_password Password for the private key. Optional
use_windows_authentication Whether to use Windows authentication. Possible values are: true, false. Optional
username Username for the credential. Optional

Context Output

Path Type Description
Nexpose.SharedCredential.id number ID of the generated credential.

nexpose-create-site


Creates a new site with the specified configuration.

Base Command

nexpose-create-site

Input

Argument Name Description Required
name Site name. Must be unique. Required
description Site’s description. Optional
assets Addresses of assets to include in site scans. Can be a comma-separated list. Required
scanTemplateId ID of a scan template to use. If not specified, the default scan template will be used. Use nexpose-get-report-templates to get a list of all available templates. Optional
importance Site importance. Defaults to “normal” if not specified. Possible values are: very_low, low, normal, high, very_high. Optional

Context Output

Path Type Description
Nexpose.Site.Id number ID of the created site.

nexpose-create-vulnerability-exception


Create a new vulnerability exception.

Base Command

nexpose-create-vulnerability-exception

Input

Argument Name Description Required
expires The date and time the vulnerability exception is set to expire in ISO 8601 date format. Optional
vulnerability_id ID of the vulnerability to create the exception for. Example: 7-zip-cve-2008-6536. Required
scope_type The type of the exception scope. If set to anything other than Global, scope_id parameter is required. Possible values are: Global, Site, Asset, Asset Group. Required
state State of the vulnerability exception. Possible values are: Expired, Approved, Rejected, Under Review. Required
comment A comment from the submitter as to why the exception was submitted. Optional
reason Reason why the vulnerability exception was submitted. Possible values are: False Positive, Compensating Control, Acceptable Use, Acceptable Risk, Other. Required
scope_id ID of the chosen scope_type (site ID, asset ID, etc.). Required if scope_type is anything other than Global. Optional

Context Output

Path Type Description
Nexpose.VulnerabilityException.id number ID of the generated vulnerability exception.

nexpose-delete-asset


Delete an asset.

Base Command

nexpose-delete-asset

Input

Argument Name Description Required
id ID of the asset to delete. Required

Context Output

There is no context output for this command.

Command example

!nexpose-delete-asset id=1

Human Readable Output

Asset 1 has been deleted.

nexpose-delete-scan-schedule


Delete a scheduled scan.

Base Command

nexpose-delete-scan-schedule

Input

Argument Name Description Required
site_id ID of the site to delete. Optional
site_name Name of the site to delete (can be used instead of site_id). Optional
schedule_id ID of the scheduled scan to delete. Required

Context Output

There is no context output for this command.

nexpose-delete-shared-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Delete a shared credential.

Base Command

nexpose-delete-shared-credential

Input

Argument Name Description Required
id ID of the shared credential to delete. Required

Context Output

There is no context output for this command.

nexpose-delete-site-scan-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Delete a site scan credential.

Base Command

nexpose-delete-site-scan-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
credential_id ID of the site scan credential to delete. Required

Context Output

There is no context output for this command.

nexpose-delete-site


Deletes a site.

Base Command

nexpose-delete-site

Input

Argument Name Description Required
id ID of a site to delete. Optional
site_name Name of the site to delete (can be used instead of site_id). Optional

Context Output

There is no context output for this command.

nexpose-delete-vulnerability-exception


Delete a vulnerability exception.

Base Command

nexpose-delete-vulnerability-exception

Input

Argument Name Description Required
id ID of the vulnerability exception to delete. Required

Command example

!nexpose-delete-vulnerability-exception id=1

Human Readable Output

Vulnerability exception with ID 1 has been deleted.

nexpose-get-sites


Retrieves accessible sites.

Base Command

nexpose-get-sites

Input

Argument Name Description Required
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit A number of records to limit the response to. Default is 10. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;”. For example: “riskScore,DESC;hostName,ASC”. Optional

Context Output

Path Type Description
Nexpose.Site.Id number The identifier of the site.
Nexpose.Site.Name string The site name.
Nexpose.Site.Assets number The number of assets that belong to the site.
Nexpose.Site.Type string The type of the site. Valid values are agent, dynamic, static
Nexpose.Site.Vulnerabilities number The total number of vulnerabilities.
Nexpose.Site.Risk number The risk score (with criticality adjustments) of the site.
Nexpose.Site.LastScan date The date and time of the site’s last scan.

Command example

!nexpose-get-sites limit=5

Context Example

{
    "Nexpose": {
        "Site": [
            {
                "Assets": 4,
                "Id": 1,
                "LastScan": "2021-08-03T14:09:15.321Z",
                "Name": "Authenticated-Assets",
                "Risk": 20416,
                "Type": "static",
                "Vulnerabilities": 41
            },
            {
                "Assets": 18,
                "Id": 2,
                "LastScan": "2021-06-29T07:06:54.733Z",
                "Name": "PANW",
                "Risk": 213245,
                "Type": "static",
                "Vulnerabilities": 455
            },
            {
                "Assets": 10,
                "Id": 3,
                "LastScan": "2020-11-26T17:13:54.117Z",
                "Name": "Test",
                "Risk": 18820,
                "Type": "static",
                "Vulnerabilities": 45
            }
        ]
    }
}

Human Readable Output

Nexpose Sites

Id Name Assets Vulnerabilities Risk Type LastScan
1 Authenticated-Assets 4 41 20416.0 static 2021-08-03T14:09:15.321Z
2 PANW 18 455 213245.0 static 2021-06-29T07:06:54.733Z
3 Test 10 45 18820.0 static 2020-11-26T17:13:54.117Z

nexpose-get-report-templates


Returns all available report templates.

Base Command

nexpose-get-report-templates

Input

There are no input arguments for this command.

Context Output

Path Type Description
Nexpose.Template.Id number The identifier of the report template.
Nexpose.Template.Name string The name of the report template.
Nexpose.Template.Description string The description of the report template.
Nexpose.Template.Type string The type of the report template. document is a templatized, typically printable, report that has various sections of content. export is data-oriented output, typically CSV. file is a printable report template using a report template file.

Command example


#### Context Example

```json
{
    "Nexpose": {
        "Template": [
            {
                "Description": "Provides comprehensive details about discovered assets, vulnerabilities, and users.",
                "Id": "audit-report",
                "Name": "Audit Report",
                "Type": "document"
            },
            {
                "Description": "Compares current scan results to those of an earlier baseline scan.",
                "Id": "baseline-comparison",
                "Name": "Baseline Comparison",
                "Type": "document"
            },
            {
                "Description": "Provides a high-level view of security data, including general results information and statistical charts.",
                "Id": "executive-overview",
                "Name": "Executive Overview",
                "Type": "document"
            },
            {
                "Description": "Provides information and metrics about 10 discovered vulnerabilities with the highest risk scores.",
                "Id": "highest-risk-vulns",
                "Name": "Highest Risk Vulnerabilities",
                "Type": "document"
            },
            {
                "Description": "Lists results for standard policy scans (AS/400, Oracle, Domino, Windows Group, CIFS/SMB account). Does not include Policy Manager results.",
                "Id": "policy-eval",
                "Name": "Policy Evaluation",
                "Type": "document"
            },
            {
                "Description": "Provides detailed remediation instructions for each discovered vulnerability.",
                "Id": "remediation-plan",
                "Name": "Remediation Plan",
                "Type": "document"
            },
            {
                "Description": "Lists test results for each discovered vulnerability, including how it was verified.",
                "Id": "report-card",
                "Name": "Report Card",
                "Type": "document"
            }
        ]
    }
}

Human Readable Output

Nexpose Templates

Id Name Description Type
audit-report Audit Report Provides comprehensive details about discovered assets, vulnerabilities, and users. document
baseline-comparison Baseline Comparison Compares current scan results to those of an earlier baseline scan. document
executive-overview Executive Overview Provides a high-level view of security data, including general results information and statistical charts. document
highest-risk-vulns Highest Risk Vulnerabilities Provides information and metrics about 10 discovered vulnerabilities with the highest risk scores. document
policy-eval Policy Evaluation Lists results for standard policy scans (AS/400, Oracle, Domino, Windows Group, CIFS/SMB account). Does not include Policy Manager results. document
remediation-plan Remediation Plan Provides detailed remediation instructions for each discovered vulnerability. document
report-card Report Card Lists test results for each discovered vulnerability, including how it was verified. document

nexpose-create-asset


Create a new asset.

Base Command

nexpose-create-asset

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
date The date the data was collected on the asset in ISO 8601 format. Required
ip Primary IPv4 or IPv6 address of the asset. Required
host_name Hostname of the asset. Optional
host_name_source The source used to detect the host name. “User” indicates the host name source is user-supplied. Possible values are: User, DNS, NetBIOS, DCE, EPSEC, LDAP, Other. Optional

Context Output

Path Type Description
Nexpose.Asset.id string ID of the newly created asset.

Command example

!nexpose-create-asset site_id="1" date="2022-01-01T10:00:00Z" ip="192.0.2.0"

Context Example

{
    "Nexpose": {
        "Asset": {
            "id": 1
        }
    }
}

Human Readable Output

New asset has been created with ID 1.

nexpose-create-assets-report


Generates a new report on given assets according to a template and arguments.

Base Command

nexpose-create-assets-report

Input

Argument Name Description Required
assets Asset IDs to create the report on. Can be a comma-separated list. Required
template Report template ID to create the report with. If not provided, the first available template will be used. Optional
name Report name. Optional
format Report format (uses PDF by default). Possible values are: pdf, rtf, xml, html, text. Optional
download_immediately Whether to download the report immediately after the report is generated. Defaults to “true”. If the report takes longer than 10 seconds to generate, set to “false”. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
InfoFile.EntryId string Entry ID of the report file.
InfoFile.Name string Name of the report file.
InfoFile.Extension string File extension of the report file.
InfoFile.Info string Information about the report file.
InfoFile.Size number Size of the report file (in bytes).
InfoFile.Type string Type of the report file.
Nexpose.Report.ID string The identifier of the report.
Nexpose.Report.InstanceID string The identifier of the report instance.
Nexpose.Report.Name string The report name.
Nexpose.Report.Format string The report format.

nexpose-create-sites-report


Generates a new report on given sites according to a template and arguments.

Base Command

nexpose-create-sites-report

Input

Argument Name Description Required
sites Site IDs to create the report on. Can be a comma-separated list. Optional
site_names Names of sites to create the report on. Can be a comma-separated list. Optional
template Report template ID to use for report’s creation. If not provided, the first available template will be used. Optional
name Report name. Optional
format Report format (uses PDF by default). Possible values are: pdf, rtf, xml, html, text. Optional
download_immediately If true, downloads the report immediately after the report is generated. The default is “true”. If the report takes longer than 10 seconds to generate, set to “false”. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
InfoFile.EntryId string Entry ID of the report file.
InfoFile.Name string Name of the report file.
InfoFile.Extension string File extension of the report file.
InfoFile.Info string Info about the report file.
InfoFile.Size number Size of the report file.
InfoFile.Type string Type of the report file.
Nexpose.Report.ID string The identifier of the report.
Nexpose.Report.InstanceID string The identifier of the report instance.
Nexpose.Report.Name string The report name.
Nexpose.Report.Format string The report format.

nexpose-create-site-scan-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Create a new site scan credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteCredential

Base Command

nexpose-create-site-scan-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
name Name of the credential. Required
service Credential service type. Possible values are: AS400, CIFS, CIFSHash, CVS, DB2, FTP, HTTP, MS-SQL, MySQL, Notes, Oracle, POP, PostgresSQL, Remote-Exec, SNMP, SNMPv3, SSH, SSH-Key, Sybase, Telnet. Required
database Database name. Optional
description Description for the credential. Optional
domain Domain address. Optional
host_restriction Hostname or IP address to restrict the credentials to. Optional
http_realm HTTP realm. Optional
notes_id_password Password for the notes account that will be used for authenticating. Optional
ntlm_hash NTLM password hash. Optional
oracle_enumerate_sids Whether the scan engine should attempt to enumerate SIDs from the environment. Possible values are: true, false. Optional
oracle_listener_password Oracle Net Listener password. Used to enumerate SIDs from your environment. Optional
oracle_sid Oracle database name. Optional
password Password for the credential. Optional
port_restriction Further restricts the credential to attempt to authenticate on a specific port. Can be used only if host_restriction is used. Optional
community_name SNMP community for authentication. Optional
authentication_type SNMPv3 authentication type for the credential. Possible values are: No-Authentication, MD5, SHA. Optional
privacy_password SNMPv3 privacy password to use. Optional
privacy_type SNMPv3 privacy protocol to use. Possible values are: No-Privacy, DES, AES-128, AES-192, AES-192-With-3-DES-Key-Extension, AES-256, AES-256-With-3-DES-Key-Extension. Optional
ssh_key_pem PEM formatted private key. Optional
ssh_permission_elevation Elevation type to use for scans. Possible values are: None, sudo, sudosu, su, pbrun, Privileged-Exec. Optional
ssh_permission_elevation_password Password to use for elevation. Optional
ssh_permission_elevation_username Username to use for elevation. Optional
ssh_private_key_password Password for the private key. Optional
use_windows_authentication Whether to use Windows authentication. Possible values are: true, false. Optional
username Username for the credential. Optional

Context Output

Path Type Description
Nexpose.SiteScanCredential.id number ID of the generated credential.

nexpose-create-scan-report


Generates a new report for a specified scan.

Base Command

nexpose-create-scan-report

Input

Argument Name Description Required
scan ID of the scan to create a report about. Required
template Report template ID to use for creation. If not provided, the first available template will be used. Optional
name Report name. Optional
format Report format (uses PDF by default). Possible values are: pdf, rtf, xml, html, text. Optional
download_immediately If true, downloads the report immediately after the report is generated. The default is “true”. If the report takes longer than 10 seconds to generate, set to “false”. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
InfoFile.EntryId string Entry ID of the report file.
InfoFile.Name string Name of the report file.
InfoFile.Extension string File extension of the report file.
InfoFile.Info string Info about the report file.
InfoFile.Size number Size of the report file.
InfoFile.Type string Type of the report file.
Nexpose.Report.ID string The identifier of the report.
Nexpose.Report.InstanceID string The identifier of the report instance.
Nexpose.Report.Name string The report name.
Nexpose.Report.Format string The report format.

Command example

!nexpose-create-scan-report scan=1 download_immediately=false

Context Example

{
    "Nexpose": {
        "Report": {
            "Format": "pdf",
            "ID": 3241,
            "InstanceID": 3212,
            "Name": "report 2022-11-30 09:25:36.359529"
        }
    }
}

Human Readable Output

Report Information

Format ID InstanceID Name
pdf 3241 3212 report 2022-11-30 09:25:36.359529

nexpose-create-scan-schedule


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Create a new site scan schedule.

Base Command

nexpose-create-scan-schedule

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
enabled Whether to enable the scheduled scan after creation. Possible values are: True, False. Default is True. Optional
on_scan_repeat The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. Possible values are: Restart-Scan, Resume-Scan. Required
start The scheduled start date and time formatted in ISO 8601 format. Repeating schedules will determine the next schedule to begin based on this date and time. Required
excluded_asset_group_ids A list of IDs for asset groups to exclude from the scan. Optional
excluded_addresses A list of addresses to exclude from the scan. Optional
included_asset_group_ids A list of IDs for asset groups to include in the scan. Optional
included_addresses A list of addresses to include in the scan. Optional
duration_days Maximum duration of the scan in days. Optional
duration_hours Maximum duration of the scan in hours. Optional
duration_minutes Maximum duration of the scan in minutes. Optional
frequency How frequently the schedule should repeat (Every…). Possible values are: Hour, Day, Week, Date-of-month. Optional
interval_time The interval time the schedule should repeat. This depends on the value set in frequency. For example, if the value of frequency is set to “Day” and interval is set to 2, then the schedule will repeat every 2 days. Required only if frequency is used. Optional
date_of_month Specifies the schedule repeat day of the interval month. For example, if date_of_month is 17 and interval is set to 2, then the schedule will repeat every 2 months on the 17th day of the month. Required and used only if frequency is set to Date of month. Optional
scan_name A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. Optional
scan_template ID of the scan template to use. Optional

Context Output

Path Type Description
Nexpose.ScanSchedule.id int ID of the newly created scan schedule.

nexpose-list-assigned-shared-credential


Retrieve information about shared credentials for a specific site.

Base Command

nexpose-list-assigned-shared-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
limit The number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.AssignedSharedCredential.enabled string Flag indicating whether the shared credential is enabled for the site’s scans.
Nexpose.AssignedSharedCredential.id string ID of the shared credential.
Nexpose.AssignedSharedCredential.name string The name of the shared credential.
Nexpose.AssignedSharedCredential.service string Credential service type.

Command example

!nexpose-list-assigned-shared-credential site_id=1 limit=3

Context Example

{
    "Nexpose": {
        "AssignedSharedCredential": [
            {
                "enabled": true,
                "id": 1,
                "name": "Test 1",
                "service": "ftp"
            },
            {
                "enabled": true,
                "id": 2,
                "name": "Test 2",
                "service": "ftp"
            },
            {
                "enabled": true,
                "id": 3,
                "name": "Test 3",
                "service": "ftp"
            }
        ]
    }
}

Human Readable Output

Nexpose Assigned Shared Credentials

Id Name Service Enabled
1 Test 1 ftp true
2 Test 2 ftp true
3 Test 3 ftp true

nexpose-list-vulnerability


Retrieve information about all or a specific vulnerability.

Base Command

nexpose-list-vulnerability

Input

Argument Name Description Required
id ID of a specific vulnerability to retrieve. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit The number of records to limit the response to. Default is 10. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;”. For example: “riskScore,DESC;hostName,ASC”. Optional

Context Output

Path Type Description
Nexpose.Vulnerability.added string The date the vulnerability coverage was added in ISO 8601 format.
Nexpose.Vulnerability.categories array All vulnerability categories assigned to this vulnerability.
Nexpose.Vulnerability.cves array All CVEs assigned to this vulnerability.
Nexpose.Vulnerability.cvss.v2.accessComplexity string Access Complexity (AC) component that measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system.
Nexpose.Vulnerability.cvss.v2.accessVector string Access Vector (Av) component that reflects how the vulnerability is exploited.
Nexpose.Vulnerability.cvss.v2.authentication string Authentication (Au) component that measures the number of times an attacker must authenticate to a target in order to exploit a vulnerability.
Nexpose.Vulnerability.cvss.v2.availabilityImpact string Availability Impact (A) component that measures the impact to availability of a successfully exploited vulnerability.
Nexpose.Vulnerability.cvss.v2.confidentialityImpact string Confidentiality Impact (C) component that measures the impact on confidentiality of a successfully exploited vulnerability.
Nexpose.Vulnerability.cvss.v2.exploitScore number The CVSS exploit score.
Nexpose.Vulnerability.cvss.v2.impactScore number The CVSS impact score.
Nexpose.Vulnerability.cvss.v2.integrityImpact string Integrity Impact (I) component that measures the impact to integrity of a successfully exploited vulnerability.
Nexpose.Vulnerability.cvss.v2.score number The CVSS score (ranges from 0-10).
Nexpose.Vulnerability.cvss.v2.vector string The CVSS v2 vector.
Nexpose.Vulnerability.cvss.v3.attackComplexity string Access Complexity (AC) component that measures the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability.
Nexpose.Vulnerability.cvss.v3.attackVector string Attack Vector (AV) component that measures context by which vulnerability exploitation is possible.
Nexpose.Vulnerability.cvss.v3.availabilityImpact string Availability Impact (A) that measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability.
Nexpose.Vulnerability.cvss.v3.confidentialityImpact string Confidentiality Impact (C) component that measures the impact on confidentiality of a successfully exploited vulnerability.
Nexpose.Vulnerability.cvss.v3.exploitScore number The CVSS impact score.
Nexpose.Vulnerability.cvss.v3.impactScore number The CVSS exploit score.
Nexpose.Vulnerability.cvss.v3.integrityImpact string Integrity Impact (I) that measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information.
Nexpose.Vulnerability.cvss.v3.privilegeRequired string Privileges Required (PR) that measures the level of privileges an attacker must possess before successfully exploiting the vulnerability.
Nexpose.Vulnerability.cvss.v3.scope string Scope (S) that measures the collection of privileges defined by a computing authority (e.g., an application, an operating system, or a sandbox environment) when granting access to computing resources (e.g., files, CPU, memory, etc.). These privileges are assigned based on some method of identification and authorization.
Nexpose.Vulnerability.cvss.v3.score number The CVSS score (ranges from 0-10).
Nexpose.Vulnerability.cvss.v3.userInteraction string User Interaction (UI) that measures the requirement for a user, other than the attacker, to participate in the successful compromise of the vulnerable component.
Nexpose.Vulnerability.cvss.v3.vector string The CVSS v3 vector.
Nexpose.Vulnerability.denialOfService boolean Whether the vulnerability can lead to Denial of Service (DoS).
Nexpose.Vulnerability.description.html string Hypertext Markup Language (HTML) representation of the content.
Nexpose.Vulnerability.description.text string Textual representation of the content.
Nexpose.Vulnerability.exploits number The exploits that can be used to exploit a vulnerability.
Nexpose.Vulnerability.id string The identifier of the vulnerability.
Nexpose.Vulnerability.malwareKits number The malware kits that are known to be used to exploit the vulnerability.
Nexpose.Vulnerability.modified string The last date the vulnerability was modified in ISO 8601 format.
Nexpose.Vulnerability.pci.adjustedCVSSScore number The CVSS score of the vulnerability, adjusted for PCI rules and exceptions, on a scale of 0-10.
Nexpose.Vulnerability.pci.adjustedSeverityScore number The severity score of the vulnerability, adjusted for PCI rules and exceptions, on a scale of 0-10.
Nexpose.Vulnerability.pci.fail boolean Whether, if present on a host, this vulnerability would cause a PCI failure. True if “status” is “Fail”, false otherwise.
Nexpose.Vulnerability.pci.specialNotes string Any special notes or remarks about the vulnerability that pertain to PCI compliance.
Nexpose.Vulnerability.pci.status string The PCI compliance status of the vulnerability. Can be either “Pass”, or “Fail”.
Nexpose.Vulnerability.published string The date the vulnerability was first published or announced in ISO 8601 format.
Nexpose.Vulnerability.riskScore number The risk score of the vulnerability, rounded to a maximum of two digits of precision. If using the default Rapid7 Real Risk model, this value ranges from 0-1000.
Nexpose.Vulnerability.severity string The severity of the vulnerability, can be either “Moderate”, “Severe”, or “Critical”.
Nexpose.Vulnerability.severityScore number The severity score of the vulnerability, on a scale of 0-10.
Nexpose.Vulnerability.title string The title (summary) of the vulnerability.

Command example

!nexpose-list-vulnerability limit=3

Context Example

{
    "Nexpose": {
        "Vulnerability": [
            {
                "added": "2018-05-16",
                "categories": [
                    "7-Zip"
                ],
                "cves": [
                    "CVE-2008-6536"
                ],
                "cvss": {
                    "v2": {
                        "accessComplexity": "L",
                        "accessVector": "N",
                        "authentication": "N",
                        "availabilityImpact": "C",
                        "confidentialityImpact": "C",
                        "exploitScore": 9.9968,
                        "impactScore": 10.0008,
                        "integrityImpact": "C",
                        "score": 10,
                        "vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C"
                    }
                },
                "denialOfService": false,
                "description": {
                    "html": "<p>Unspecified vulnerability in 7-zip before 4.5.7 has unknown impact and remote attack vectors, as demonstrated by the PROTOS GENOME test suite for Archive Formats (c10).</p>",
                    "text": "Unspecified vulnerability in 7-zip before 4.5.7 has unknown impact and remote attack vectors, as demonstrated by the PROTOS GENOME test suite for Archive Formats (c10)."
                },
                "exploits": 0,
                "id": "7-zip-cve-2008-6536",
                "malwareKits": 0,
                "modified": "2018-06-08",
                "pci": {
                    "adjustedCVSSScore": 10,
                    "adjustedSeverityScore": 5,
                    "fail": true,
                    "status": "Fail"
                },
                "published": "2009-03-29",
                "riskScore": 898.63,
                "severity": "Critical",
                "severityScore": 10,
                "title": "7-Zip: CVE-2008-6536: Unspecified vulnerability in 7-zip before 4.5.7"
            },
            {
                "added": "2018-05-16",
                "categories": [
                    "7-Zip",
                    "Remote Execution"
                ],
                "cves": [
                    "CVE-2016-2334"
                ],
                "cvss": {
                    "v2": {
                        "accessComplexity": "M",
                        "accessVector": "N",
                        "authentication": "N",
                        "availabilityImpact": "C",
                        "confidentialityImpact": "C",
                        "exploitScore": 8.5888,
                        "impactScore": 10.0008,
                        "integrityImpact": "C",
                        "score": 9.3,
                        "vector": "AV:N/AC:M/Au:N/C:C/I:C/A:C"
                    },
                    "v3": {
                        "attackComplexity": "L",
                        "attackVector": "L",
                        "availabilityImpact": "H",
                        "confidentialityImpact": "H",
                        "exploitScore": 1.8346,
                        "impactScore": 5.8731,
                        "integrityImpact": "H",
                        "privilegeRequired": "N",
                        "scope": "U",
                        "score": 7.8,
                        "userInteraction": "R",
                        "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
                    }
                },
                "denialOfService": false,
                "description": {
                    "html": "<p>Heap-based buffer overflow in the NArchive::NHfs::CHandler::ExtractZlibFile method in 7zip before 16.00 and p7zip allows remote attackers to execute arbitrary code via a crafted HFS+ image.</p>",
                    "text": "Heap-based buffer overflow in the NArchive::NHfs::CHandler::ExtractZlibFile method in 7zip before 16.00 and p7zip allows remote attackers to execute arbitrary code via a crafted HFS+ image."
                },
                "exploits": 0,
                "id": "7-zip-cve-2016-2334",
                "malwareKits": 0,
                "modified": "2018-06-08",
                "pci": {
                    "adjustedCVSSScore": 9,
                    "adjustedSeverityScore": 5,
                    "fail": true,
                    "status": "Fail"
                },
                "published": "2016-12-13",
                "riskScore": 717.53,
                "severity": "Critical",
                "severityScore": 9,
                "title": "7-Zip: CVE-2016-2334: Heap-based buffer overflow vulnerability"
            },
            {
                "added": "2018-05-16",
                "categories": [
                    "7-Zip",
                    "Trojan"
                ],
                "cves": [
                    "CVE-2016-7804"
                ],
                "cvss": {
                    "v2": {
                        "accessComplexity": "M",
                        "accessVector": "N",
                        "authentication": "N",
                        "availabilityImpact": "P",
                        "confidentialityImpact": "P",
                        "exploitScore": 8.5888,
                        "impactScore": 6.443,
                        "integrityImpact": "P",
                        "score": 6.8,
                        "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P"
                    },
                    "v3": {
                        "attackComplexity": "L",
                        "attackVector": "L",
                        "availabilityImpact": "H",
                        "confidentialityImpact": "H",
                        "exploitScore": 1.8346,
                        "impactScore": 5.8731,
                        "integrityImpact": "H",
                        "privilegeRequired": "N",
                        "scope": "U",
                        "score": 7.8,
                        "userInteraction": "R",
                        "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
                    }
                },
                "denialOfService": false,
                "description": {
                    "html": "<p>Untrusted search path vulnerability in 7 Zip for Windows 16.02 and earlier allows remote attackers to gain privileges via a Trojan horse DLL in an unspecified directory.</p>",
                    "text": "Untrusted search path vulnerability in 7 Zip for Windows 16.02 and earlier allows remote attackers to gain privileges via a Trojan horse DLL in an unspecified directory."
                },
                "exploits": 0,
                "id": "7-zip-cve-2016-7804",
                "malwareKits": 0,
                "modified": "2018-06-08",
                "pci": {
                    "adjustedCVSSScore": 6,
                    "adjustedSeverityScore": 4,
                    "fail": true,
                    "specialNotes": "The presence of malware, including rootkits, backdoors, or trojan horse programs are a violation of PCI DSS, and result in an automatic failure. ",
                    "status": "Fail"
                },
                "published": "2017-05-22",
                "riskScore": 578.88,
                "severity": "Severe",
                "severityScore": 7,
                "title": "7-Zip: CVE-2016-7804: Untrusted search path vulnerability"
            }
        ]
    }
}

Human Readable Output

Nexpose Vulnerabilities

Title MalwareKits Exploits CVSS CVSSv3 Risk PublishedOn ModifiedOn Severity
7-Zip: CVE-2008-6536: Unspecified vulnerability in 7-zip before 4.5.7 0 0 10.0   898.63 2009-03-29 2018-06-08 Critical
7-Zip: CVE-2016-2334: Heap-based buffer overflow vulnerability 0 0 9.3 7.8 717.53 2016-12-13 2018-06-08 Critical
7-Zip: CVE-2016-7804: Untrusted search path vulnerability 0 0 6.8 7.8 578.88 2017-05-22 2018-06-08 Severe

nexpose-list-scan-schedule


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Retrieve information about scan schedules for a specific site or a specific scan schedule.

Base Command

nexpose-list-scan-schedule

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
schedule_id ID of the scheduled scan (optional, will return a single specific scan if used). Optional
limit A number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.ScanSchedule.assets.excludedAssetGroups.assetGroupIDs array List of asset group identifiers that will be excluded from scans.
Nexpose.ScanSchedule.assets.excludedTargets.addresses array List of addresses that will be excluded from scans.
Nexpose.ScanSchedule.assets.includedAssetGroups.assetGroupIDs array List of asset group identifiers that will be included in scans.
Nexpose.ScanSchedule.assets.includedTargets.addresses array List of addresses that will be included in scans.
Nexpose.ScanSchedule.duration string Specifies in ISO 8601 duration format the maximum duration the scheduled scan is allowed to run.
Nexpose.ScanSchedule.enabled string Flag indicating whether the scan schedule is enabled.
Nexpose.ScanSchedule.id int The identifier of the scan schedule.
Nexpose.ScanSchedule.nextRuntimes array List the next 10 dates when the schedule will launch.
Nexpose.ScanSchedule.onScanRepeat string Specifies the desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching is maximum duration.
Nexpose.ScanSchedule.repeat.dayOfWeek unknown Specifies the desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching is maximum duration.
Nexpose.ScanSchedule.repeat.every unknown The frequency in which the schedule repeats. Each value represents a different unit of time and is used in conjunction with the property interval.
Nexpose.ScanSchedule.repeat.interval unknown The interval time the schedule should repeat. This depends on the value set in every.
Nexpose.ScanSchedule.repeat.weekOfMonth unknown This property only applies to schedules with an every value of “day-of-month”. The week of the month the scheduled task should repeat.
Nexpose.ScanSchedule.repeat.scanEngineId unknown The identifier of the scan engine to be used for this scan schedule. If not set, the site’s assigned scan engine will be used.
Nexpose.ScanSchedule.repeat.scanName unknown A user-defined name for the scan launched by the schedule.
Nexpose.ScanSchedule.repeat.scanTemplateId unknown The identifier of the scan template to be used for this scan schedule. If not set, the site’s assigned scan template will be used.
Nexpose.ScanSchedule.repeat.start unknown The scheduled start date and time. Repeating schedules will determine the next schedule to begin based on this date and time.

nexpose-list-shared-credential


Retrieve information about all or a specific shared credential.

Base Command

nexpose-list-shared-credential

Input

Argument Name Description Required
id ID of a specific shared credential to retrieve. Optional
limit A number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.SharedCredential.account.authenticationType string SNMPv3 authentication type for the credential.
Nexpose.SharedCredential.account.communityName string SNMP community for authentication.
Nexpose.SharedCredential.account.database string Database name.
Nexpose.SharedCredential.account.domain string Domain address.
Nexpose.SharedCredential.account.enumerateSids boolean Whether the scan engine should attempt to enumerate SIDs from the environment.
Nexpose.SharedCredential.account.notesIDPassword string Password for the notes account that will be used for authenticating.
Nexpose.SharedCredential.account.ntlmHash string NTLM password hash.
Nexpose.SharedCredential.account.oracleListenerPassword string The Oracle Net Listener password. Used to enumerate SIDs from the environment.
Nexpose.SharedCredential.account.password string Password for the credential.
Nexpose.SharedCredential.account.pemKey string PEM formatted private key.
Nexpose.SharedCredential.account.permissionElevation string Elevation type to use for scans.
Nexpose.SharedCredential.account.permissionElevationPassword string Password to use for elevation.
Nexpose.SharedCredential.account.permissionElevationUserName string Username to use for elevation.
Nexpose.SharedCredential.account.privacyPassword string SNMPv3 privacy password to use.
Nexpose.SharedCredential.account.privacyType string SNMPv3 privacy protocol to use.
Nexpose.SharedCredential.account.privateKeyPassword string Password for the private key.
Nexpose.SharedCredential.account.realm string HTTP realm.
Nexpose.SharedCredential.account.service string Credential service type.
Nexpose.SharedCredential.account.sid string Oracle database name.
Nexpose.SharedCredential.account.useWindowsAuthentication boolean Whether to use Windows authentication.
Nexpose.SharedCredential.account.username string Username for the credential.
Nexpose.SharedCredential.description string Description for the credential.
Nexpose.SharedCredential.hostRestriction string Hostname or IP address to restrict the credentials to.
Nexpose.SharedCredential.id number ID of the shared credential.
Nexpose.SharedCredential.name string Name of the credential.
Nexpose.SharedCredential.portRestriction number Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `hostRestriction` is used.
Nexpose.SharedCredential.siteAssignment string Site assignment configuration for the credential.
Nexpose.SharedCredential.sites array List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan.

Command example

!nexpose-list-shared-credential limit=3

Context Example

{
    "Nexpose": {
        "SharedCredential": [
            {
                "account": {
                    "authenticationType": "md5",
                    "privacyType": "no-privacy",
                    "service": "snmpv3",
                    "username": "test"
                },
                "id": 1,
                "name": "shared credentials",
                "siteAssignment": "specific-sites",
                "sites": [
                    1
                ]
            },
            {
                "account": {
                    "service": "as400",
                    "username": "test"
                },
                "id": 2,
                "name": "shared credentials",
                "siteAssignment": "specific-sites",
                "sites": [
                    1
                ]
            },
            {
                "account": {
                    "permissionElevation": "sudosu",
                    "permissionElevationUsername": "test",
                    "service": "ssh",
                    "username": "test"
                },
                "id": 3,
                "name": "shared credentials",
                "siteAssignment": "specific-sites",
                "sites": [
                    1
                ]
            }
        ]
    }
}

Human Readable Output

Nexpose Shared Credentials

Id Name Service UserName
1 shared credentials snmpv3 test
2 shared credentials as400 test
3 shared credentials ssh test

nexpose-list-site-scan-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Retrieve information about all or a specific sca credential.

Base Command

nexpose-list-site-scan-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
credential_id ID of a specific scan credential to retrieve. Optional
limit A number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.SiteScanCredential.account.authenticationType string SNMPv3 authentication type for the credential.
Nexpose.SiteScanCredential.account.communityName string SNMP community for authentication.
Nexpose.SiteScanCredential.account.database string Database name.
Nexpose.SiteScanCredential.account.domain string Domain address.
Nexpose.SiteScanCredential.account.enumerateSids boolean Whether the scan engine should attempt to enumerate SIDs from the environment.
Nexpose.SiteScanCredential.account.notesIDPassword string Password for the notes account that will be used for authenticating.
Nexpose.SiteScanCredential.account.ntlmHash string NTLM password hash.
Nexpose.SiteScanCredential.account.oracleListenerPassword string The Oracle Net Listener password. Used to enumerate SIDs from the environment.
Nexpose.SiteScanCredential.account.password string Password for the credential.
Nexpose.SiteScanCredential.account.pemKey string PEM formatted private key.
Nexpose.SiteScanCredential.account.permissionElevation string Elevation type to use for scans.
Nexpose.SiteScanCredential.account.permissionElevationPassword string Password to use for elevation.
Nexpose.SiteScanCredential.account.permissionElevationUserName string Username to use for elevation.
Nexpose.SiteScanCredential.account.privacyPassword string SNMPv3 privacy password to use.
Nexpose.SiteScanCredential.account.privacyType string SNMPv3 privacy protocol to use.
Nexpose.SiteScanCredential.account.privateKeyPassword string Password for the private key.
Nexpose.SiteScanCredential.account.realm string HTTP realm.
Nexpose.SiteScanCredential.account.service string Credential service type.
Nexpose.SiteScanCredential.account.sid string Oracle database name.
Nexpose.SiteScanCredential.account.useWindowsAuthentication boolean Whether to use Windows authentication.
Nexpose.SiteScanCredential.account.username string Username for the credential.
Nexpose.SiteScanCredential.description string Description for the credential.
Nexpose.SiteScanCredential.hostRestriction string Hostname or IP address to restrict the credentials to.
Nexpose.SiteScanCredential.id number ID of the credential.
Nexpose.SiteScanCredential.name string Name of the credential.
Nexpose.SiteScanCredential.portRestriction number Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `hostRestriction` is used.

nexpose-list-vulnerability-exceptions


Retrieve information about scan schedules for a specific site or a specific scan schedule.

Base Command

nexpose-list-vulnerability-exceptions

Input

Argument Name Description Required
id ID of the vulnerability exception to retrieve. If not set, retrieve all vulnerability exceptions. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;”. For example: “riskScore,DESC;hostName,ASC”. Default is submit.date,ASC. Optional
limit A number of records to limit the response to. Default is 10. Optional

Context Output

Path Type Description
Nexpose.VulnerabilityException.expires string The date and time the vulnerability exception is set to expire.
Nexpose.VulnerabilityException.id int The The identifier of the vulnerability exception.
Nexpose.VulnerabilityException.scope.id int The identifier of the vulnerability to which the exception applies.
Nexpose.VulnerabilityException.scope.key string If the scope type is “Instance”, an optional key to discriminate the instance the exception applies to.
Nexpose.VulnerabilityException.scope.port int If the scope type is “Instance” and the vulnerability is detected on a service, the port on which the exception applies.
Nexpose.VulnerabilityException.scope.type string The type of the exception scope. One of: “Global”, “Site”, “Asset”, “Asset Group”, “Instance”.
Nexpose.VulnerabilityException.scope.vulnerability string The identifier of the vulnerability to which the exception applies.
Nexpose.VulnerabilityException.state string The state of the vulnerability exception. One of: “Deleted”, “Expired”, “Approved”, “Rejected”, `“Under Review”.
Nexpose.VulnerabilityException.submit.comment string A comment from the submitter as to why the exception was submitted.
Nexpose.VulnerabilityException.submit.date string The date and time the vulnerability exception was submitted.
Nexpose.VulnerabilityException.submit.name string The login name of the user who submitted the vulnerability exception.
Nexpose.VulnerabilityException.submit.reason string The reason the vulnerability exception was submitted. One of: “False Positive”, “Compensating Control”, “Acceptable Use”, “Acceptable Risk”, “Other”
Nexpose.VulnerabilityException.submit.user int The identifier of the user who submitted the vulnerability exception.

Command example

!nexpose-list-vulnerability-exceptions sort="submit.date,ASC" limit=3

Context Example

{
    "Nexpose": {
        "VulnerabilityException": [
            {
                "expires": "2028-03-01T04:31:56Z",
                "id": 1,
                "review": {
                    "comment": "Auto approved by submitter.",
                    "date": "2022-10-31T14:39:15.736Z",
                    "name": "admin",
                    "user": 1
                },
                "scope": {
                    "type": "global",
                    "vulnerability": "tlsv1_0-enabled"
                },
                "state": "approved",
                "submit": {
                    "date": "2022-06-29T16:10:06.616880Z",
                    "name": "admin",
                    "reason": "false positive",
                    "user": 1
                }
            },
            {
                "id": 2,
                "review": {
                    "date": "2022-10-30T13:54:31.084Z",
                    "name": "admin",
                    "user": 1
                },
                "scope": {
                    "type": "global",
                    "vulnerability": "php-cve-2018-10545"
                },
                "state": "rejected",
                "submit": {
                    "date": "2022-07-13T13:27:31.647402Z",
                    "name": "admin",
                    "reason": "acceptable use",
                    "user": 1
                }
            },
            {
                "id": 3,
                "scope": {
                    "type": "global",
                    "vulnerability": "cifs-smb-signing-disabled"
                },
                "state": "under review",
                "submit": {
                    "date": "2022-10-27T11:40:34.109268Z",
                    "name": "admin",
                    "reason": "acceptable use",
                    "user": 1
                }
            }
        ]
    }
}

Human Readable Output

Nexpose Vulnerability Exceptions

Id Vulnerability ExceptionScope Reason ReportedBy ReviewStatus ReviewedOn ExpiresOn
1 tlsv1_0-enabled global false positive admin approved 2022-10-31T14:39:15.736Z 2028-03-01T04:31:56Z
2 php-cve-2018-10545 global acceptable use admin rejected 2022-10-30T13:54:31.084Z  
3 cifs-smb-signing-disabled global acceptable use admin under review    

nexpose-start-site-scan


Starts a scan for the specified site.

Base Command

nexpose-start-site-scan

Input

Argument Name Description Required
site ID of the site. Optional
site_name Name of the site (can be used instead of site). Optional
hosts Specific host(s) on the site to run the scan on. Can be an IP address or a hostname. Can be a comma-separated list. Optional
name Scan name. Optional

Context Output

Path Type Description
Nexpose.Scan.Id number The identifier of the scan.
Nexpose.Scan.ScanType string The scan type (automated, manual, scheduled).
Nexpose.Scan.StartedBy date The name of the user who started the scan.
Nexpose.Scan.Assets number The number of assets found in the scan.
Nexpose.Scan.TotalTime string The duration of the scan in minutes.
Nexpose.Scan.Completed date The end time of the scan in ISO8601 format.
Nexpose.Scan.Status string The scan status. Valid values are aborted, unknown, running, finished, stopped, error, paused, dispatched, integrating.
Nexpose.Scan.Vulnerabilities.Critical number The number of critical vulnerabilities.
Nexpose.Scan.Vulnerabilities.Moderate number The number of moderate vulnerabilities.
Nexpose.Scan.Vulnerabilities.Severe number The number of severe vulnerabilities.
Nexpose.Scan.Vulnerabilities.Total number The total number of vulnerabilities.

nexpose-stop-scan


Stop a running scan.

Base Command

nexpose-stop-scan

Input

Argument Name Description Required
id ID of a running scan. Required

nexpose-pause-scan


Pause a running scan.

Base Command

nexpose-pause-scan

Input

Argument Name Description Required
id ID of a running scan. Required

nexpose-resume-scan


Resume a paused scan.

Base Command

nexpose-resume-scan

Input

Argument Name Description Required
id ID of a paused scan. Required

nexpose-get-scans


Return a list of scans. Returns only active scans by default (active=true).

Base Command

nexpose-get-scans

Input

Argument Name Description Required
active Whether to return only active scans. Possible values are: true, false. Default is true. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit A number of records to limit the response to. Default is 10. Optional
sort Criteria to sort the records by, in the format: property[,ASC|DESC]. If not specified, default sort order is ascending. Multiple sort criteria can be specified, separated by a “;”. For example: “riskScore,DESC;hostName,ASC”. Optional

Context Output

Path Type Description
Nexpose.Scan.Id number The identifier of the scan.
Nexpose.Scan.ScanType string The scan type (automated, manual, scheduled).
Nexpose.Scan.StartedBy date The name of the user who started the scan.
Nexpose.Scan.Assets number The number of assets found in the scan
Nexpose.Scan.TotalTime string The duration of the scan in minutes.
Nexpose.Scan.Completed date The end time of the scan in ISO8601 format.
Nexpose.Scan.Status string The scan status. Valid values are aborted, unknown, running, finished, stopped, error, paused, dispatched, integrating.

Command example

!nexpose-get-scans active=false limit=3

Context Example

{
    "Nexpose": {
        "Scan": [
            {
                "Assets": 0,
                "Completed": "2019-12-03T20:48:01.368Z",
                "Id": 1,
                "Message": null,
                "ScanName": "Tue 03 Dec 2019 10:47 PM",
                "ScanType": "Manual",
                "StartedBy": null,
                "Status": "finished",
                "TotalTime": "51.316 seconds"
            },
            {
                "Assets": 0,
                "Completed": "2019-12-03T20:53:09.453Z",
                "Id": 2,
                "Message": null,
                "ScanName": "Tue 03 Dec 2019 10:52 PM",
                "ScanType": "Manual",
                "StartedBy": null,
                "Status": "finished",
                "TotalTime": "29.91 seconds"
            },
            {
                "Assets": 0,
                "Completed": "2019-12-03T21:01:33.970Z",
                "Id": 3,
                "Message": null,
                "ScanName": "scan 2019-12-03 19:58:25.961787",
                "ScanType": "Manual",
                "StartedBy": null,
                "Status": "finished",
                "TotalTime": "28.904 seconds"
            }
        ]
    }
}

Human Readable Output

Nexpose Scans

Id ScanType ScanName Assets TotalTime Completed Status
1 Manual Tue 03 Dec 2019 10:47 PM 0 51.316 seconds 2019-12-03T20:48:01.368Z finished
2 Manual Tue 03 Dec 2019 10:52 PM 0 29.91 seconds 2019-12-03T20:53:09.453Z finished
3 Manual scan 2019-12-03 19:58:25.961787 0 28.904 seconds 2019-12-03T21:01:33.970Z finished

nexpose-disable-shared-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Disable an assigned shared credential.

Base Command

nexpose-disable-shared-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
credential_id ID of the scan schedule to update. Required

nexpose-download-report


Returns the generated report.

Base Command

nexpose-download-report

Input

Argument Name Description Required
report_id ID of the report. Required
instance_id ID of the report instance. Supports a “latest” value. Required
name Report name. Optional
format Report format (uses PDF by default). Possible values are: pdf, rtf, xml, html, text, nexpose-simple-xml. Default is pdf. Optional

Context Output

Path Type Description
InfoFile.EntryId string Entry ID of the report file.
InfoFile.Name string Name of the report file.
InfoFile.Extension string File extension of the report file.
InfoFile.Info string Information about the report file.
InfoFile.Size number Size of the report file (in bytes).
InfoFile.Type string Type of the report file.

Command example

!nexpose-download-report report_id=1 instance_id=latest

Context Example

{
    "InfoFile": {
        "EntryID": "4127@403762e2-be4e-4f12-8a17-26cdb21b129e",
        "Extension": "pdf",
        "Info": "application/pdf",
        "Name": "report 2022-11-30 09:25:43.835638.pdf",
        "Size": 76699,
        "Type": "PDF document, version 1.4"
    }
}

nexpose-enable-shared-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Enable an assigned shared credential.

Base Command

nexpose-enable-shared-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
credential_id ID of the scan schedule to update. Required

nexpose-get-report-status


Returns the status of a report generation process.

Base Command

nexpose-get-report-status

Input

Argument Name Description Required
report_id ID of the report. Required
instance_id ID of the report instance. Supports a “latest” value. Required

Context Output

Path Type Description
Nexpose.Report.ID string The identifier of the report.
Nexpose.Report.InstanceID string The identifier of the report instance.
Nexpose.Report.Status string The status of the report generation process. Valid values: “aborted”, “failed”, “complete”, “running”, “unknown”.

Command example

!nexpose-get-report-status report_id=1 instance_id=latest

Context Example

{
    "Nexpose": {
        "Report": {
            "ID": "1",
            "InstanceID": "latest",
            "Status": "complete"
        }
    }
}

Human Readable Output

Report Generation Status

ID InstanceID Status
1 latest complete

nexpose-update-scan-schedule


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Update an existing site scan schedule.

Base Command

nexpose-update-scan-schedule

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
schedule_id ID of the scan schedule to update. Optional
enabled A flag indicating whether the scheduled scan is enabled. Possible values are: True, False. Default is True. Optional
on_scan_repeat The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. Possible values are: Restart-Scan, Resume-Scan. Required
start The scheduled start date and time formatted in ISO 8601 format. Repeating schedules will determine the next schedule to begin based on this date and time. Required
excluded_asset_group_ids A list of IDs for asset groups to exclude from the scan. Optional
excluded_addresses A list of addresses to exclude from the scan. Optional
included_asset_group_ids A list of IDs for asset groups to include in the scan. Optional
included_addresses A list of addresses to include in the scan. Optional
duration_days Maximum duration of the scan in days. Optional
duration_hours Maximum duration of the scan in hours. Optional
duration_minutes Maximum duration of the scan in minutes. Optional
frequency How frequently should the schedule repeat (Every…). Possible values are: Hour, Day, Week, Date-of-month. Optional
interval_time The interval time the schedule should repeat. This depends on the value set in frequency. For example, if the value of frequency is set to “Day” and interval is set to 2, then the schedule will repeat every 2 days. Required only if frequency is used. Optional
date_of_month Specifies the schedule repeat day of the interval month. For example, if date_of_month is 17 and interval is set to 2, then the schedule will repeat every 2 months on the 17th day of the month. Required and used only if frequency is set to Date of month. Optional
scan_name A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. Optional
scan_template ID of the scan template to use. Optional

Context Output

There is no context output for this command.

nexpose-update-site-scan-credential


Note:
This command couldn’t have been tested on our side, and therefore could have issues. Please let us know if you encounter any bugs or issues.

Update an existing site scan credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/setSiteCredentials.

Base Command

nexpose-update-site-scan-credential

Input

Argument Name Description Required
site_id ID of the site. Optional
site_name Name of the site (can be used instead of site_id). Optional
credential_id ID of the site scan credential to update. Required
name Name of the credential. Required
service Credential service type. Possible values are: AS400, CIFS, CIFSHash, CVS, DB2, FTP, HTTP, MS-SQL, MySQL, Notes, Oracle, POP, PostgresSQL, Remote-Exec, SNMP, SNMPv3, SSH, SSH-Key, Sybase, Telnet. Required
database Database name. Optional
description Description for the credential. Optional
domain Domain address. Optional
host_restriction Hostname or IP address to restrict the credentials to. Optional
http_realm HTTP realm. Optional
notes_id_password Password for the notes account that will be used for authenticating. Optional
ntlm_hash NTLM password hash. Optional
oracle_enumerate_sids Whether the scan engine should attempt to enumerate SIDs from the environment. Possible values are: true, false. Optional
oracle_listener_password Oracle Net Listener password. Used to enumerate SIDs from your environment. Optional
oracle_sid Oracle database name. Optional
password Password for the credential. Optional
port_restriction Further restricts the credential to attempt to authenticate on a specific port. Can be used only if host_restriction is used. Optional
community_name SNMP community for authentication. Optional
authentication_type SNMPv3 authentication type for the credential. Possible values are: No-Authentication, MD5, SHA. Optional
privacy_password SNMPv3 privacy password to use. Optional
privacy_type SNMPv3 Privacy protocol to use. Possible values are: No-Privacy, DES, AES-128, AES-192, AES-192-With-3-DES-Key-Extension, AES-256, AES-256-With-3-DES-Key-Extension. Optional
ssh_key_pem PEM formatted private key. Optional
ssh_permission_elevation Elevation type to use for scans. Possible values are: None, sudo, sudosu, su, pbrun, Privileged Exec. Optional
ssh_permission_elevation_password Password to use for elevation. Optional
ssh_permission_elevation_username Username to use for elevation. Optional
ssh_private_key_password Password for the private key. Optional
use_windows_authentication Whether to use Windows authentication. Possible values are: true, false. Optional
username Username for the credential. Optional

Context Output

There is no context output for this command.

nexpose-update-vulnerability-exception-expiration


Update an existing vulnerability exception.

Base Command

nexpose-update-vulnerability-exception-expiration

Input

Argument Name Description Required
id ID of the vulnerability exception to update. Required
expiration An expiration date for the vulnerability exception formatted in ISO 8601 format. Must be a date in the future. Required

Command example

!nexpose-update-vulnerability-exception-expiration id=1 expiration=2024-10-10T10:00:00Z

Human Readable Output

Successfully updated expiration date of vulnerability exception 1.

nexpose-update-vulnerability-exception-status


Update an existing vulnerability exception.

Base Command

nexpose-update-vulnerability-exception-status

Input

Argument Name Description Required
id ID of the vulnerability exception to update. Required
status A status to update the vulnerability exception to. Possible values are: Recall, Approve, Reject. Required

Command example

!nexpose-update-vulnerability-exception-status id=1 status=Approve

Human Readable Output

Successfully updated status of vulnerability exception 1.

nexpose-update-shared-credential


Update an existing shared credential.

Base Command

nexpose-update-shared-credential

Input

Argument Name Description Required
id ID of the shared credential to update. Required
name Name of the credential. Required
site_assignment Site assignment configuration for the credential. Assign the shared scan credential either to be available to all sites, or a specific list of sites. Possible values are: All-Sites, Specific-Sites. Required
service Credential service type. Possible values are: AS400, CIFS, CIFSHash, CVS, DB2, FTP, HTTP, MS-SQL, MySQL, Notes, Oracle, POP, PostgresSQL, Remote-Exec, SNMP, SNMPv3, SSH, SSH-Key, Sybase, Telnet. Required
database Database name. Optional
description Description for the credential. Optional
domain Domain address. Optional
host_restriction Hostname or IP address to restrict the credentials to. Optional
http_realm HTTP realm. Optional
notes_id_password Password for the notes account that will be used for authenticating. Optional
ntlm_hash NTLM password hash. Optional
oracle_enumerate_sids Whether the scan engine should attempt to enumerate SIDs from the environment. Possible values are: true, false. Optional
oracle_listener_password Oracle Net Listener password. Used to enumerate SIDs from your environment. Optional
oracle_sid Oracle database name. Optional
password Password for the credential. Optional
port_restriction Further restricts the credential to attempt to authenticate on a specific port. Can be used only if host_restriction is used. Optional
sites List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. Optional
community_name SNMP community for authentication. Optional
authentication_type SNMPv3 authentication type for the credential. Possible values are: No-Authentication, MD5, SHA. Optional
privacy_password SNMPv3 privacy password to use. Optional
privacy_type SNMPv3 Privacy protocol to use. Possible values are: No-Privacy, DES, AES-128, AES-192, AES-192-With-3-DES-Key-Extension, AES-256, AES-256-With-3-DES-Key-Extension. Optional
ssh_key_pem PEM formatted private key. Optional
ssh_permission_elevation Elevation type to use for scans. Possible values are: None, sudo, sudosu, su, pbrun, Privileged-Exec. Optional
ssh_permission_elevation_password Password to use for elevation. Optional
ssh_permission_elevation_username Username to use for elevation. Optional
ssh_private_key_password Password for the private key. Optional
use_windows_authentication Whether to use Windows authentication. Possible values are: true, false. Optional
username Username for the credential. Optional

Context Output

There is no context output for this command.

nexpose-add-site-included-asset


Base Command

nexpose-add-site-included-asset

Input

Argument Name Description Required
site_id A URL parameter. Required
assets List of addresses to add to the site’s included scan assets. Each address is a string that can represent either a hostname, IPv4 address, IPv4 address range, IPv6 address, or CIDR notation. Optional
asset_group_ids List of asset group identifiers. Optional

Context Output

There is no context output for this command.

Command example

!nexpose-add-site-included-asset site_id=848 assets=8.8.8.8

Human Readable Output

Added assets 8.8.8.8 to site with ID 848

nexpose-remove-tag-asset


Remove an asset from a tag. Note that the asset must be added through the asset or tag. If the asset is added using a site, asset group, or search criteria, this action will not remove the asset from the tag.

Base Command

nexpose-remove-tag-asset

Input

Argument Name Description Required
tag_id The tag ID. Required
asset_id The asset ID. Required

Context Output

There is no context output for this command.

Command example

!nexpose-remove-tag-asset asset_id=25 tag_id=61

Human Readable Output

Asset 25 was removed from tag 61 successfully

nexpose-list-tag-asset


Return a list of assets for a tag.

Base Command

nexpose-list-tag-asset

Input

Argument Name Description Required
tag_id The tag ID. Required

Context Output

Path Type Description
Nexpose.TagAsset.id int Asset ID.
Nexpose.TagAsset.sources string The asset sources.

Command example

!nexpose-list-tag-asset tag_id=33

Context Example

{
    "Nexpose": {
        "TagAsset": [
            {
                "id": 18,
                "sources": [
                    "tag"
                ]
            },
            {
                "id": 25,
                "sources": [
                    "tag"
                ]
            },
            {
                "id": 28,
                "sources": [
                    "tag"
                ]
            }
        ]
    }
}

Human Readable Output

Tag 33 assets

Id Sources
18 tag
25 tag
28 tag

nexpose-delete-tag


Delete a tag.

Base Command

nexpose-delete-tag

Input

Argument Name Description Required
id The tag ID. Required

Context Output

There is no context output for this command.

nexpose-list-site-included-asset


Return a list of included assets for a site.

Base Command

nexpose-list-site-included-asset

Input

Argument Name Description Required
site_id A URL parameter. Required

Context Output

Path Type Description
Nexpose.IncludedAsset.site_id int The site ID.
Nexpose.IncludedAsset.addresses string A list of addresses of the included assets for the specified site.

Command example

!nexpose-list-site-included-asset site_id=848

Context Example

{
    "Nexpose": {
        "IncludedAsset": {
            "addresses": [
                "8.8.8.8",
                "1.1.1.1"
            ],
            "site_id": 848
        }
    }
}

Human Readable Output

Asset list for site ID 848

Addresses
8.8.8.8
1.1.1.1

nexpose-list-site-excluded-asset-group


Return a list of excluded asset groups for a site.

Base Command

nexpose-list-site-excluded-asset-group

Input

Argument Name Description Required
site_id A URL parameter. Required

Context Output

Path Type Description
Nexpose.ExcludedAssetGroup.site_id int The site ID.
Nexpose.ExcludedAssetGroup.resources int The asset group ID.

Command example

!nexpose-list-site-excluded-asset-group site_id=848

Context Example

{
    "Nexpose": {
        "ExcludedAssetGroup": {
            "resources": [],
            "site_id": 848
        }
    }
}

nexpose-list-site-included-asset-group


Return a list of included asset groups for a site.

Base Command

nexpose-list-site-included-asset-group

Input

Argument Name Description Required
site_id A URL parameter. Required

Context Output

Path Type Description
Nexpose.IncludedAssetGroup.site_id int The site ID.
Nexpose.IncludedAssetGroup.resources int The asset group ID.

Command example

!nexpose-list-site-included-asset-group site_id=848

Context Example

{
    "Nexpose": {
        "IncludedAssetGroup": {
            "resources": [],
            "site_id": 848
        }
    }
}

Human Readable Output

Asset group list for site ID 848

No entries.

nexpose-remove-tag-asset-group


Remove an asset group from a tag.

Base Command

nexpose-remove-tag-asset-group

Input

Argument Name Description Required
tag_id The tag ID. Required
asset_group_id The asset group ID. Required

Context Output

There is no context output for this command.

Command example

!nexpose-remove-tag-asset-group asset_group_id=1 tag_id=61

Human Readable Output

Asset group 1 was removed from tag 61 successfully

nexpose-create-tag


Create a tag.

Base Command

nexpose-create-tag

Input

Argument Name Description Required
name The tag name. Required
type The tag type. Possible values are: Owner, Location, Custom. Required
color The tag color - relevant only for “Custom” type. Possible values are: Blue, Green, Orange, Red, Purple, Default. Default is Default. Optional
ip_address_is A specific IP address to search for. Optional
host_name_is A specific host name to search for. Optional
risk_score_higher_than A minimum risk score to use as a filter. Optional
vulnerability_title_contains A string to search for in vulnerability titles. Optional
site_id_in Site IDs to filter for. Can be a comma-separated list. Optional
site_name_in Site names to filter for. Can be a comma-separated list. Optional
match Operator to determine how to match filters. “All” requires that all filters match for an asset to be included. “Any” requires only one filter to match for an asset to be included. Possible values are: All, Any. Default is Any. Optional
query Additional queries to use as a filter, following the Search Criteria API standard. The structure is {field} {operator} {value}. Multiple queries can be specified, separated by a “;” separator. For example, ‘ip-address in-range 192.0.2.0,192.0.2.1;host-name is myhost’. Optional

Context Output

Path Type Description
Nexpose.Tag.id int The tag ID.

nexpose-add-tag-asset


Add an existing asset to an existing tag.

Base Command

nexpose-add-tag-asset

Input

Argument Name Description Required
tag_id The tag ID. Required
asset_id The asset ID. Required

Context Output

There is no context output for this command.

Command example

!nexpose-add-tag-asset asset_id=25 tag_id=61

Human Readable Output

Asset 25 was added in tag 61 successfully

nexpose-remove-site-excluded-asset


Remove excluded assets from a site.

Base Command

nexpose-remove-site-excluded-asset

Input

Argument Name Description Required
site_id A URL parameter. Required
assets List of addresses to remove from the site’s excluded scan assets. Each address is a string that can represent either a hostname, IPv4 address, IPv4 address range, IPv6 address, or CIDR notation. Optional
asset_group_ids List of asset group IDs to remove from the site’s exclusion list. Optional

Context Output

There is no context output for this command.

Command example

!nexpose-remove-site-excluded-asset site_id=848 assets=8.8.8.8

Human Readable Output

Removed assets 8.8.8.8 from site with ID 848

nexpose-remove-site-included-asset


Base Command

nexpose-remove-site-included-asset

Input

Argument Name Description Required
site_id A URL parameter. Required
assets List of addresses to remove from the site’s included scan assets. Each address is a string that can represent either a hostname, IPv4 address, IPv4 address range, IPv6 address, or CIDR notation. Optional
asset_group_ids List of asset group identifiers. Optional

Context Output

There is no context output for this command.

Command example

!nexpose-remove-site-included-asset site_id=848 assets=8.8.8.8

Human Readable Output

Removed assets 8.8.8.8 from site with ID 848

nexpose-update-tag-search-criteria


Update tag search criteria.

Base Command

nexpose-update-tag-search-criteria

Input

Argument Name Description Required
tag_id The tag ID. Required
ip_address_is A specific IP address to search for. Optional
host_name_is A specific host name to search for. Optional
risk_score_higher_than A minimum risk score to use as a filter. Optional
vulnerability_title_contains A string to search for in vulnerability titles. Optional
site_id_in Site IDs to filter for. Can be a comma-separated list. Optional
site_name_in Site names to filter for. Can be a comma-separated list. Optional
match Operator to determine how to match filters. “All” requires that all filters match for an asset to be included. “Any” requires only one filter to match for an asset to be included. Possible values are: All, Any. Default is Any. Optional
query Additional queries to use as a filter, following the Search Criteria API standard. The structure is {field} {operator} {value}. Multiple queries can be specified, separated by a “;” separator. For example, ‘ip-address in-range 192.0.2.0,192.0.2.1;host-name is myhost’. Optional
overwrite Whether to overwrite the original search values or append new conditions to the existing search. Possible values are: yes, no. Default is no. Optional

Context Output

There is no context output for this command.

nexpose-add-tag-asset-group


Add existing asset groups to an existing tag.()

Base Command

nexpose-add-tag-asset-group

Input

Argument Name Description Required
tag_id The tag ID. Required
asset_group_ids The asset group IDs to add. Can be a comma-separated list. Required

Context Output

There is no context output for this command.

nexpose-list-site-excluded-asset


Return a list of excluded assets for a site.

Base Command

nexpose-list-site-excluded-asset

Input

Argument Name Description Required
site_id A URL parameter. Required

Context Output

Path Type Description
Nexpose.ExcludedAsset.site_id int The site ID.
Nexpose.ExcludedAsset.addresses string A list of addresses of the excluded assets for the specified site.

Command example

!nexpose-list-site-excluded-asset site_id=848

Context Example

{
    "Nexpose": {
        "ExcludedAsset": {
            "site_id": 848
        }
    }
}

Human Readable Output

Asset list for site ID 848

No entries.

nexpose-list-tag-asset-group


Return a list of asset groups for a tag.

Base Command

nexpose-list-tag-asset-group

Input

Argument Name Description Required
tag_id The tag ID. Required

Context Output

Path Type Description
Nexpose.TagAssetGroup.id int Asset group ID.

Command example

!nexpose-list-tag-asset-group tag_id=2

Context Example

{
    "Nexpose": {
        "TagAssetGroup": [
            3
        ]
    }
}

Human Readable Output

Tag 2 asset groups

Asset groups IDs
3

nexpose-list-tag


Return a list of tags.

Base Command

nexpose-list-tag

Input

Argument Name Description Required
id Get tag by ID. Optional
name Filters the returned tags to only those containing the value within their name. Optional
type Filters the returned tags to only those of this type. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit A number of records to limit the response to. Optional

Context Output

Path Type Description
Nexpose.Tag.color String The color associated with the tag.
Nexpose.Tag.created Date The date when the tag was created.
Nexpose.Tag.id Number The unique identifier of the tag.
Nexpose.Tag.name String The name of the tag.
Nexpose.Tag.searchCriteria.match String The match criteria used for the tag search (e.g., “all” or “any”).
Nexpose.Tag.searchCriteria.filters.field String The field name used in the tag search filter.
Nexpose.Tag.searchCriteria.filters.operator String The operator used in the tag search filter (e.g., “is”, “contains”, “is-greater-than”).
Nexpose.Tag.searchCriteria.filters.lower String The lower bound of the range used in the tag search filter.
Nexpose.Tag.searchCriteria.filters.upper String The upper bound of the range used in the tag search filter.
Nexpose.Tag.source String The source of the tag.
Nexpose.Tag.type String The type of the tag.
Nexpose.Tag.searchCriteria.filters.value String The value used in the tag search filter.
Nexpose.Tag.page.number Number The current page number in the paginated response.
Nexpose.Tag.page.size Number The number of items per page in the paginated response.
Nexpose.Tag.page.totalResources Number The total number of resources available.
Nexpose.Tag.page.totalPages Number The total number of pages available.

Command example

!nexpose-list-tag limit=2 name=test

Context Example

{
    "resources": [
        {
            "color": "default",
            "created": "2024-05-06T13:32:58.454Z",
            "id": 45,
            "name": "test",
            "searchCriteria": {
                "match": "all",
                "filters": [
                    {
                        "field": "risk-score",
                        "operator": "in-range",
                        "lower": "193.841",
                        "upper": "187.841"
                    }
                ]
            },
            "source": "custom",
            "type": "Owner"
        },
        {
            "color": "default",
            "created": "2024-05-06T13:43:52.874Z",
            "id": 46,
            "name": "new_test2",
            "searchCriteria": {
                "match": "any",
                "filters": [
                    {
                        "field": "ip-address",
                        "operator":"is",
                        "value":"3.3.3.3"
                    }
                ]
            },
            "source": "custom",
            "type": "Owner"
        }
    ],
    "page": {
        "number": 0,
        "size": 2,
        "totalResources": 8,
        "totalPages": 4
    }
}

nexpose-add-site-excluded-asset


Add excluded assets to a site.

Base Command

nexpose-add-site-excluded-asset

Input

Argument Name Description Required
site_id A URL parameter. Required
assets List of addresses to add to the site’s excluded scan assets. Each address is a string that can represent either a hostname, IPv4 address, IPv4 address range, IPv6 address, or CIDR notation. Optional
asset_group_ids List of asset group IDs to exclude. Optional

Context Output

There is no context output for this command.

Command example

!nexpose-add-site-excluded-asset site_id=848 assets=8.8.8.8

Human Readable Output

Added assets 8.8.8.8 to site with ID 848

nexpose-list-asset-group


Return a list of asset groups.

Base Command

nexpose-list-asset-group

Input

Argument Name Description Required
group_id Get asset group by ID. Optional
group_name Filters the returned asset groups to only those containing the value within their name. Optional
type Filters the returned asset groups to only those of this type. Possible values are: static, dynamic. Optional
page_size Number of records to retrieve in each API call when pagination is used. Optional
page A specific page to retrieve when pagination is used. Page indexing starts at 0. Optional
limit A number of records to limit the response to. Optional
sort The criteria to sort the records by, in the format property[,ASC|DESC]. The default sort order is ascending. Multiple sort criteria can be specified using multiple sort query parameters. Optional

Context Output

Path Type Description
Nexpose.AssetGroup.assets Number The number of assets in the asset group.
Nexpose.AssetGroup.id Number The unique identifier of the asset group.
Nexpose.AssetGroup.name String The name of the asset group.
Nexpose.AssetGroup.riskScore Number The cumulative risk score of the asset group.
Nexpose.AssetGroup.type String The type of the asset group.
Nexpose.AssetGroup.vulnerabilities.critical Number The number of critical vulnerabilities in the asset group.
Nexpose.AssetGroup.vulnerabilities.moderate Number The number of moderate vulnerabilities in the asset group.
Nexpose.AssetGroup.vulnerabilities.severe Number The number of severe vulnerabilities in the asset group.
Nexpose.AssetGroup.vulnerabilities.total Number The total number of vulnerabilities in the asset group.
Nexpose.AssetGroup.description String The description of the asset group.

Command example

!nexpose-list-asset-group limit=2

nexpose-create-asset-group


Create an asset group.

Base Command

nexpose-create-asset-group

Input

Argument Name Description Required
name The asset group name. Required
type The asset group type. Possible values are: static, dynamic. Required
description The description of the asset group. Required
ip_address_is A specific IP address to search for. Optional
host_name_is A specific host name to search for. Optional
risk_score_higher_than A minimum risk score to use as a filter. Optional
vulnerability_title_contains A string to search for in vulnerability titles. Optional
site_id_in Site IDs to filter for. Can be a comma-separated list. Optional
site_name_in Site names to filter for. Can be a comma-separated list. Optional
match Operator to determine how to match filters. “All” requires that all filters match for an asset to be included. “Any” requires only one filter to match for an asset to be included. Possible values are: All, Any. Default is Any. Optional
query Additional queries to use as a filter, following the Search Criteria API standard. The structure is {field} {operator} {value}. Multiple queries can be specified, separated by a “;” separator. For example, ‘ip-address in-range 192.0.2.0,192.0.2.1;host-name is myhost’. Optional

Context Output

Path Type Description
Nexpose.AssetGroup.id int The asset group ID.

Command example

!nexpose-create-asset-group name=test3 type=dynamic ip_address_is=1.1.1.1 query=`risk-score is-greater-than 8000` escription=test

<~PLATFORM>

License Requirements

The following configuration parameters require Cortex XSIAM with the Exposure Management add-on:

  • Fetch Assets

</~PLATFORM>

Configuration parameters

  • server — Server URL (e.g., https://192.0.2.0:8080) (required)
  • credentials — Username (required)
  • token — 2FA Token
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • connection_error_retries — Number of connection error retries
  • isFetchAssets — Fetch Assets
  • assetsFetchInterval — Assets Fetch Interval

Commands (64)

  • nexpose-add-site-excluded-asset

    Add excluded assets to a site.

  • nexpose-add-site-included-asset

    Add included assets to a site.

  • nexpose-add-tag-asset

    Add an existing asset to an existing tag.

  • nexpose-add-tag-asset-group

    Add existing asset groups to an existing tag.

  • nexpose-create-asset

    Create a new asset.

  • nexpose-create-asset-group

    Create an asset group. Note: All filters are relevant only for Dynamic asset groups.

  • nexpose-create-assets-report

    Generates a new report on given assets according to a template and arguments.

  • nexpose-create-scan-report

    Generates a new report for a specified scan.

  • nexpose-create-scan-schedule

    Beta Command Create a new site scan schedule.

  • nexpose-create-shared-credential

    Create a new shared credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSharedCredential

  • nexpose-create-site

    Creates a new site with the specified configuration.

  • nexpose-create-site-scan-credential

    Beta Command Create a new site scan credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteCredential

  • nexpose-create-sites-report

    Generates a new report on given sites according to a template and arguments.

  • nexpose-create-tag

    Create a tag.

  • nexpose-create-vulnerability-exception

    Create a new vulnerability exception.

  • nexpose-delete-asset

    Delete an asset.

  • nexpose-delete-scan-schedule

    Delete a scheduled scan.

  • nexpose-delete-shared-credential

    Beta Command Delete a shared credential.

  • nexpose-delete-site

    Deletes a site.

  • nexpose-delete-site-scan-credential

    Beta Command Delete a site scan credential.

  • nexpose-delete-tag

    Delete a tag.

  • nexpose-delete-vulnerability-exception

    Delete a vulnerability exception.

  • nexpose-disable-shared-credential

    Beta Command Disable an assigned shared credential.

  • nexpose-download-report

    Returns the generated report.

  • nexpose-enable-shared-credential

    Beta Command Enable an assigned shared credential.

  • nexpose-get-asset

    Returns the specified asset.

  • nexpose-get-asset-tags

    Returns the specified tags for an asset.

  • nexpose-get-asset-vulnerability

    Returns details and possible remediations for an asset's vulnerability.

  • nexpose-get-assets

    Returns all assets for which you have access.

  • nexpose-get-report-status

    Returns the status of a report generation process.

  • nexpose-get-report-templates

    Returns all available report templates.

  • nexpose-get-scan

    Get a specific scan.

  • nexpose-get-scans

    Return a list of scans. Returns only active scans by default (active=true).

  • nexpose-get-sites

    Retrieves accessible sites.

  • nexpose-list-asset-group

    Return a list of asset groups.

  • nexpose-list-assigned-shared-credential

    Retrieve information about shared credentials for a specific site.

  • nexpose-list-scan-schedule

    BetaCommand Retrieve information about scan schedules for a specific site or a specific scan schedule.

  • nexpose-list-shared-credential

    Retrieve information about all or a specific shared credential.

  • nexpose-list-site-excluded-asset

    Return a list of excluded assets for a site.

  • nexpose-list-site-excluded-asset-group

    Return a list of excluded asset groups for a site.

  • nexpose-list-site-included-asset

    Return a list of included assets for a site.

  • nexpose-list-site-included-asset-group

    Return a list of included asset groups for a site.

  • nexpose-list-site-scan-credential

    Beta Command Retrieve information about all or a specific sca credential.

  • nexpose-list-tag

    Return a list of tags.

  • nexpose-list-tag-asset

    Return the tag assets list.

  • nexpose-list-tag-asset-group

    Return a list of asset groups for a tag.

  • nexpose-list-vulnerability

    Retrieve information about all or a specific vulnerability.

  • nexpose-list-vulnerability-exceptions

    Retrieve information about scan schedules for a specific site or a specific scan schedule.

  • nexpose-pause-scan

    Pause a running scan.

  • nexpose-remove-site-excluded-asset

    Remove excluded assets from a site.

  • nexpose-remove-site-included-asset

    Remove included assets from a site.

  • nexpose-remove-tag-asset

    Remove an asset from a tag. Note that the asset must be added through the asset or tag. If the asset is added using a site, asset group, or search criteria, this action will not remove the asset from the tag.

  • nexpose-remove-tag-asset-group

    Remove an asset group from a tag.

  • nexpose-resume-scan

    Resume a paused scan.

  • nexpose-search-assets

    Search and return all assets matching specific filters. Returns only assets the user has access to.

  • nexpose-start-assets-scan Deprecated

    Deprecated. Use `nexpose-start-site-scan` using the `hosts` argument instead.

  • nexpose-start-site-scan

    Starts a scan for the specified site.

  • nexpose-stop-scan

    Stop a running scan.

  • nexpose-update-scan-schedule

    Beta Command Update an existing site scan schedule.

  • nexpose-update-shared-credential

    Update an existing shared credential.

  • nexpose-update-site-scan-credential

    Beta Command Update an existing site scan credential. For detailed explanation of all parameters of this command, see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/setSiteCredentials.

  • nexpose-update-tag-search-criteria

    Update tag search criteria.

  • nexpose-update-vulnerability-exception-expiration

    Update an existing vulnerability exception.

  • nexpose-update-vulnerability-exception-status

    Update an existing vulnerability exception.

import csv
import io
import json
import traceback
from copy import deepcopy
from enum import Enum, EnumMeta
from time import strptime, struct_time
from typing import overload
from collections.abc import AsyncGenerator

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

VENDOR_NAME = "Rapid7 Nexpose"  # Vendor name to use for indicators.
API_DEFAULT_PAGE_SIZE = 10  # Default page size that's set on the API. Used for calculations.
DEFAULT_PAGE_SIZE = 50  # Default page size to use
MATCH_DEFAULT_VALUE = "any"  # Default "match" value to use when using search filters. Can be either "all" or "any".
REMOVE_RESPONSE_LINKS = True  # Whether to remove `links` keys from responses.
REPORT_DOWNLOAD_WAIT_TIME = 60  # Time in seconds to wait before downloading a report after starting its generation
CONNECTION_ERRORS_RETRIES = 5  # num of times to retry in case of connection-errors
CONNECTION_ERRORS_INTERVAL = 1  # num of seconds between each time to send an http-request in case of a connection error.
VALID_TAG_TYPES = ["custom", "location", "owner"]
VALID_ASSET_GROUP_TYPES = ["dynamic", "static"]
VALID_TAG_COLORS = ["blue", "green", "orange", "red", "purple", "default"]

urllib3.disable_warnings()  # Disable insecure warnings

DEFAULT_BATCH_SIZE = 3000
# Max number of concurrent send-to-XSIAM HTTP requests in flight at once (XSUP-69895).
# Without this cap the integration fans out one asyncio task + one aiohttp.ClientSession per batch,
# opening dozens of simultaneous connections to the ingestion endpoint on large reports, which causes
# the server to truncate responses mid-stream (aiohttp TransferEncodingError) / time out new connections.
MAX_CONCURRENT_XSIAM_SENDS = 5
# Max number of batch-send tasks allowed to be in flight before the stream-reader applies backpressure.
# Keeps memory bounded and surfaces send failures promptly instead of streaming an entire huge report ahead
# of the senders. Kept >= MAX_CONCURRENT_XSIAM_SENDS so the semaphore (not this) is the active throttle.
MAX_IN_FLIGHT_BATCH_TASKS = 10
# Total timeout (seconds) for a single send-to-XSIAM HTTP request, so a stuck upload fails fast and is
# retried instead of hanging until the engine's control-channel timeout trips.
XSIAM_SEND_TIMEOUT_SECONDS = 60
# Idle-read timeout (seconds) for streaming the Rapid7 report download. No overall cap, so large reports
# can finish in a single fetch; fails only if the connection stalls for this long.
RAPID7_STREAM_SOCK_READ_TIMEOUT_SECONDS = 120
RAPID7_STREAM_SOCK_CONNECT_TIMEOUT_SECONDS = 30
BASE_QUERY_FOR_ASSETS = """WITH asset_tags AS (
    SELECT
        dta.asset_id,
        json_agg(
            json_build_object(
                'Name', dt.tag_name,
                'Type', dt.tag_type
            )
        ) AS aggregated_tags_json
    FROM
        dim_tag_asset dta
    JOIN
        dim_tag dt USING (tag_id)
    GROUP BY
        dta.asset_id
),
asset_softwares AS (
    SELECT
        das.asset_id,
        json_agg(
            json_build_object(
                'Software_ID', das.software_id,
                'Fingerprint_source_ID', das.fingerprint_source_id,
                'Vendor', ds.vendor,
                'Family', ds.family,
                'Name', ds.name,
                'Version', ds.version,
                'Software_class', ds.software_class,
                'Cpe', ds.cpe
            )
        ) AS aggregated_software_json
    FROM
        dim_software ds
    JOIN
        dim_asset_software das ON ds.software_id = das.software_id
    GROUP BY
        das.asset_id
),
asset_macs AS (
    SELECT
        dama.asset_id,
        json_agg(dama.mac_address) AS aggregated_mac_addresses
    FROM
        dim_asset_mac_address dama
    GROUP BY
        dama.asset_id
),
asset_ips AS (
    SELECT
        daia.asset_id,
        json_agg(daia.ip_address) AS aggregated_ip_addresses
    FROM
        dim_asset_ip_address daia
    GROUP BY
        daia.asset_id
),
asset_files AS (
    SELECT
        daf.asset_id,
        json_agg(
            json_build_object(
                'file_id', daf.file_id,
                'type', daf.type,
                'name', daf.name,
                'size', daf.size
            )
        ) AS aggregated_files_json
    FROM
        dim_asset_file daf
    GROUP BY
        daf.asset_id
),
asset_service AS (
    SELECT
        dase.asset_id,
        json_agg(
            json_build_object(
                'service_id', dase.service_id,
                'protocol', dase.protocol_id,
                'port', dase.port,
                'service_fingerprint_id', dase.service_fingerprint_id,
                'certainty', dase.certainty
            )
        ) AS asset_services
    FROM
        dim_asset_service dase
    GROUP BY
        dase.asset_id
),
    assets_host AS (
        SELECT
            dahn.asset_id,
            json_agg(
                json_build_object(
                    'source_type_id', dahn.source_type_id,
                    'description', dhnst.description
                )
                ) AS asset_hosts
        FROM
            dim_asset_host_name dahn
        JOIN
            dim_host_name_source_type dhnst ON dahn.source_type_id = dhnst.type_id
        GROUP BY
            dahn.asset_id
    ),
-- Finds the single, absolute latest scan for each asset (regardless of status)
asset_latest_scan AS (
    SELECT DISTINCT ON (fas.asset_id)
        fas.asset_id,
        ds.finished AS last_scan_date,
        ds.status_id AS last_scan_status_id,
        ds.scan_id AS scan_id,
        ds.started AS started,
        ds.type_id AS type_id,
        ds.scan_name AS scan_name,
        fas.vulnerabilities AS vulnerabilities,
        fas.aggregated_credential_status_id AS aggregated_credential_status_id

    FROM
        fact_asset_scan fas
    JOIN
        dim_scan ds ON fas.scan_id = ds.scan_id
    ORDER BY
        fas.asset_id, ds.finished DESC -- Order by asset ID, then descending finish time to get the absolute latest
),
latest_os_certainty AS (
    SELECT DISTINCT ON (fasos.asset_id)
        fasos.asset_id,
        fasos.certainty AS latest_os_certainty_value
    FROM
        fact_asset_scan_operating_system fasos
    JOIN
        dim_scan ds ON fasos.scan_id = ds.scan_id
    ORDER BY
        fasos.asset_id, ds.finished DESC, fasos.certainty DESC -- Use certainty as a tie-breaker if dates are equal
)

SELECT
    da.last_assessed_for_vulnerabilities AS "dim_asset.last_assessed_for_vulnerabilities",
    fad.last_discovered AS "fact_asset_discovery.last_discovered",
    da.asset_id AS "dim_asset.asset_id",
    da.host_name AS "dim_asset.host_name",
    da.host_type_id AS "dim_asset.host_type_id",
    dht.description AS "dim_host_type.description",
    CASE
        WHEN da.ip_address LIKE '%:%' THEN NULL
        ELSE da.ip_address
    END AS "ipv4",

    CASE
        WHEN da.ip_address LIKE '%:%' THEN da.ip_address
        ELSE NULL
    END AS "ipv6",
    da.mac_address AS "dim_asset.mac_address",
    da.sites AS "dim_asset.sites",
    da.operating_system_id AS "dim_asset.operating_system_id",
    dos.architecture AS "dim_operating_system.architecture",
    dos.description AS "dim_operating_system.description",
    dos.family AS "dim_operating_system.family",
    dos.name AS "dim_operating_system.name",
    dos.system AS "dim_operating_system.system",
    dos.asset_type AS "dim_operating_system.asset_type",
    dos.vendor AS "dim_operating_system.vendor",
    dos.version AS "dim_operating_system.version",
    dos.cpe AS "dim_operating_system.cpe",
    loc.latest_os_certainty_value AS "fact_asset_scan_operating_system.certainty",
    atags.aggregated_tags_json AS "dim_tag_asset.tags",
    am.aggregated_mac_addresses AS "dim_asset_mac_address.mac_address",
    ai.aggregated_ip_addresses AS "dim_asset_ip_address.ip_address",
    af.aggregated_files_json AS "asset_files",
    asoft.aggregated_software_json AS "asset_softwares",
    als.last_scan_date AS "dim_scan.finished",
    als.last_scan_status_id AS "dim_scan.status_id",
    als.scan_id AS "dim_scan.scan_id",
    als.started AS "dim_scan.started",
    als.type_id AS "dim_scan.type_id",
    als.scan_name AS "dim_scan.scan_name",
    als.vulnerabilities AS "fact_asset_scan.vulnerabilities",
    als.aggregated_credential_status_id AS "fact_asset_scan.aggregated_credential_status_id",
    dacs.aggregated_credential_status_description AS "dim_aggregated_credential_status.aggregated_credential_status_description",
    ah.asset_hosts AS "asset hosts",
    ase.asset_services AS "asset_services"

FROM
    dim_asset da
LEFT JOIN
    fact_asset_discovery fad ON da.asset_id = fad.asset_id
LEFT JOIN
    dim_operating_system dos ON da.operating_system_id = dos.operating_system_id
LEFT JOIN
    asset_tags atags ON da.asset_id = atags.asset_id
LEFT JOIN
    asset_macs am ON da.asset_id = am.asset_id
LEFT JOIN
    asset_ips ai ON da.asset_id = ai.asset_id
LEFT JOIN
    asset_files af ON da.asset_id = af.asset_id
LEFT JOIN
    dim_host_type dht ON da.host_type_id = dht.host_type_id
LEFT JOIN
    asset_softwares asoft ON da.asset_id = asoft.asset_id
LEFT JOIN
    assets_host ah ON da.asset_id = ah.asset_id
INNER JOIN
    asset_latest_scan als ON da.asset_id = als.asset_id
LEFT JOIN
    dim_aggregated_credential_status dacs ON als.aggregated_credential_status_id = dacs.aggregated_credential_status_id
LEFT JOIN
    latest_os_certainty loc ON da.asset_id = loc.asset_id -- Join the new CTE
LEFT JOIN
    asset_service ase ON da.asset_id = ase.asset_id

WHERE
    als.last_scan_date >= CURRENT_DATE - INTERVAL '100 days'
    AND als.last_scan_status_id = 'C'

ORDER BY
    da.asset_id ASC"""

BASE_QUERY_FOR_VULNERABILITIES = """
WITH cve_references AS (
    SELECT
        dvr.vulnerability_id,
        array_to_string(array_agg(dvr.reference), ', ') AS aggregated_cves
    FROM
        dim_vulnerability_reference AS dvr
    WHERE
        dvr.source = 'CVE'
    GROUP BY
        dvr.vulnerability_id
),
aggregated_categories AS (
    SELECT
        dvc.vulnerability_id,
        json_agg(
            json_build_object(
                'id', dvc.category_id,
                'name', dvc.category_name
            )
        ) AS vuln_categories
    FROM
        dim_vulnerability_category AS dvc
    GROUP BY
        dvc.vulnerability_id
),
aggregated_details AS (
    SELECT
        fasvi.asset_id,
        fasvi.vulnerability_id,
        fasvi.scan_id,
        fasvi.date AS scan_date,
        
        -- Aggregate the related finding details into a JSON array of objects
        json_agg(
            json_build_object(
                'status_id', fasvi.status_id,
                'proof', htmlToText(fasvi.proof),
                'service_id', fasvi.service_id,
                'port', fasvi.port,
                'protocol_id', fasvi.protocol_id
            )
        ) AS finding_details_json
    FROM
        fact_asset_scan_vulnerability_instance fasvi
    INNER JOIN
        fact_asset_vulnerability_finding favf
        ON fasvi.asset_id = favf.asset_id
        AND fasvi.vulnerability_id = favf.vulnerability_id
        AND fasvi.scan_id = favf.scan_id
    GROUP BY
        fasvi.asset_id,
        fasvi.vulnerability_id,
        fasvi.scan_id,
        fasvi.date
)
SELECT
    ad.asset_id,
    ad.vulnerability_id AS vuln_id,
    ad.scan_id,
    ad.scan_date AS date,
    ad.finding_details_json,
    dv.nexpose_id AS vulnerability_name,
    dv.severity AS vulnerability_severity,
    dv.title,
    REPLACE(REPLACE(dv.description, CHR(10), ' '), CHR(13), ' ') AS description,
    dv.date_published,
    dv.date_added,
    dv.severity_score,
    dv.riskscore,
    dv.cvss_authentication_id,
    dv.cvss_exploit_score,
    dv.cvss_impact_score,
    dv.cvss_v2_score,
    dv.cvss_v2_exploit_score,
    dv.cvss_v2_impact_score,
    dv.cvss_v3_score,
    dv.cvss_v3_exploit_score,
    dv.denial_of_service,
    dv.exploits,
    dv.malware_kits,
    dv.date_modified,
    ac.vuln_categories,
    cef.aggregated_cves
FROM
    aggregated_details ad
LEFT JOIN dim_vulnerability AS dv
    ON ad.vulnerability_id = dv.vulnerability_id
LEFT JOIN aggregated_categories AS ac
    ON ad.vulnerability_id = ac.vulnerability_id
LEFT JOIN cve_references cef
    ON ad.vulnerability_id = cef.vulnerability_id
WHERE
    ad.scan_date >= CURRENT_DATE - INTERVAL '100 days'
ORDER BY
    ad.asset_id, ad.vulnerability_id, ad.scan_date DESC
"""
BASE_QUERY = {"asset": BASE_QUERY_FOR_ASSETS, "vulnerability": BASE_QUERY_FOR_VULNERABILITIES}
VENDOR = {"asset": "Rapid7", "vulnerability": "Rapid7"}
PRODUCT = {"asset": "nexpose_assets", "vulnerability": "nexpose_vulnerabilities"}
XSIAM_EVENT_CHUNK_SIZE = 2**20  # 1 Mib
XSIAM_EVENT_CHUNK_SIZE_LIMIT = 9 * (10**6)  # 9 MB, note that the allowed max size for 1 entry is 5MB.


def log(event_type: str, log_line: str):
    full_log_line = f"[{event_type}] {log_line}"
    demisto.debug(full_log_line)


class ScanStatus(Enum):
    """An Enum of possible scan status values."""

    PAUSE = "pause"
    RESUME = "resume"
    STOP = "stop"


class FlexibleEnum(EnumMeta):
    """A custom EnumMeta to allow flexible conversion from strings to Enum."""

    def __getitem__(cls, item: Any):
        try:
            return super().__getitem__(item)

        except KeyError:
            return super().__getitem__(item.upper().replace(" ", "_").replace("-", "_"))


class CredentialService(Enum, metaclass=FlexibleEnum):
    """An Enum of possible service values for credentials."""

    AS400 = "as400"
    CIFS = "cifs"
    CIFSHASH = "cifshash"
    CVS = "cvs"
    DB2 = "db2"
    FTP = "ftp"
    HTTP = "http"
    MS_SQL = "ms-sql"
    MYSQL = "mysql"
    NOTES = "notes"
    ORACLE = "oracle"
    POP = "pop"
    POSTGRESQL = "postgresql"
    REMOTE_EXEC = "remote-exec"
    SNMP = "snmp"
    SNMPV3 = "snmpv3"
    SSH = "ssh"
    SSH_KEY = "ssh-key"
    SYBASE = "sybase"
    TELNET = "telnet"


class RepeatFrequencyType(Enum, metaclass=FlexibleEnum):
    """An Enum of possible repeat frequency for scheduled scans."""

    HOUR = "hour"
    DAY = "day"
    WEEK = "week"
    DATE_OF_MONTH = "date-of-month"
    DAY_OF_MONTH = "day-of-month"


class SharedCredentialSiteAssignment(Enum, metaclass=FlexibleEnum):
    """An Enum of possible site assignment values for shared credentials."""

    ALL_SITES = "all-sites"
    SPECIFIC_SITES = "specific-sites"


class SNMPv3AuthenticationType(Enum, metaclass=FlexibleEnum):
    """An Enum of possible authentication type values for shared credentials."""

    NO_AUTHENTICATION = "no-authentication"
    MD5 = "md5"
    SHA = "sha"


class SNMPv3PrivacyType(Enum, metaclass=FlexibleEnum):
    """An Enum of possible privacy type values for SNMPv3P credentials."""

    NO_PRIVACY = "no-privacy"
    DES = "des"
    AES_128 = "aes-128"
    AES_192 = "aes-192"
    AES_192_WITH_3_DES_KEY_EXTENSION = "aes-192-with-3-des-key-extension"
    AES_256 = "aes-256"
    AES_265_WITH_3_DES_KEY_EXTENSION = "aes-265-with-3-des-key-extension"


class SSHElevationType(Enum, metaclass=FlexibleEnum):
    """An Enum of possible permission elevation values for SSH credentials."""

    NONE = "none"
    SUDO = "sudo"
    SUDOSU = "sudosu"
    SU = "su"
    PBRUN = "pbrun"
    PRIVILEGED_EXEC = "privileged-exec"


class VulnerabilityExceptionScopeType(Enum, metaclass=FlexibleEnum):
    """An Enum of possible vulnerability exception scope type values."""

    GLOBAL = "Global"
    SITE = "Site"
    ASSET = "Asset"
    ASSET_GROUP = "Asset Group"


class Client(BaseClient):
    """Client class for interactions with Rapid7 Nexpose API."""

    def __init__(
        self,
        url: str,
        username: str,
        password: str,
        token: str | None = None,
        verify: bool = True,
        connection_error_retries: int = CONNECTION_ERRORS_RETRIES,
    ):
        """
        Initialize the client.

        Args:
            url (str): Nexpose server base URL.
            username (str): Username to use for authentication.
            password (str): Password to use for authentication.
            token (str | None, optional): 2FA token to use for authentication.
            verify (bool | None, optional): Whether to verify SSL certificates. Defaults to True.
        """
        self.base_url = url
        self._auth_username = username
        self._auth_password = password
        self._auth_token = token
        self._headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
        }
        self.connection_error_retries = CONNECTION_ERRORS_RETRIES

        # Add 2FA token to headers if provided
        if token:
            self._headers.update({"Token": token})

        super().__init__(
            base_url=url.rstrip("/") + "/api/3",
            auth=(username, password),
            headers=self._headers,
            ok_codes=(200, 201),
            verify=verify,
        )

    def _http_request(self, **kwargs):  # type: ignore[override]
        """Wrapper for BaseClient._http_request() that optionally removes `links` keys from responses."""
        for _time in range(1, self.connection_error_retries + 1):
            try:
                response = super()._http_request(**kwargs)
                if REMOVE_RESPONSE_LINKS:
                    return remove_dict_key(response, "links")
                return response
            except (DemistoException, requests.ReadTimeout) as error:
                demisto.error(f"Error {error} running _http_request in time {_time}")
                if (
                    isinstance(error, DemistoException) and not isinstance(error.exception, requests.ConnectionError)
                ) or _time == self.connection_error_retries:
                    raise
                else:
                    time.sleep(1)  # pylint: disable=sleep-exists

        return None

    def _generate_session_id(self) -> str:
        """
        Generate a new session ID for internal API requests.

        Note:
            This is used for internal non-documented API requests that are used when using the web interface,
            and have no alternative in the native API.

        Returns:
            str: A session ID.
        """
        internal_api_headers = self._headers.copy()
        internal_api_headers["Content-Type"] = "application/x-www-form-urlencoded"

        return self._http_request(
            method="POST",
            full_url=self.base_url.rstrip("/") + "/data/user/login",
            headers=internal_api_headers,
            data={
                "nexposeccusername": self._auth_username,
                "nexposeccpassword": self._auth_password,
            },
            ok_codes=(200,),
        ).get("sessionID")

    def _paged_http_request(
        self, page_size: int | None = None, page: int | None = None, sort: str | None = None, limit: int | None = None, **kwargs
    ) -> list:
        """
        Run _http_request with pagination handling.

        Args:
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of items to return. None means to not use a limit.
                Defaults to None.
            **kwargs: Parameters to pass when calling `_http_request`.

        Returns:
            list: A list containing all paginated items.
        """
        if DEFAULT_PAGE_SIZE and not page_size:
            page_size = DEFAULT_PAGE_SIZE

        kwargs["params"] = kwargs.get("params", {})  # If `params` is None, set it to an empty dict

        if page:
            kwargs["params"]["page"] = str(page)

        kwargs["params"].update(
            find_valid_params(
                page=page,
                size=page_size,
            )
        )

        # If sort is not None, split it into a list and add to kwargs
        if sort:
            kwargs["params"]["sort"] = sort.split(sep=";")

        response: dict = self._http_request(**kwargs)
        result = response.get("resources", [])

        if not result:
            return []

        if not page:
            total_pages = response.get("page", {}).get("totalPages", 1)
            demisto.debug(f"Total pages = {total_pages}")
            page_count = 0

            # Note: page indexing on Nexpose's API starts at 0
            while (page_count + 1) < total_pages and (limit is None or len(result) < limit):
                page_count += 1
                kwargs["params"]["page"] = str(page_count)
                response = self._http_request(**kwargs)
                resources = response["resources"]
                demisto.debug(f"Received {len(resources)} resources with page {page_count=}, {page_size=}")
                result.extend(resources)

        if limit and limit < len(result):
            return result[:limit]

        return result

    def create_asset(
        self,
        site_id: str,
        date: str,
        ip_address: str | None = None,
        hostname: str | None = None,
        hostname_source: str | None = None,
    ) -> dict:
        """
        | Create a new asset on a site.
        |
        | https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createAsset

        Note:
            The API endpoint that's used has additional parameters, and can be also used to update existing assets.
            These options are currently not supported.

        Args:
            site_id (str): ID of the site to create the asset on.
            date (str): The date the data was collected on the asset.
            ip_address (str | None, optional): IP address of the asset to create.
            hostname (str | None, optional): Hostname of the asset to create.
            hostname_source (str | None, optional): Source of the hostname.

        Returns:
            dict: API response with information about the newly generated asset.
        """
        if ip_address is None and hostname is None:
            raise ValueError('At least one of "ip" and "host_name" arguments must be passed.')

        post_data: dict = {"date": date}

        if ip_address is not None:
            post_data["ip"] = ip_address

        if hostname is not None:
            post_data["hostName"] = {"name": hostname}

            if hostname_source is not None:
                post_data["hostName"]["source"] = hostname_source.lower()

        return self._http_request(
            method="POST",
            url_suffix=f"/sites/{site_id}/assets",
            json_data=post_data,
            resp_type="json",
        )

    def create_report(self, report_id: str) -> dict:
        """
        | Generates a configured report.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/generateReport

        Args:
            report_id (str): ID of the configured report to generate.

        Returns:
            dict: API response with information about the newly created report instance.
        """
        return self._http_request(
            url_suffix=f"/reports/{report_id}/generate",
            method="POST",
            resp_type="json",
        )

    def create_report_config(self, scope: dict[str, Any], template_id: str, report_name: str, report_format: str) -> dict:
        """
        | Create a new report configuration.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createReport

        Args:
            scope (dict[str, Any]): Scope of the report, see Nexpose's documentation for more details.
            template_id (str): ID of report template to use.
            report_name (str): Name for the report that will be generated.
            report_format (str): Format of the report that will be generated.

        Returns:
            dict: API response with information about the newly created report configuration.
        """
        post_data = {
            "scope": scope,
            "template": template_id,
            "name": report_name,
            "format": report_format.lower(),
        }

        return self._http_request(
            url_suffix="/reports",
            method="POST",
            json_data=post_data,
            resp_type="json",
        )

    def create_shared_credential(
        self,
        name: str,
        site_assignment: SharedCredentialSiteAssignment,
        service: CredentialService,
        database_name: str | None = None,
        description: str | None = None,
        domain: str | None = None,
        host_restriction: str | None = None,
        http_realm: str | None = None,
        notes_id_password: str | None = None,
        ntlm_hash: str | None = None,
        oracle_enumerate_sids: bool | None = None,
        oracle_listener_password: str | None = None,
        oracle_sid: str | None = None,
        password: str | None = None,
        port_restriction: str | None = None,
        sites: list[int] | None = None,
        snmp_community_name: str | None = None,
        snmpv3_authentication_type: SNMPv3AuthenticationType | None = None,
        snmpv3_privacy_password: str | None = None,
        snmpv3_privacy_type: SNMPv3PrivacyType | None = None,
        ssh_key_pem: str | None = None,
        ssh_permission_elevation: SSHElevationType | None = None,
        ssh_permission_elevation_password: str | None = None,
        ssh_permission_elevation_username: str | None = None,
        ssh_private_key_password: str | None = None,
        use_windows_authentication: bool | None = None,
        username: str | None = None,
    ) -> dict:
        """
        | Create a new shared credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSharedCredential

        Args:
            name (str): Name of the credential.
            site_assignment (SharedCredentialSiteAssignment): Site assignment configuration for the credential.
            service (CredentialService): Credential service type.
            database_name (str | None, optional): Database name.
            description (str | None, optional): Description for the credential.
            domain (str | None, optional): Domain address.
            host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
            http_realm (str | None, optional): HTTP realm.
            notes_id_password (str | None, optional):
                Password for the notes account that will be used for authenticating.
            ntlm_hash (str | None, optional): NTLM password hash.
            oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate
                SIDs from the environment.
            oracle_listener_password (str | None, optional): The Oracle Net Listener password.
                Used to enumerate SIDs from the environment.
            oracle_sid (str | None, optional): Oracle database name.
            password (str | None, optional): Password for the credential.
            port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
                on a specific port. Can be used only if `host_restriction` is used.
            sites (list[int] | None, optional): List of site IDs for the shared credential that are explicitly assigned
                access to the shared scan credential, allowing it to use the credential during a scan.
            snmp_community_name (str | None, optional): SNMP community for authentication.
            snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential.
            snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use.
            snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use.
            ssh_key_pem (str | None, optional): PEM formatted private key.
            ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans.
            ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
            ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
            ssh_private_key_password (str | None, optional): Password for the private key.
            use_windows_authentication (bool | None, optional): Whether to use Windows authentication.
            username (str | None, optional): Username for the credential.

        Returns:
            dict: API response with information about the newly created shared credential.
        """
        account_data = create_credential_creation_body(
            service=service,
            database_name=database_name,
            domain=domain,
            http_realm=http_realm,
            notes_id_password=notes_id_password,
            ntlm_hash=ntlm_hash,
            oracle_enumerate_sids=oracle_enumerate_sids,
            oracle_listener_password=oracle_listener_password,
            oracle_sid=oracle_sid,
            password=password,
            snmp_community_name=snmp_community_name,
            snmpv3_authentication_type=snmpv3_authentication_type,
            snmpv3_privacy_password=snmpv3_privacy_password,
            snmpv3_privacy_type=snmpv3_privacy_type,
            ssh_key_pem=ssh_key_pem,
            ssh_permission_elevation=ssh_permission_elevation,
            ssh_permission_elevation_password=ssh_permission_elevation_password,
            ssh_permission_elevation_username=ssh_permission_elevation_username,
            ssh_private_key_password=ssh_private_key_password,
            use_windows_authentication=use_windows_authentication,
            username=username,
        )

        post_data = find_valid_params(
            description=description,
            hostRestriction=host_restriction,
            name=name,
            siteAssignment=site_assignment.value,
        )

        if port_restriction is not None and host_restriction is not None:
            post_data["portRestriction"] = port_restriction

        if sites is not None and site_assignment == SharedCredentialSiteAssignment.SPECIFIC_SITES:
            post_data["sites"] = sites

        post_data["account"] = account_data

        return self._http_request(
            method="POST",
            url_suffix="/shared_credentials",
            json_data=post_data,
            resp_type="json",
        )

    def create_site(
        self,
        name: str,
        description: str | None = None,
        assets: list[str] | None = None,
        site_importance: str | None = None,
        template_id: str | None = None,
    ) -> dict:
        """
        | Create a new site.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSite

        Args:
            name (str): Name of the site. Must be unique.
            description (str | None, optional): Description of the site. Defaults to None.
            assets (list[str] | None, optional): List of asset IDs to be included in site scans. Defaults to None.
            site_importance (str | None, optional): Importance of the site.
                Defaults to None (results in using API's default - "normal").
            template_id (str | None, optional): The identifier of a scan template.
                Defaults to None (results in using default scan template).

        Returns:
            dict: API response with information about the newly created site.
        """
        post_data = find_valid_params(
            name=name,
            description=description,
            importance=site_importance.lower() if site_importance else None,
            scanTemplateId=template_id,
        )

        if assets:
            post_data["scan"] = {"assets": {"includedTargets": {"addresses": assets}}}

        return self._http_request(
            url_suffix="/sites",
            method="POST",
            json_data=post_data,
            resp_type="json",
        )

    def create_site_scan_credential(
        self,
        site_id: str,
        name: str,
        service: CredentialService,
        database_name: str | None = None,
        description: str | None = None,
        domain: str | None = None,
        host_restriction: str | None = None,
        http_realm: str | None = None,
        notes_id_password: str | None = None,
        ntlm_hash: str | None = None,
        oracle_enumerate_sids: bool | None = None,
        oracle_listener_password: str | None = None,
        oracle_sid: str | None = None,
        password: str | None = None,
        port_restriction: str | None = None,
        snmp_community_name: str | None = None,
        snmpv3_authentication_type: SNMPv3AuthenticationType | None = None,
        snmpv3_privacy_password: str | None = None,
        snmpv3_privacy_type: SNMPv3PrivacyType | None = None,
        ssh_key_pem: str | None = None,
        ssh_permission_elevation: SSHElevationType | None = None,
        ssh_permission_elevation_password: str | None = None,
        ssh_permission_elevation_username: str | None = None,
        ssh_private_key_password: str | None = None,
        use_windows_authentication: bool | None = None,
        username: str | None = None,
    ) -> dict:
        """
        | Create a new site scan credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteCredential

        Args:
            name (str): Name of the credential.
                Assign the shared scan credential either to be available to all sites, or a specific list of sites.
            site_id (str): ID of the site to create the credential for.
            service (CredentialService): Credential service type.
            database_name (str | None, optional): Database name.
            description (str | None, optional): Description for the credential.
            domain (str | None, optional): Domain address.
            host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
            http_realm (str | None, optional): HTTP realm.
            notes_id_password (str | None, optional):
                Password for the notes account that will be used for authenticating.
            ntlm_hash (str | None, optional): NTLM password hash.
            oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate
                SIDs from the environment.
            oracle_listener_password (str | None, optional): The Oracle Net Listener password.
                Used to enumerate SIDs from the environment.
            oracle_sid (str | None, optional): Oracle database name.
            password (str | None, optional): Password for the credential.
            port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
                on a specific port. Can be used only if `host_restriction` is used.
            snmp_community_name (str | None, optional): SNMP community for authentication.
            snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential.
            snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use.
            snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use.
            ssh_key_pem (str | None, optional): PEM formatted private key.
            ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans.
            ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
            ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
            ssh_private_key_password (str | None, optional): Password for the private key.
            use_windows_authentication (bool | None, optional): Whether to use Windows authentication.
            username (str | None, optional): Username for the credential.

        Returns:
            dict: API response with information about the newly created shared credential.
        """
        account_data = create_credential_creation_body(
            service=service,
            database_name=database_name,
            domain=domain,
            http_realm=http_realm,
            notes_id_password=notes_id_password,
            ntlm_hash=ntlm_hash,
            oracle_enumerate_sids=oracle_enumerate_sids,
            oracle_listener_password=oracle_listener_password,
            oracle_sid=oracle_sid,
            password=password,
            snmp_community_name=snmp_community_name,
            snmpv3_authentication_type=snmpv3_authentication_type,
            snmpv3_privacy_password=snmpv3_privacy_password,
            snmpv3_privacy_type=snmpv3_privacy_type,
            ssh_key_pem=ssh_key_pem,
            ssh_permission_elevation=ssh_permission_elevation,
            ssh_permission_elevation_password=ssh_permission_elevation_password,
            ssh_permission_elevation_username=ssh_permission_elevation_username,
            ssh_private_key_password=ssh_private_key_password,
            use_windows_authentication=use_windows_authentication,
            username=username,
        )

        post_data = find_valid_params(
            description=description,
            hostRestriction=host_restriction,
            name=name,
        )

        if port_restriction is not None and host_restriction is not None:
            post_data["portRestriction"] = port_restriction

        post_data["account"] = account_data

        return self._http_request(
            method="POST",
            url_suffix=f"/sites/{site_id}/site_credentials",
            json_data=post_data,
            resp_type="json",
        )

    def create_site_scan_schedule(
        self,
        site_id: str,
        start_date: str,
        enabled: bool,
        excluded_asset_groups: list[int] | None = None,
        excluded_targets: list[str] | None = None,
        included_asset_groups: list[int] | None = None,
        included_targets: list[str] | None = None,
        duration: str | None = None,
        repeat_behaviour: str | None = None,
        frequency: RepeatFrequencyType | None = None,
        interval: int | None = None,
        date_of_month: int | None = None,
        scan_name: str | None = None,
        scan_template_id: str | None = None,
    ) -> dict:
        """
        | Create a new site scan schedule.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteScanSchedule

        Args:
            site_id (str): ID of the site to create a new scheduled scan for.
            start_date (str): The scheduled start date and time formatted in ISO 8601 format.
            enabled (bool): A flag indicating whether the scan schedule is enabled.
            excluded_asset_groups (list[int] | None, optional): Asset groups to exclude from the scan.
            excluded_targets (list[str] | None, optional): Addresses to exclude from the scan.
                Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range,
                ipv6 address, or CIDR notation.
            included_asset_groups (list[int] | None, optional): Asset groups to include in the scan.
            included_targets (list[str] | None, optional): Addresses to include in the scan.
                Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range,
                ipv6 address, or CIDR notation.
            duration (str | None, optional): An ISO 8601 formatted duration string that Specifies the maximum duration
                the scheduled scan is allowed to run.
            repeat_behaviour (str | None, optional): The desired behavior of a repeating scheduled scan
                when the previous scan was paused due to reaching its maximum duration.
            frequency (RepeatFrequencyType | None, optional): Frequency for the schedule to repeat.
                Required if using other repeat settings.
            interval (int | None, optional): The interval time the schedule should repeat.
                Required if using other repeat settings.
            date_of_month(int | None, optional): Specifies the schedule repeat day of the interval month.
                Required and used only if frequency is set to "DATE_OF_MONTH".
            scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule.
                If not explicitly set in the schedule, the scan name will be generated prior to the scan launching.
            scan_template_id (str | None, optional): ID of the scan template to use.

        Returns:
            dict: API response with information about the newly created scan schedule.
        """
        assets: dict = {}
        repeat: dict = {}

        if excluded_asset_groups:
            assets["excludedAssetGroups"] = {"assetGroupIDs": excluded_asset_groups}

        if excluded_targets:
            assets["excludedTargets"] = {"addresses": excluded_targets}

        if included_asset_groups:
            assets["includedAssetGroups"] = {"assetGroupIDs": included_asset_groups}

        if included_targets:
            assets["includedTargets"] = {"addresses": included_targets}

        if frequency is not None:
            if interval is None:
                raise ValueError("'interval' parameter must be set when frequency is used.")

            if frequency == RepeatFrequencyType.DATE_OF_MONTH and date_of_month is None:
                raise ValueError("'date-of-month' parameter must be set if frequency is set to 'Date of month'.")

            repeat["every"] = frequency.value

        repeat.update(
            find_valid_params(
                interval=interval,
                dateOfMonth=date_of_month,
            )
        )

        post_data = find_valid_params(
            duration=duration,
            enabled=enabled,
            onScanRepeat=repeat_behaviour.lower() if repeat_behaviour is not None else None,
            scanName=scan_name,
            scanTemplateId=scan_template_id,
            start=start_date,
        )

        post_data.update(
            find_valid_params(
                strict_mode=True,
                assets=assets,
                repeat=repeat,
            )
        )

        return self._http_request(
            url_suffix=f"/sites/{site_id}/scan_schedules",
            method="POST",
            json_data=post_data,
            resp_type="json",
        )

    def create_vulnerability_exception(
        self,
        vulnerability_id: str,
        scope_type: VulnerabilityExceptionScopeType,
        state: str,
        reason: str,
        scope_id: int | None = None,
        expires: str | None = None,
        comment: str | None = None,
    ) -> dict:
        """
        | Create a new vulnerability exception.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createVulnerabilityException

        Args:
            vulnerability_id (str): ID of the vulnerability to create an exception for.
            scope_type (VulnerabilityExceptionScopeType): The type of the exception scope.
            state (str): The state of the vulnerability exception.
            reason (str): The reason the vulnerability exception was submitted.
                Can be one of: "False Positive", "Compensating Control", "Acceptable Use",
                "Acceptable Risk", and "Other".
            scope_id (int): ID of the chosen `scope_type` (site ID, asset ID, etc.).
                Required if `scope_type` is anything other than `Global`
            expires (str | None, optional): The date and time the vulnerability exception is set to expire.
            comment (str | None, optional): A comment from the submitter as to why the exception was submitted.

        Returns:
            dict: API response with information about the newly created vulnerability exception.
        """
        scope_obj = {
            "id": scope_id,
            "type": scope_type.value,
            "vulnerability": vulnerability_id,
        }

        submit_obj = find_valid_params(
            reason=reason,
            comment=comment,
        )

        post_data = find_valid_params(
            expires=expires,
            scope=scope_obj,
            state=state,
            submit=submit_obj,
        )

        return self._http_request(
            url_suffix="/vulnerability_exceptions",
            method="POST",
            json_data=post_data,
            resp_type="json",
        )

    def delete_asset(self, asset_id: str) -> dict:
        """
        | Delete an asset.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteAsset

        Args:
            asset_id (str): ID of the asset to delete.

        Returns:
            dict: API response.
        """
        return self._http_request(
            method="DELETE",
            url_suffix=f"/assets/{asset_id}",
            resp_type="json",
        )

    def delete_scan_schedule(self, site_id: str, scheduled_scan_id: str) -> dict:
        """
        | Delete a scheduled scan.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSiteScanSchedule

        Args:
            site_id (str): ID of the site to delete the scheduled scan from.
            scheduled_scan_id (str): ID of the scheduled scan to delete.

        Returns:
            dict: API response.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/scan_schedules/{scheduled_scan_id}",
            method="DELETE",
            resp_type="json",
        )

    def delete_site(self, site_id: str) -> dict:
        """
        | Delete a site.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSite

        Args:
            site_id (str): ID of the site to delete.

        Returns:
            dict: API response with information about the deleted site.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}",
            method="DELETE",
            resp_type="json",
        )

    def delete_shared_credential(self, shared_credential_id: str) -> dict:
        """
        | Delete a shared credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSharedCredential

        Args:
            shared_credential_id (str): ID of the shared credential to delete.

        Returns:
            dict: API response with information about the deleted shared credential.
        """
        return self._http_request(
            url_suffix=f"/shared_credentials/{shared_credential_id}",
            method="DELETE",
            resp_type="json",
        )

    def delete_site_scan_credential(self, site_id: str, site_credential_id: str) -> dict:
        """
        | Delete a site scan credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSiteCredential

        Args:
            site_id (str): ID of the site to delete the scan credential from.
            site_credential_id (str): ID of the scan credential to delete.

        Returns:
            dict: API response with information about the deleted site scan credential.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/site_credentials/{site_credential_id}",
            method="DELETE",
            resp_type="json",
        )

    def delete_vulnerability_exception(self, vulnerability_exception_id: str) -> dict:
        """
        | Delete a vulnerability exception.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/removeVulnerabilityException

        Args:
            vulnerability_exception_id (str): ID of the vulnerability exception to delete.

        Returns:
            dict: API response with information about the deleted vulnerability exception.
        """

        return self._http_request(
            url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}",
            method="DELETE",
            resp_type="json",
        )

    def download_report(self, report_id: str, instance_id: str) -> bytes:
        """
        | Download a report.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/downloadReport

        Args:
            report_id (str): ID of the report to download.
            instance_id (str): ID of the report instance.

        Returns:
            bytes: Report file in bytes.
        """

        request_headers = self._headers.copy()
        request_headers["Accept"] = "*/*"

        return self._http_request(
            url_suffix=f"/reports/{report_id}/history/{instance_id}/output",
            method="GET",
            headers=request_headers,
            resp_type="content",
        )

    def get_asset_vulnerability(self, asset_id: str, vulnerability_id: str) -> dict:
        """
        | Retrieve information about vulnerability findings on an asset.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerability

        Args:
            asset_id (str): ID of the asset to retrieve information about.
            vulnerability_id (str): ID of the vulnerability to look for.

        Returns:
            dict: API response with information about vulnerability findings on the asset.
        """
        return self._http_request(
            url_suffix=f"/assets/{asset_id}/vulnerabilities/{vulnerability_id}",
            method="GET",
            resp_type="json",
        )

    def get_asset_vulnerabilities(
        self,
        asset_id: str,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieves a list of all vulnerability findings on an asset.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerabilities

        Args:
            asset_id (str): ID of the site to retrieve linked assets from.
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of assets to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list with all vulnerability findings on an asset.
        """
        return self._paged_http_request(
            url_suffix=f"/assets/{asset_id}/vulnerabilities",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_asset(self, asset_id: str) -> dict:
        """
        | Retrieve information about a specific asset.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAsset

        Args:
            asset_id (str): ID of the asset to retrieve information about.

        Returns:
            dict: API response with information about a specific asset.
        """
        return self._http_request(
            url_suffix=f"/assets/{asset_id}",
            method="GET",
            resp_type="json",
        )

    def get_asset_tags(self, asset_id: str) -> dict:
        """
        | Retrieve tags about a specific asset.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetTags

        Args:
            asset_id (str): ID of the asset to retrieve information about.

        Returns:
            dict: API response with list of tags about a specific asset.
        """
        return self._http_request(
            url_suffix=f"/assets/{asset_id}/tags",
            method="GET",
            resp_type="json",
        )

    def get_asset_vulnerability_solution(self, asset_id: str, vulnerability_id: str) -> dict:
        """
        | Retrieve information about solutions that can be used to remediate a vulnerability on an asset.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerabilitySolutions

        Args:
            asset_id (str): ID of the asset to retrieve solutions for.
            vulnerability_id (str): ID of the vulnerability to retrieve solutions for.

        Returns:
            dict: API response with information about solutions that can be used to remediate
                a vulnerability on an asset.
        """
        return self._http_request(
            url_suffix=f"/assets/{asset_id}/vulnerabilities/{vulnerability_id}/solution",
            method="GET",
            resp_type="json",
        )

    def get_assets(
        self,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of all assets.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssets

        Args:
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of assets to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list of all assets (up to a limit, if set).
        """
        return self._paged_http_request(
            url_suffix="/assets",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_assigned_shared_credentials(self, site_id: str) -> dict:
        """
        | Retrieve information about all credentials that are shared with a specific site.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteSharedCredentials

        Args:
            site_id (str): ID of the site to retrieve credentials that are shared with.

        Returns:
            dict: API response with information shared credentials that are shared with a specific site.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/shared_credentials",
            method="GET",
            resp_type="json",
        ).get("resources")

    def get_report_history(self, report_id: str, instance_id: str) -> dict:
        """
        | Retrieve information about a generated report.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getReportInstance

        Args:
            report_id (str): ID of the report to retrieve information about.
            instance_id (str): ID of the report instance to retrieve information about.

        Returns:
            dict: API response with information about the generated report.
        """
        return self._http_request(
            url_suffix=f"/reports/{report_id}/history/{instance_id}",
            method="GET",
            resp_type="json",
        )

    def get_report_templates(self) -> dict:
        """
        | Retrieve a list of all available report templates.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getReportTemplates

        Returns:
            dict: API response with information about all available report templates.
        """
        return self._http_request(
            url_suffix="/report_templates",
            method="GET",
            resp_type="json",
        )

    def get_scan(self, scan_id: str) -> dict:
        """
        | Retrieve information about a specific scan.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getScan

        Args:
            scan_id (str): ID of the scan to retrieve.

        Returns:
            dict: API response with information about a specific scan.
        """
        return self._http_request(
            url_suffix=f"/scans/{scan_id}",
            method="GET",
            resp_type="json",
        )

    def get_scans(
        self,
        active: bool | None = False,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of all scans.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getScans

        Args:
            active (bool | None, optional): Whether to return active scans or not. Defaults to False.
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of scans to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list of all scans (up to a limit, if set).
        """
        params = {"active": active} if active is not None else {}

        return self._paged_http_request(
            url_suffix="/scans",
            method="GET",
            params=params,
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_scan_schedule(self, site_id: str, schedule_id: str) -> dict:
        """
        | Retrieve information about a specific scan schedule.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScanSchedule

        Args:
            site_id (str): ID of the site to retrieve scan schedule from.
            schedule_id (str): ID of the scan schedule to retrieve.

        Returns:
            dict: A dictionary containing information about the scan schedule.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/scan_schedules/{schedule_id}",
            method="GET",
            resp_type="json",
        )

    def get_scan_schedules(self, site_id: str) -> list[dict]:
        """
        | Retrieve information about scan schedules for a specific site.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScanSchedules

        Args:
            site_id (str): ID of the site to retrieve scan schedules from.

        Returns:
            list[dict]: A list of scan schedules for the site.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/scan_schedules",
            method="GET",
            resp_type="json",
        ).get("resources")

    def get_shared_credential(self, credential_id: str) -> dict:
        """
        | Retrieve information about a specific shared credential.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSharedCredential

        Args:
            credential_id (str): ID of the shared credential to retrieve information about.

        Returns:
            dict: API response with information about a specific shared credential.
        """
        return self._http_request(
            url_suffix=f"/shared_credentials/{credential_id}",
            method="GET",
            resp_type="json",
        )

    def get_shared_credentials(self) -> list[dict]:
        """
        | Retrieve information about all shared credentials.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSharedCredentials

        Returns:
            list[dict]: A list with all shared credentials.
        """
        return self._http_request(
            url_suffix="/shared_credentials",
            method="GET",
            resp_type="json",
        ).get("resources")

    def get_site_assets(
        self,
        site_id: str,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of all assets that are linked with a specific site.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteAssets

        Args:
            site_id (str): ID of the site to retrieve linked assets from.
            page_size (int | None, optional): Number of assets to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of sites to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list with all assets that are linked with a specific site (up to a limit, if set).
        """
        return self._paged_http_request(
            url_suffix=f"/sites/{site_id}/assets",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_site_scan_credential(self, site_id: str, credential_id: str) -> dict:
        """
        | Retrieve information about a specific site scan credential.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteCredential

        Args:
            site_id (str): ID of the site to retrieve scan credentials from.
            credential_id (str): ID of the scan credential to retrieve.

        Returns:
            dict: API response with information about a specific site scan credential.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/site_credentials/{credential_id}",
            method="GET",
            resp_type="json",
        )

    def get_site_scan_credentials(self, site_id: str) -> list[dict]:
        """
        | Retrieve information about a specific site scan credential.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteCredentials

        Args:
            site_id (str): ID of the site to retrieve scan credentials from.

        Returns:
            list[dict]: A list with information about all site scan credentials.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/site_credentials",
            method="GET",
            resp_type="json",
        ).get("resources")

    def get_site_scans(
        self,
        site_id: str,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of scans from a specific site.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScans

        Args:
            site_id (str): ID of the site to retrieve scans from.
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of scans to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: API response with information about all scans from the specific site.
        """
        return self._paged_http_request(
            url_suffix=f"/sites/{site_id}/scans",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_sites(
        self,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of sites.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSites

        Args:
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of sites to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list of sites (up to a limit, if set).
        """
        return self._paged_http_request(
            url_suffix="/sites",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_vulnerabilities(
        self,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve information about all existing vulnerabilities.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilities

        Args:
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of sites to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list of sites (up to a limit, if set).
        """
        return self._paged_http_request(
            url_suffix="/vulnerabilities",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_vulnerability(self, vulnerability_id: str) -> dict:
        """
        | Retrieve information about a specific vulnerability.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerability

        Args:
            vulnerability_id (str): ID of the vulnerability to retrieve information about.

        Returns:
            dict: API response with information about a specific vulnerability.
        """
        return self._http_request(
            url_suffix=f"/vulnerabilities/{vulnerability_id}",
            method="GET",
            resp_type="json",
        )

    def get_vulnerability_exception(self, vulnerability_exception_id: str) -> dict:
        """
        | Retrieve information about an exception made on a vulnerability.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilityException

        Args:
            vulnerability_exception_id (str): ID of the vulnerability exception to retrieve information about.

        Returns:
            dict: API response with information about a specific exception made on a vulnerability.
        """
        return self._http_request(
            url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}",
            method="GET",
            resp_type="json",
        )

    def get_vulnerability_exceptions(
        self,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve exceptions defined on vulnerabilities.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilityExceptions

        Args:
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of sites to return. None means to not use a limit.
        """
        return self._paged_http_request(
            url_suffix="/vulnerability_exceptions",
            method="GET",
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def search_assets(
        self,
        filters: list[dict] | None = None,
        match: str = MATCH_DEFAULT_VALUE,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        """
        | Retrieve a list of all assets with access permissions that match the provided search filters.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/findAssets

        Args:
            filters (list[dict] | None, optional): List of filters to use for searching assets. Defaults to None.
            match (str): Determine if the filters should match all or any of the filters.
                Can be either "all" or "any". Defaults to MATCH_DEFAULT_VALUE.
            page_size (int | None, optional): Number of scans to return per page when using pagination.
                Defaults to DEFAULT_PAGE_SIZE.
            page (int | None, optional): Specific pagination page to retrieve. Defaults to None.
            sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format.
                Defaults to None.
            limit (int | None, optional): Limit the number of sites to return. None means to not use a limit.
                Defaults to None.

        Returns:
            list[dict]: A list of assets (up to a limit, if set) matching the filters.
        """
        post_data = find_valid_params(
            filters=filters,
            match=match,
        )

        return self._paged_http_request(
            url_suffix="/assets/search",
            method="POST",
            json_data=post_data,
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def set_assigned_shared_credential_status(self, site_id: str, shared_credential_id: str, enabled: bool) -> dict:
        """
        | Update the status of a shared credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/enableSharedCredentialOnSite

        Args:
            site_id (str): ID of the site to update the shared credential status on.
            shared_credential_id (str): ID of the shared credential to update the status of.
            enabled (bool): A flag indicating whether the shared credential should be enabled or not.

        Returns:
            dict: API response with information about the updated shared credential.
        """
        return self._http_request(
            url_suffix=f"/sites/{site_id}/shared_credentials/{shared_credential_id}/enabled",
            method="PUT",
            data=json.dumps(enabled),  # type: ignore
            resp_type="json",
        )

    def update_scan_schedule(
        self,
        site_id: str,
        scan_schedule_id: int,
        repeat_behaviour: str,
        start_date: str,
        enabled: bool,
        excluded_asset_groups: list[int] | None = None,
        excluded_targets: list[str] | None = None,
        included_asset_groups: list[int] | None = None,
        included_targets: list[str] | None = None,
        duration: str | None = None,
        frequency: RepeatFrequencyType | None = None,
        interval: int | None = None,
        date_of_month: int | None = None,
        scan_name: str | None = None,
        scan_template_id: str | None = None,
    ) -> dict:
        """
        | Update a site scan schedule.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSiteScanSchedule

        Args:
            site_id (str): ID of the site to create a new scheduled scan for.
            scan_schedule_id (int): ID of the scan schedule to update.
            repeat_behaviour (str | None, optional): The desired behavior of a repeating scheduled scan
                when the previous scan was paused due to reaching its maximum duration.
            start_date (str): The scheduled start date and time formatted in ISO 8601 format.
            enabled (bool): A flag indicating whether the scan schedule is enabled.
            excluded_asset_groups (list[int] | None, optional): Asset groups to exclude from the scan.
            excluded_targets (list[str] | None, optional): Addresses to exclude from the scan.
                Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range,
                ipv6 address, or CIDR notation.
            included_asset_groups (list[int] | None, optional): Asset groups to include in the scan.
            included_targets (list[str] | None, optional): Addresses to include in the scan.
                Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range,
                ipv6 address, or CIDR notation.
            duration (str | None, optional): An ISO 8601 formatted duration string that Specifies the maximum duration
                the scheduled scan is allowed to run.
            frequency (RepeatFrequencyType | None, optional): Frequency for the schedule to repeat.
                Required if using other repeat settings.
            interval (int | None, optional): The interval time the schedule should repeat.
                Required if using other repeat settings.
            date_of_month(int | None, optional): Specifies the schedule repeat day of the interval month.
                Required and used only if frequency is set to `DATE_OF_MONTH`.
            scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule.
                If not explicitly set in the schedule, the scan name will be generated prior to the scan launching.
            scan_template_id (str | None, optional): ID of the scan template to use.

        Returns:
            str: ID of the newly created scan schedule.
        """
        assets: dict = {}
        repeat: dict = {}

        if excluded_asset_groups:
            assets["excludedAssetGroups"] = {"assetGroupIDs": excluded_asset_groups}

        if excluded_targets:
            assets["excludedTargets"] = {"addresses": excluded_targets}

        if included_asset_groups:
            assets["includedAssetGroups"] = {"assetGroupIDs": included_asset_groups}

        if included_targets:
            assets["includedTargets"] = {"addresses": included_targets}

        if frequency is not None:
            if interval is None:
                raise ValueError("'interval' parameter must be set when frequency is used.")

            if frequency == RepeatFrequencyType.DATE_OF_MONTH and date_of_month is None:
                raise ValueError("'date-of-month' parameter must be set if frequency is set to 'Date of month'.")

            repeat["every"] = frequency.value

        repeat.update(
            find_valid_params(
                interval=interval,
                dateOfMonth=date_of_month,
            )
        )

        post_data = find_valid_params(
            duration=duration,
            enabled=enabled,
            onScanRepeat=repeat_behaviour.lower(),
            scanName=scan_name,
            scanTemplateId=scan_template_id,
            start=start_date,
        )

        post_data.update(
            find_valid_params(
                strict_mode=True,
                assets=assets,
                repeat=repeat,
            )
        )

        return self._http_request(
            url_suffix=f"/sites/{site_id}/scan_schedules/{scan_schedule_id}",
            method="PUT",
            json_data=post_data,
            resp_type="json",
        )

    def start_site_scan(self, site_id: str, scan_name: str, hosts: list[str]) -> dict:
        """
        | Start a scan for a specific site.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/startScan

        Args:
            site_id (str): ID of the site to start a scan on.
            scan_name (str): Name to set for the new scan.
            hosts (list[str]): Hosts to scan.

        Returns:
            dict: API response with information about the started scan.
        """
        post_data: dict = {
            "name": scan_name,
        }

        if hosts:
            post_data["hosts"] = hosts

        return self._http_request(
            url_suffix=f"/sites/{site_id}/scans",
            method="POST",
            json_data=post_data,
            resp_type="json",
        )

    def update_scan_status(self, scan_id: str, status: ScanStatus) -> dict:
        """
        | Update status for a specific scan.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/setScanStatus

        Args:
            scan_id (str): ID of the scan to update.
            status (ScanStatus): Status to set the scan to.
        """
        return self._http_request(
            url_suffix=f"/scans/{scan_id}/{status.value}",
            method="POST",
            resp_type="json",
        )

    def update_shared_credential(
        self,
        shared_credential_id: str,
        name: str,
        site_assignment: SharedCredentialSiteAssignment,
        service: CredentialService,
        database_name: str | None = None,
        description: str | None = None,
        domain: str | None = None,
        host_restriction: str | None = None,
        http_realm: str | None = None,
        notes_id_password: str | None = None,
        ntlm_hash: str | None = None,
        oracle_enumerate_sids: bool | None = None,
        oracle_listener_password: str | None = None,
        oracle_sid: str | None = None,
        password: str | None = None,
        port_restriction: str | None = None,
        sites: list[int] | None = None,
        snmp_community_name: str | None = None,
        snmpv3_authentication_type: SNMPv3AuthenticationType | None = None,
        snmpv3_privacy_password: str | None = None,
        snmpv3_privacy_type: SNMPv3PrivacyType | None = None,
        ssh_key_pem: str | None = None,
        ssh_permission_elevation: SSHElevationType | None = None,
        ssh_permission_elevation_password: str | None = None,
        ssh_permission_elevation_username: str | None = None,
        ssh_private_key_password: str | None = None,
        use_windows_authentication: bool | None = None,
        username: str | None = None,
    ) -> dict:
        """
        | Update an existing new shared credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSharedCredential

        Args:
            shared_credential_id (str): ID of the shared credential to update.
            name (str): Name of the credential.
            site_assignment (SharedCredentialSiteAssignment): Site assignment configuration for the credential.
            service (CredentialService): Credential service type.
            database_name (str | None, optional): Database name.
            description (str | None, optional): Description for the credential.
            domain (str | None, optional): Domain address.
            host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
            http_realm (str | None, optional): HTTP realm.
            notes_id_password (str | None, optional):
            Password for the notes account that will be used for authenticating.
            ntlm_hash (str | None, optional): NTLM password hash.
            oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate
                SIDs from the environment.
            oracle_listener_password (str | None, optional): The Oracle Net Listener password.
                Used to enumerate SIDs from the environment.
            oracle_sid (str | None, optional): Oracle database name.
            password (str | None, optional): Password for the credential.
            port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
                on a specific port. Can be used only if `host_restriction` is used.
            sites (list[int] | None, optional): List of site IDs for the shared credential that are explicitly assigned
                access to the shared scan credential, allowing it to use the credential during a scan.
            snmp_community_name (str | None, optional): SNMP community for authentication.
            snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential.
            snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use.
            snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use.
            ssh_key_pem (str | None, optional): PEM formatted private key.
            ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans.
            ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
            ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
            ssh_private_key_password (str | None, optional): Password for the private key.
            use_windows_authentication (bool | None, optional): Whether to use Windows authentication.
            username (str | None, optional): Username for the credential.

        Returns:
            dict: API response with information about the newly created shared credential.
        """
        account_data = create_credential_creation_body(
            service=service,
            database_name=database_name,
            domain=domain,
            http_realm=http_realm,
            notes_id_password=notes_id_password,
            ntlm_hash=ntlm_hash,
            oracle_enumerate_sids=oracle_enumerate_sids,
            oracle_listener_password=oracle_listener_password,
            oracle_sid=oracle_sid,
            password=password,
            snmp_community_name=snmp_community_name,
            snmpv3_authentication_type=snmpv3_authentication_type,
            snmpv3_privacy_password=snmpv3_privacy_password,
            snmpv3_privacy_type=snmpv3_privacy_type,
            ssh_key_pem=ssh_key_pem,
            ssh_permission_elevation=ssh_permission_elevation,
            ssh_permission_elevation_password=ssh_permission_elevation_password,
            ssh_permission_elevation_username=ssh_permission_elevation_username,
            ssh_private_key_password=ssh_private_key_password,
            use_windows_authentication=use_windows_authentication,
            username=username,
        )

        post_data = find_valid_params(
            description=description,
            hostRestriction=host_restriction,
            name=name,
            siteAssignment=site_assignment.value,
        )

        if port_restriction is not None and host_restriction is not None:
            post_data["portRestriction"] = port_restriction

        if sites is not None and site_assignment == SharedCredentialSiteAssignment.SPECIFIC_SITES:
            post_data["sites"] = sites

        post_data["account"] = account_data

        return self._http_request(
            method="PUT",
            url_suffix=f"/shared_credentials/{shared_credential_id}",
            json_data=post_data,
            resp_type="json",
        )

    def update_site_scan_credential(
        self,
        site_id: str,
        credential_id: str,
        name: str,
        service: CredentialService,
        database_name: str | None = None,
        description: str | None = None,
        domain: str | None = None,
        host_restriction: str | None = None,
        http_realm: str | None = None,
        notes_id_password: str | None = None,
        ntlm_hash: str | None = None,
        oracle_enumerate_sids: bool | None = None,
        oracle_listener_password: str | None = None,
        oracle_sid: str | None = None,
        password: str | None = None,
        port_restriction: str | None = None,
        snmp_community_name: str | None = None,
        snmpv3_authentication_type: SNMPv3AuthenticationType | None = None,
        snmpv3_privacy_password: str | None = None,
        snmpv3_privacy_type: SNMPv3PrivacyType | None = None,
        ssh_key_pem: str | None = None,
        ssh_permission_elevation: SSHElevationType | None = None,
        ssh_permission_elevation_password: str | None = None,
        ssh_permission_elevation_username: str | None = None,
        ssh_private_key_password: str | None = None,
        use_windows_authentication: bool | None = None,
        username: str | None = None,
    ) -> dict:
        """
        | Update an existing site scan credential.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSiteCredential

        Args:
            name (str): Name of the credential.
                Assign the shared scan credential either to be available to all sites, or a specific list of sites.
            site_id (str): ID of the site to create the credential for.
            credential_id (str): ID of the site scan credential to update.
            service (CredentialService): Credential service type.
            database_name (str | None, optional): Database name.
            description (str | None, optional): Description for the credential.
            domain (str | None, optional): Domain address.
            host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
            http_realm (str | None, optional): HTTP realm.
            notes_id_password (str | None, optional):
                Password for the notes account that will be used for authenticating.
            ntlm_hash (str | None, optional): NTLM password hash.
            oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate
                SIDs from the environment.
            oracle_listener_password (str | None, optional): The Oracle Net Listener password.
                Used to enumerate SIDs from the environment.
            oracle_sid (str | None, optional): Oracle database name.
            password (str | None, optional): Password for the credential.
            port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
                on a specific port. Can be used only if `host_restriction` is used.
            snmp_community_name (str | None, optional): SNMP community for authentication.
            snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential.
            snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use.
            snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use.
            ssh_key_pem (str | None, optional): PEM formatted private key.
            ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans.
            ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
            ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
            ssh_private_key_password (str | None, optional): Password for the private key.
            use_windows_authentication (bool | None, optional): Whether to use Windows authentication.
            username (str | None, optional): Username for the credential.

        Returns:
            dict: API response with information about the newly created shared credential.
        """
        account_data = create_credential_creation_body(
            service=service,
            database_name=database_name,
            domain=domain,
            http_realm=http_realm,
            notes_id_password=notes_id_password,
            ntlm_hash=ntlm_hash,
            oracle_enumerate_sids=oracle_enumerate_sids,
            oracle_listener_password=oracle_listener_password,
            oracle_sid=oracle_sid,
            password=password,
            snmp_community_name=snmp_community_name,
            snmpv3_authentication_type=snmpv3_authentication_type,
            snmpv3_privacy_password=snmpv3_privacy_password,
            snmpv3_privacy_type=snmpv3_privacy_type,
            ssh_key_pem=ssh_key_pem,
            ssh_permission_elevation=ssh_permission_elevation,
            ssh_permission_elevation_password=ssh_permission_elevation_password,
            ssh_permission_elevation_username=ssh_permission_elevation_username,
            ssh_private_key_password=ssh_private_key_password,
            use_windows_authentication=use_windows_authentication,
            username=username,
        )

        post_data = find_valid_params(
            description=description,
            hostRestriction=host_restriction,
            name=name,
        )

        post_data["id"] = credential_id

        if port_restriction is not None and host_restriction is not None:
            post_data["portRestriction"] = port_restriction

        post_data["account"] = account_data

        return self._http_request(
            method="PUT",
            url_suffix=f"/sites/{site_id}/site_credentials/{credential_id}",
            json_data=post_data,
            resp_type="json",
        )

    def update_vulnerability_exception_status(self, vulnerability_exception_id: str, status: str) -> dict:
        """
        | Update the status of a vulnerability exception.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateVulnerabilityExceptionStatus

        Args:
            vulnerability_exception_id (str): ID of the vulnerability exception to update.
            status (str): Status to set the vulnerability exception to.

        Returns:
            dict: API response with information about the updated vulnerability exception.
        """
        return self._http_request(
            url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/{status.lower()}",
            method="POST",
            resp_type="json",
        )

    def update_vulnerability_exception_expiration(self, vulnerability_exception_id: str, expiration_date: str) -> dict:
        """
        | Update the expiration date for a vulnerability exception.
        |
        | For more information see:
            https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateVulnerabilityExceptionExpiration

        Args:
            vulnerability_exception_id (str): ID of the vulnerability exception to update.
            expiration_date (str): The new expiration date for the vulnerability exception.

        Returns:
            dict: API response with information about the updated vulnerability exception.
        """
        return self._http_request(
            url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/expires",
            method="PUT",
            data=json.dumps(expiration_date),
            resp_type="json",
        )

    def find_asset_site(self, asset_id: str) -> Optional["Site"]:
        """
        Find the site of a given asset ID.

        Note:
            This method is from older versions of this pack. It uses an internal API to fetch a list of scans data
            for an asset, and fetches site data for these scans to determine asset's site. This will not work if:
            The asset has no previous scans, or if the asset has been moved to another site since the last scan.

        Args:
            asset_id (str): ID of the asset to find additional data for.

        Returns:
            Site: Site object containing data (ID, name) of the asset's site.
        """
        request_headers = self._headers.copy()
        request_headers.update({"nexposeCCSessionID": self._generate_session_id()})

        try:
            response_data: dict = self._http_request(
                full_url=self.base_url.rstrip("/") + f"/data/assets/{asset_id}/scans",
                method="POST",
                headers=request_headers,
                resp_type="json",
            )

        except Exception:
            return None

        finally:
            self._session.cookies.clear()  # Remove cookies received and saved to session be this request.

        if not response_data.get("records"):
            return None

        record_data = response_data["records"][0]

        if None in (record_data.get("siteID"), record_data.get("siteName")):
            return None

        return Site(
            site_id=str(response_data["records"][0]["siteID"]),
            site_name=str(response_data["records"][0]["siteName"]),
        )

    def find_site_id(self, name: str) -> str | None:
        """
        Find a site ID by its name.

        Returns:
            str | None: Site ID corresponding to the passed name. None if no match was found.
        """
        for site in self.get_sites():
            if site["name"] == name:
                return str(site["id"])

        return None

    def create_tag(
        self, name: str, type: str, color: str, filters: list[dict] | None = None, match: str | None = MATCH_DEFAULT_VALUE
    ) -> dict:
        """
        | Create a new tag.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createTag

        Args:
            name (str): Name of the tag.
            type (str): Type of the tag.
            color (str): Color of the tag.
            filters (list[dict], optional): Filters to apply to the tag.
            match (str, optional): Match criteria for the filters. Default is MATCH_DEFAULT_VALUE.

        Returns:
            dict: API response.
        """
        json_data: dict[str, str | dict] = {"type": type, "name": name, "color": color}
        if filters:
            json_data["searchCriteria"] = find_valid_params(filters=filters, match=match)

        return self._http_request(
            method="POST",
            url_suffix="/tags",
            json_data=json_data,
            resp_type="json",
        )

    def delete_tag(self, id: int) -> dict:
        """
        | Delete a tag by ID.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteTag

        Args:
            id (int): ID of the tag to delete.

        Returns:
            dict: API response.
        """
        return self._http_request(method="DELETE", url_suffix=f"/tags/{id}", resp_type="json")

    def get_tag_by_id(self, id: int) -> dict:
        """
        | Get details of a tag by ID.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getTag

        Args:
            id (int): ID of the tag to retrieve.

        Returns:
            dict: API response.
        """
        return self._http_request(url_suffix=f"/tags/{id}", method="GET", resp_type="json")

    def get_tags_list(
        self,
        name: str | None = None,
        type: str | None = None,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        limit: int | None = None,
    ):
        """
        | Get a list of tags.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getTags

        Args:
            name (str, optional): Filter tags by name.
            type (str, optional): Filter tags by type.
            page_size (int, optional): Number of results per page. Default is DEFAULT_PAGE_SIZE.
            page (int, optional): Page number to retrieve.
            limit (int, optional): Maximum number of results to retrieve.

        Returns:
            dict: API response.
        """
        params = assign_params(name=name, type=type)

        return self._paged_http_request(
            url_suffix="/tags",
            method="GET",
            params=params,
            page_size=page_size,
            page=page,
            limit=limit,
            resp_type="json",
        )

    def update_tag_search_criteria(self, id: int, filters: list[dict] = [], match: str | None = MATCH_DEFAULT_VALUE) -> dict:
        """
        | Update search criteria for a tag by ID.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateTagSearchCriteria

        Args:
            id (int): ID of the tag to update.
            filters (list[dict], optional): New filters to apply to the tag.
            match (str, optional): Match criteria for the filters. Default is MATCH_DEFAULT_VALUE.

        Returns:
            dict: API response.
        """
        return self._http_request(
            method="PUT",
            url_suffix=f"/tags/{id}/search_criteria",
            json_data=find_valid_params(filters=filters, match=match),
            resp_type="json",
        )

    def send_http_request(self, method: str, url: str, body: list[Any] | None = None) -> dict:
        """
        | Send a generic HTTP request.
        |
        | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html

        Args:
            method (str): HTTP method (e.g., 'GET', 'POST').
            url (str): URL suffix for the request.
            body (list[Any], optional): JSON data to send in the body of the request.

        Returns:
            dict: API response.
        """
        request_data: dict[str, Any] = {"method": method, "url_suffix": url, "resp_type": "json"}
        if body is not None:
            request_data["json_data"] = body

        return self._http_request(**request_data)

    def create_asset_group(
        self, name: str, type: str, description: str, filters: list[dict] | None = None, match: str | None = MATCH_DEFAULT_VALUE
    ) -> dict:
        json_data = assign_params(
            name=name, type=type, description=description, searchCriteria=find_valid_params(filters=filters, match=match)
        )

        return self._http_request(method="POST", url_suffix="/asset_groups", json_data=json_data, resp_type="json")

    def get_asset_groups(
        self,
        name: str | None = None,
        type: str | None = None,
        page_size: int | None = DEFAULT_PAGE_SIZE,
        page: int | None = None,
        sort: str | None = None,
        limit: int | None = None,
    ) -> list[dict]:
        params = assign_params(name=name, type=type)

        return self._paged_http_request(
            url_suffix="/asset_groups",
            method="GET",
            params=params,
            page_size=page_size,
            page=page,
            sort=sort,
            limit=limit,
            resp_type="json",
        )

    def get_asset_group_by_id(self, id: int) -> list[dict]:
        return self._http_request(
            url_suffix=f"/asset_groups/{id}",
            method="GET",
            resp_type="json",
        )


class InsightVMClient:
    """
    Asynchronous client for interacting with the Rapid7 InsightVM API.
    Handles session and authentication management.
    """

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
        token: str = "",
        verify: bool = True,
    ):
        self._base_url = base_url.rstrip("/")
        self._auth_username = username
        self._auth_password = password
        self._auth_token = token
        self._verify = verify
        self._headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
        }
        self.connection_error_retries = CONNECTION_ERRORS_RETRIES

        # Add 2FA token to headers if provided
        if token:
            self._headers.update({"Token": token})

        auth_string = base64.b64encode(f"{username}:{password}".encode()).decode("utf-8")
        self._headers.update({"Authorization": f"Basic {auth_string}"})

    async def __aenter__(self):
        """Asynchronous context manager entry: creates the aiohttp session."""
        # Create a single session that persists for the client's lifespan.
        # total=None removes aiohttp's default 300s overall deadline so large report downloads
        # can stream in a single fetch; sock_read still fails fast if the connection stalls.
        timeout = aiohttp.ClientTimeout(
            total=None,
            sock_read=RAPID7_STREAM_SOCK_READ_TIMEOUT_SECONDS,
            sock_connect=RAPID7_STREAM_SOCK_CONNECT_TIMEOUT_SECONDS,
        )
        self._session = aiohttp.ClientSession(timeout=timeout)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Asynchronous context manager exit: closes the aiohttp session."""
        await self._session.close()

    async def http_request(
        self,
        method: str,
        endpoint: str,
        payload: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> aiohttp.ClientResponse:
        """
        Executes an asynchronous HTTP request and returns the raw response object.
        Includes retry logic for server errors.

        Args:
            method (str): The HTTP method to use (e.g. "GET", "POST").
            endpoint (str): The API endpoint (appended to the base URL).
            payload (Optional[Dict[str, Any]]): Optional JSON body for the request.
            headers (Optional[Dict[str, str]]): Optional per-request header overrides
                merged on top of the client's default headers. Used, for example, to
                request "Accept: text/csv" when downloading a CSV report output, since
                the default "Accept: application/json" causes the report `/output`
                endpoint to respond with HTTP 406 (Not Acceptable).
        """
        url = self._base_url + endpoint

        # Merge any per-request header overrides on top of the client defaults.
        request_headers = self._headers if not headers else {**self._headers, **headers}

        # Select the method function (get, post, etc.)
        request_func = getattr(self._session, method.lower())

        MAX_RETRIES = 3
        # Retry on: 500/502/503/504 (Server Errors), 429 (Rate Limit)
        RETRYABLE_STATUSES = {500, 502, 503, 504, 429}

        for attempt in range(MAX_RETRIES + 1):
            if attempt > 0:
                delay = 5**attempt
                demisto.debug(f"Retrying {method} {endpoint}... Attempt {attempt}/{MAX_RETRIES}. Waiting {delay}s...")
                await asyncio.sleep(delay)

            try:
                response = await request_func(url, headers=request_headers, json=payload, ssl=False)

                # 1. Handle Retryable Errors
                if response.status in RETRYABLE_STATUSES:
                    try:
                        message = response.message
                    except AttributeError:
                        message = "No message from Rapid7"
                    demisto.debug(f"API returned retryable status {response.status}: {message}")
                    response.close()  # Close connection before retrying
                    continue

                # 2. Handle Fatal Client Errors (4xx)
                if 400 <= response.status < 500:
                    try:
                        error_body = await response.text()
                    except Exception:
                        error_body = "Could not read error body."
                    response.close()
                    raise DemistoException(f"Client API Error ({response.status}): {error_body}")

                # 3. Success (2xx)
                # Return the RAW response object so the caller can read headers/json as needed.
                return response

            except aiohttp.ClientConnectorError as e:
                demisto.debug(f"Connection error: {e}")
                if attempt == MAX_RETRIES:
                    raise

        raise DemistoException(f"API request failed after {MAX_RETRIES} attempts.")


class Site:
    """A class representing a site, which can be identified by ID or name."""

    def __init__(self, site_id: str | None = None, site_name: str | None = None, client: Client | None = None) -> None:
        """
        Create a new Site object.
        Required parameters are either `site_id`, or both `site_name` and `client`.

        Args:
            site_id (str | None, optional): ID of the site.
            site_name (str | None, optional): Name of the site to create an object for.
            client (Client | None): Client object to use for API requests.
                Required to fetch ID if only name is provided.

        Raises:
            ValueError: If neither of `site_id` and `site_name` was provided,
            or `site_name` was provided without a `site_id`, and `client` was not provided.
            InvalidSiteNameException: If no ID was provided and a site with a matching name could not be found.
        """
        self.id: str
        self.name: str | None = None

        if site_id:
            self.id = site_id

        elif site_name:
            if client:
                site_id = client.find_site_id(site_name)

                if not site_id:
                    raise DemistoException(f'No site with name "{site_name}" was found.')

                self.id = site_id

            else:
                raise ValueError("Can't fetch site ID as no Client was provided.")

        else:
            raise ValueError("Either a site ID or a site name must be passed as an argument.")

        self.name = site_name


def convert_asset_search_filters(search_filters: str | list[str]) -> list[dict]:
    """
    | Convert string-based asset search filters to dict-based asset search filters that can be used in Nexpose's API.
    |
    | Format specification can be found under "Search Criteria" on:
        https://help.rapid7.com/insightvm/en-us/api/index.html#section/Overview/Responses

    Args:
        search_filters (str | list[str]): List of string-based search filters.

    Returns:
        list(dict): List of the same search filters in a dict-based format.
    """
    range_operators = ["in-range", "is-between", "not-in-range"]
    numeric_operators = ["is-earlier-than", "is-greater-than"]
    numeric_operators.extend(range_operators)
    values_field = ["site-id"]

    if isinstance(search_filters, str):
        search_filters = [search_filters]

    normalized_filters = []

    for search_filter in search_filters:
        # Example: risk-score is-between 5,10
        #   _field = risk-score
        #   _operator = is-greater-than
        #   _value = 5,10
        _field, _operator, _value = search_filter.split(" ")
        values = argToList(_value)

        if _operator in numeric_operators:
            values = [float(value) for value in values]

        filter_dict = {
            "field": _field,
            "operator": _operator,
        }

        if len(values) > 1:
            if _operator in range_operators:
                filter_dict["lower"] = values[0]
                filter_dict["upper"] = values[1]
            else:
                filter_dict["values"] = values
        elif _field in values_field:
            filter_dict["values"] = values
        else:
            filter_dict["value"] = values[0]

        normalized_filters.append(filter_dict)

    return normalized_filters


def convert_datetime_str(time_str: str) -> struct_time:
    """
    Convert an ISO 8601 datetime string to a `struct_time` object.

    Args:
        time_str (str): A string representing an ISO 8601 datetime.

    Returns:
        struct_time: The datetime represented in a `struct_time` object.
    """
    try:
        return strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ")

    except ValueError:
        return strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")


def create_credential_creation_body(
    service: CredentialService,
    database_name: str | None = None,
    domain: str | None = None,
    http_realm: str | None = None,
    notes_id_password: str | None = None,
    ntlm_hash: str | None = None,
    oracle_enumerate_sids: bool | None = None,
    oracle_listener_password: str | None = None,
    oracle_sid: str | None = None,
    password: str | None = None,
    snmp_community_name: str | None = None,
    snmpv3_authentication_type: SNMPv3AuthenticationType | None = None,
    snmpv3_privacy_password: str | None = None,
    snmpv3_privacy_type: SNMPv3PrivacyType | None = None,
    ssh_key_pem: str | None = None,
    ssh_permission_elevation: SSHElevationType | None = None,
    ssh_permission_elevation_password: str | None = None,
    ssh_permission_elevation_username: str | None = None,
    ssh_private_key_password: str | None = None,
    use_windows_authentication: bool | None = None,
    username: str | None = None,
) -> dict:
    """
    Create `account` body for credential-creation API requests.

    Args:
        service (CredentialService): Credential service type.
        database_name (str | None, optional): Database name.
        domain (str | None, optional): Domain address.
        http_realm (str | None, optional): HTTP realm.
        notes_id_password (str | None, optional):
            Password for the notes account that will be used for authenticating.
        ntlm_hash (str | None, optional): NTLM password hash.
        oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate
            SIDs from the environment.
        oracle_listener_password (str | None, optional): The Oracle Net Listener password.
            Used to enumerate SIDs from the environment.
        oracle_sid (str | None, optional): Oracle database name.
        password (str | None, optional): Password for the credential.
        snmp_community_name (str | None, optional): SNMP community for authentication.
        snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential.
        snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use.
        snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use.
        ssh_key_pem (str | None, optional): PEM formatted private key.
        ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans.
        ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
        ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
        ssh_private_key_password (str | None, optional): Password for the private key.
        use_windows_authentication (bool | None, optional): Whether to use Windows authentication.
        username (str | None, optional): Username for the credential.

    Returns:
        dict: `account` body to use for credential-creation API requests.
    """
    missing_params: list[str] = []
    special_validation_errors: list[str] = []

    account_data: dict = {"service": service.value}

    s = CredentialService  # Simplify object name for shorter lines

    # Services where "username" field is required
    if service in (
        s.AS400,
        s.CIFS,
        s.CIFSHASH,
        s.CVS,
        s.DB2,
        s.FTP,
        s.HTTP,
        s.MS_SQL,
        s.MYSQL,
        s.ORACLE,
        s.POP,
        s.POSTGRESQL,
        s.REMOTE_EXEC,
        s.SNMPV3,
        s.SSH,
        s.SSH_KEY,
        s.SYBASE,
        s.TELNET,
    ):
        if username is None:
            missing_params.append("Username")

        else:
            account_data["username"] = username

    # Services where "password" field is required
    if service in (
        s.AS400,
        s.CIFS,
        s.CIFSHASH,
        s.CVS,
        s.DB2,
        s.FTP,
        s.HTTP,
        s.MS_SQL,
        s.MYSQL,
        s.ORACLE,
        s.POP,
        s.POSTGRESQL,
        s.REMOTE_EXEC,
        s.SSH,
        s.SYBASE,
        s.TELNET,
    ):
        if password is None:
            missing_params.append("Password")

        else:
            account_data["password"] = password

    # Services with optional "useWindowsAuthentication" field.
    if service in (s.MS_SQL, s.SYBASE) and use_windows_authentication is not None:
        account_data["useWindowsAuthentication"] = use_windows_authentication

    # Services with optional "domain" field.
    if service in (s.AS400, s.CIFS, s.CIFSHASH, s.CVS, s.MS_SQL, s.SYBASE) and domain is not None:
        if service in (s.MS_SQL, s.SYBASE):
            if use_windows_authentication:
                account_data["domain"] = domain

        else:
            account_data["domain"] = domain

    # Services with optional "database" field.
    if service in (s.DB2, s.MS_SQL, s.MYSQL, s.POSTGRESQL, s.SYBASE) and database_name is not None:
        account_data["database"] = database_name

    if service == s.CIFSHASH:
        if ntlm_hash is None:
            missing_params.append("NTLM hash")

        else:
            account_data["ntlmHash"] = ntlm_hash

    if service == s.HTTP and http_realm is not None:
        account_data["realm"] = http_realm

    if service == s.NOTES and notes_id_password is not None:
        account_data["notesIDPassword"] = notes_id_password

    if service == s.ORACLE:
        if oracle_sid is not None:
            account_data["sid"] = oracle_sid

        if oracle_enumerate_sids is not None:
            account_data["enumerateSids"] = oracle_enumerate_sids

            if oracle_enumerate_sids and oracle_listener_password is None:
                missing_params.append("Oracle Listener Password")

            else:
                account_data["oracleListenerPassword"] = oracle_listener_password

    if service == s.SNMP:
        if snmp_community_name is None:
            missing_params.append("Community Name")

        else:
            account_data["community"] = snmp_community_name

    if service == s.SNMPV3:
        if snmpv3_authentication_type is None:
            missing_params.append("Authentication Type")

        else:
            account_data["authenticationType"] = snmpv3_authentication_type.value

        if snmpv3_authentication_type != SNMPv3AuthenticationType.NO_AUTHENTICATION:
            if password is None:
                special_validation_errors.append(
                    f"Password is required for {service.value} services "
                    "when authentication type is set to anything other "
                    'than "no-authentication".'
                )

            else:
                account_data["password"] = password

        if snmpv3_privacy_type is not None:
            account_data["privacyType"] = snmpv3_privacy_type.value

            if snmpv3_privacy_type != SNMPv3PrivacyType.NO_PRIVACY and snmpv3_privacy_password is None:
                special_validation_errors.append(
                    f"Privacy password is required for {service.value} services when the "
                    f"authentication type is set to any value other than "
                    f'"no-authentication", and privacy type is set to any value other '
                    f'than "no-privacy".'
                )

            else:
                account_data["privacyPassword"] = snmpv3_privacy_password

    if service in (s.SSH, s.SSH_KEY):
        if ssh_permission_elevation:
            account_data["permissionElevation"] = ssh_permission_elevation.value

        if ssh_permission_elevation not in (SSHElevationType.NONE, SSHElevationType.PBRUN):
            missing_elevation_params: list[str] = []

            if ssh_permission_elevation_username is None:
                missing_elevation_params.append("Elevation Username")

            else:
                account_data["permissionElevationUsername"] = ssh_permission_elevation_username

            if ssh_permission_elevation_password is None:
                missing_elevation_params.append("Elevation Password")

            else:
                account_data["permissionElevationPassword"] = ssh_permission_elevation_password

            if len(missing_elevation_params) > 0:
                special_validation_errors.append(
                    f"{', '.join(missing_elevation_params)} are required for "
                    f"\"{service.value}\" services when \"ssh_permission_elevation\" "
                    f"is not set to \"none\" or \"pbrun\"."
                )

    if service == s.SSH_KEY:
        if ssh_key_pem is None:
            missing_params.append("SSH Key PEM")

        else:
            account_data["pemKey"] = ssh_key_pem

        if ssh_private_key_password is not None:
            account_data["privateKeyPassword"] = ssh_private_key_password

    error_message: str = ""
    if len(missing_params) > 0:
        error_message += f"Missing required parameters for \"{service.value}\": {', '.join(missing_params)}.\n"

    if len(special_validation_errors) > 0:
        for special_validation_error in special_validation_errors:
            error_message += f"{special_validation_error}\n"

    if error_message:
        raise ValueError(error_message.rstrip("\n"))

    return account_data


def create_report(
    client: Client,
    scope: dict[str, Any],
    template_id: str | None = None,
    report_name: str | None = None,
    report_format: str | None = None,
    download_immediately: bool | None = None,
) -> dict | CommandResults:
    """
    Create a report and optionally download it.

    Args:
        client (Client): Client to use for API requests.
        scope (dict[str, Any]): Scope of the report, see Nexpose's documentation for more details.
        template_id (str | None, optional): ID of report template to use.
            Defaults to None (will result in using the first available template)
        report_name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default.
        report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF.
        download_immediately: (bool | None, optional) = Whether to download the report automatically after creation.
            Defaults to True.
    """
    if template_id is None:
        templates_data = client.get_report_templates()

        if not templates_data.get("resources"):
            return CommandResults(
                readable_output="No available templates were found.",
                raw_response=templates_data,
            )

        template_id = templates_data["resources"][0]["id"]

    if report_name is None:
        report_name = "report " + str(datetime.now())

    if not report_format:
        report_format = "pdf"

    if download_immediately is None:
        download_immediately = True

    report_data = client.create_report_config(
        scope=scope,
        template_id=template_id,
        report_name=report_name,
        report_format=report_format.lower(),
    )

    instance_data = client.create_report(report_data["id"])

    context = {
        "Name": report_name,
        "ID": report_data["id"],
        "InstanceID": instance_data["id"],
        "Format": report_format.lower(),
    }
    hr = tableToMarkdown("Report Information", context)

    if download_immediately:
        try:
            # Wait for the report to be completed
            time.sleep(REPORT_DOWNLOAD_WAIT_TIME)  # pylint: disable=E9003

            return download_report_command(
                client=client,
                report_id=report_data["id"],
                instance_id=instance_data["id"],
                name=report_name,
                report_format=report_format,
            )

        except Exception as e:
            # A 404 response could mean that the report generation process has not finished yet.
            # In that case report's information will be returned to the user for them to download it manually.
            if "404" not in str(e):
                raise

    return CommandResults(
        readable_output=hr,
        outputs_prefix="Nexpose.Report",
        outputs=context,
        outputs_key_field=["ID", "InstanceID"],
        raw_response=instance_data,
    )


def find_asset_last_scan_data(asset_data: dict) -> tuple[str, str]:
    """
    Find the date and ID for the last scan of an asset.

    Note:
        `-` is used as a placeholder for missing values instead of `None` because of backwards compatibility.

    Args:
        asset_data (dict): A dictionary representing an asset as received from the API.

    Returns:
        tuple[str, str]: A tuple containing the date (first value) and ID (seconds value) of the last scan of the asset.
    """
    scan_date = "-"
    scan_id = "-"

    if asset_data.get("history"):
        sorted_scans = sorted(asset_data["history"], key=lambda x: convert_datetime_str(x.get("date")), reverse=True)

        if "date" in sorted_scans[0]:
            scan_date = sorted_scans[0]["date"]

        if "scanId" in sorted_scans[0]:
            scan_id = sorted_scans[0]["scanId"]

    return scan_date, scan_id


def find_valid_params(strict_mode: bool = False, **kwargs) -> dict:
    """
    A function for filtering dictionaries (passed as kwargs) to remove keys that have a None value.

    Args:
        strict_mode (bool, optional): If set to true, keys with a False value (e.g. [], {}, '', False)
        will be removed as well.
        kwargs: A collection of keyword args to filter.

    Returns:
        dict: A dictionary containing only keywords with a value that isn't None.
    """
    new_kwargs = {}

    for key, value in kwargs.items():
        if (strict_mode and value) or (not strict_mode and value is not None):
            new_kwargs[key] = value

    return new_kwargs


def get_scan_entry(scan: dict) -> CommandResults:
    """
    Generate entry data from scan data (as received from the API).

    Args:
        scan (dict): Scan data as it was received from the API.

    Returns:
        CommandResults: Scan data in a normalized format that will be displayed in the UI.
    """
    scan_output = normalize_scan_data(scan)

    vulnerability_headers = [
        "Critical",
        "Severe",
        "Moderate",
        "Total",
    ]

    vulnerability_output = generate_new_dict(
        data=scan["vulnerabilities"],
        name_mapping={
            "critical": "Critical",
            "severe": "Severe",
            "moderate": "Moderate",
            "total": "Total",
        },
        include_none=True,
    )

    scan_hr = tableToMarkdown(
        name=f"Nexpose Scan ID {scan['id']!s}",
        t=scan_output,
        headers=[
            "Id",
            "ScanType",
            "ScanName",
            "StartedBy",
            "Assets",
            "TotalTime",
            "Completed",
            "Status",
            "Message",
        ],
        removeNull=True,
    )

    scan_hr += tableToMarkdown("Vulnerabilities", vulnerability_output, vulnerability_headers, removeNull=True)
    scan_output["Vulnerabilities"] = vulnerability_output

    return CommandResults(
        outputs_prefix="Nexpose.Scan",
        outputs_key_field="Id",
        outputs=scan_output,
        readable_output=scan_hr,
        raw_response=scan,
    )


def generate_duration_time(
    years: int | None = None,
    months: int | None = None,
    weeks: int | None = None,
    days: int | None = None,
    hours: int | None = None,
    minutes: int | None = None,
    seconds: float | None = None,
) -> str | None:
    """
    | Generate an ISO 8601 duration string.
    | More info about format's specification can be found on:
        https://en.wikipedia.org/wiki/ISO_8601#Durations
    |
    | If an overflow of a time unit occurs, the next unit will be incremented.
    | For months, 4 weeks are will be added to a month, even though months have variable length.

    Args:
        years (int | None, optional): Duration years.
        months (int | None, optional): Duration months.
        weeks (int | None, optional): Duration weeks.
        days (int | None, optional): Duration days.
        hours (int | None, optional): Duration hours.
        minutes (int | None, optional): Duration minutes.
        seconds (float | None, optional): Duration seconds.

    Returns:
        str: The duration represented in an ISO 8601 duration string.
    """
    if not any((years, months, weeks, days, hours, minutes, seconds)):
        return None

    duration_str = "P"

    if years:
        duration_str += f"{years}Y"

    if months:
        duration_str += f"{months}M"

    if weeks:
        duration_str += f"{weeks}W"

    if days:
        duration_str += f"{days}D"

    if hours or minutes or seconds:
        duration_str += "T"

        if hours:
            duration_str += f"{hours}H"

        if minutes:
            duration_str += f"{minutes}M"

        if seconds:
            duration_str += f"{seconds}S"

    return duration_str


def normalize_scan_data(scan_data: dict) -> dict:
    """
    Normalizes scan data received from the API to a HumanReadable format that will be displayed in the UI.

    Args:
        scan_data (dict): Scan data as it was received from the API.

    Returns:
        dict: Scan data in a normalized format that will be displayed in the UI.
    """
    result = generate_new_dict(
        data=scan_data,
        name_mapping={
            "id": "Id",
            "scanType": "ScanType",
            "scanName": "ScanName",
            "startedBy": "StartedBy",
            "assets": "Assets",
            "endTime": "Completed",
            "status": "Status",
            "message": "Message",
        },
        include_none=True,
    )

    if scan_data.get("duration"):
        result["TotalTime"] = readable_duration_time(scan_data["duration"])

    else:
        result["TotalTime"] = "No duration data was found."

    return result


def readable_duration_time(duration: str) -> str:
    """
    | Convert an ISO 8601 duration string to a human-readable string format.
    | More info about format's specification can be found on:
        https://en.wikipedia.org/wiki/ISO_8601#Durations

    Args:
        duration (str): An ISO 8601 duration string.

    Returns:
        str: The duration represented in a human-readable string format.
    """
    # Assure duration is in a valid format
    if not re.fullmatch(r"P(?:[\d.]+[YMWD]){0,4}T(?:[\d.]+[HMS]){0,3}", duration):
        raise ValueError(f'"{duration}" is not a valid ISO 8601 duration string.')

    p_duration, t_duration = duration.replace("T", ",T").split(",")
    p_duration = re.findall(r"([\d.]+[A-Z])", p_duration)
    t_duration = re.findall(r"([\d.]+[A-Z])", t_duration)
    duration_mapping_p = {
        "Y": "years",
        "M": "months",
        "W": "weeks",
        "D": "days",
    }
    duration_mapping_t = {
        "H": "hours",
        "M": "minutes",
        "S": "seconds",
    }
    duration_values: dict = {
        "years": 0,
        "months": 0,
        "weeks": 0,
        "days": 0,
        "hours": 0,
        "minutes": 0,
        "seconds": 0,
    }

    for item in p_duration:
        designator = item[-1]
        number_float = float(item[:-1])

        if number_float.is_integer():
            number_int = int(number_float)

        else:
            number_int = round(number_float)

        duration_values[duration_mapping_p[designator]] = number_int

    for item in t_duration:
        designator = item[-1]
        number_float = float(item[:-1])

        if number_float.is_integer():
            duration_values[duration_mapping_t[designator]] = int(number_float)

        else:
            duration_values[duration_mapping_t[designator]] = number_float

    result = []
    for item in duration_values:
        zero_up_to_now = True

        if duration_values[item] > 0:
            zero_up_to_now = False

        if not zero_up_to_now:
            result += [f"{duration_values[item]} {item}"]

    return ", ".join(result)


@overload
def remove_dict_key(data: dict, key: Any) -> dict:  # pragma: no cover
    pass


@overload
def remove_dict_key(data: list, key: Any) -> list:  # pragma: no cover
    pass


@overload
def remove_dict_key(data: tuple, key: Any) -> tuple:  # pragma: no cover
    pass


def remove_dict_key(data: dict | list | tuple, key: Any) -> dict | list | tuple:
    """
    Recursively remove a dictionary key from an object

    Args:
        data (dict | list | tuple): A dictionary or an iterable to remove keys for dictionaries within it.
        key (Any): Key to remove from dictionaries.

    Returns:
        dict | list | tuple: The data-structure (original or copy) with the specified key name removed
        from all dictionaries within.
    """
    if isinstance(data, dict):
        if key in data:
            del data[key]

        for k in data:
            remove_dict_key(data[k], key)

    if isinstance(data, list | tuple):
        for item in data:
            remove_dict_key(item, key)

    return data


@overload
def generate_new_dict(data: dict, name_mapping: dict[str, str], include_none: bool = False) -> dict:  # pragma: no cover
    pass


@overload
def generate_new_dict(data: list, name_mapping: dict[str, str], include_none: bool = False) -> list:  # pragma: no cover
    pass


def generate_new_dict(data: dict | list, name_mapping: dict[str, str], include_none: bool = False) -> dict | list | tuple:
    """
    Generate a new dictionary from an existing dictionary, with the keys renamed according to `name_mapping`.

    Args:
        data (dict | list): The dictionary to generate a new dictionary from.
            If a list is passed, the function will run recursively on each item in the list.
        name_mapping (dict[str, str]): A mapping between old key names to the new key names
            in a `key-path: new-key` format.
        include_none (bool, optional): Whether to include keys with `None` values in the new dictionary.
    """
    if isinstance(data, dict):
        new_dict = {}

        for key_path, new_key in name_mapping.items():
            value = find_dict_item(data, key_path)

            if include_none or value is not None:
                new_dict[new_key] = value

        return new_dict

    elif isinstance(data, list):
        return [generate_new_dict(item, name_mapping, include_none) for item in data]

    elif isinstance(data, tuple):
        return tuple(generate_new_dict(item, name_mapping, include_none) for item in data)

    else:
        return data


def find_dict_item(data: dict | list | tuple, key_path: str) -> Any:
    """
    Find a dictionary item by its key path.

    Note:
        This code snippet assumes that `data` does not contain None values.
        If it does, None values will be returned both if the value is None, or if the key couldn't be found.

    Args:
        data (dict | list | tuple): A dictionary, a list, or a tuple to search for the key path in.
        key_path (str): The key path to search for. Keys are separated by a dot (.) character.

    Returns:
        Any: The value of the key path if found, None otherwise.
    """
    if isinstance(data, dict):
        key_path_list = key_path.split(".")

        if key_path_list[0] in data:
            if len(key_path_list) == 1:
                return data[key_path_list[0]]

            else:
                return find_dict_item(
                    data=data[key_path_list[0]],
                    key_path=".".join(key_path_list[1:]),
                )

        else:
            return None

    elif isinstance(data, list | tuple):
        result = [
            find_dict_item(
                data=item,
                key_path=key_path,
            )
            for item in data
        ]

        return [item for item in result if item is not None]

    return None


def parse_asset_filters(client, **kwargs):
    """
    Parse and generate a list of asset filters based on provided keyword arguments.

    Args:
        client (Client): Client to use for API requests.
        **kwargs: Arbitrary keyword arguments representing filter criteria.
            - ip_address_is (str): A specific IP address to filter assets by.
            - host_name_is (str): A specific host name to filter assets by.
            - risk_score_higher_than (str): A minimum risk score to filter assets by.
            - vulnerability_title_contains (str): A keyword to filter assets by vulnerability title.
            - query (str): A semicolon-separated list of custom query strings.
            - site_id_in (str): A comma-separated list of site IDs to filter assets by.
            - site_name_in (str): A comma-separated list of site names to filter assets by.


    Returns:
        list[str]: A list of asset filter strings.
    """
    filters_data: list[str] = []

    if kwargs.get("ip_address_is"):
        filters_data.append("ip-address is " + kwargs["ip_address_is"])

    if kwargs.get("host_name_is"):
        filters_data.append("host-name is " + kwargs["host_name_is"])

    if kwargs.get("risk_score_higher_than"):
        filters_data.append("risk-score is-greater-than " + kwargs["risk_score_higher_than"])

    if kwargs.get("vulnerability_title_contains"):
        filters_data.append("vulnerability-title contains " + kwargs["vulnerability_title_contains"])

    if kwargs.get("query"):
        filters_data.extend(kwargs["query"].split(";"))

    sites: list[Site] = []

    for site_id in argToList(kwargs.get("site_id_in")):
        sites.append(Site(site_id=site_id, client=client))

    for site_name in argToList(kwargs.get("site_name_in")):
        sites.append(Site(site_name=site_name, client=client))

    if sites:
        str_site_ids: str = ""

        if isinstance(sites, list):
            str_site_ids = ",".join([site.id for site in sites])

        elif isinstance(sites, Site):
            str_site_ids = sites.id

        filters_data.append("site-id in " + str_site_ids)

    return filters_data


def validate_input(input_value: str | None, valid_options: list[str], arg_name: str, is_required: bool = True):
    """
    Validates the input value against a list of valid options.

    Args:
        input_value (str | None): The input value to validate.
        valid_options (list[str]): The list of valid options.
        parameter_name (str): The name of the parameter being validated (used in the error message).
        is_required (bool): Whether the input value is required. Defaults to True.

    Raises:
        DemistoException: If the input value is invalid or not in the list of valid options.

    Returns:
        bool: True if the input value is valid or not required and None is provided.
    """
    if input_value is None and not is_required:
        return True
    elif not input_value or input_value.lower() not in valid_options:
        raise DemistoException(f"{input_value} is an invalid {arg_name} the only options are: {', '.join(valid_options)}")
    return True


# --- Command Functions --- #
def create_asset_command(
    client: Client,
    date: str,
    site_id: str | None = None,
    site_name: str | None = None,
    ip: str | None = None,
    host_name: str | None = None,
    host_name_source: str | None = None,
) -> CommandResults:
    """
    Create a new asset.

    Args:
        client (Client): The client to use.
        date (str): The date the data was collected on the asset.
        site_id (str | None, optional): Name of the site to create the asset in.
        site_name (str | None, optional): Name of the site to create the asset in. Can be used instead of "site_id".
        ip (str | None, optional): The IP address of the asset.
        host_name (str | None, optional): The hostname of the asset.
        host_name_source (str | None, optional): The source of the hostname.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    response_data = client.create_asset(
        site_id=site.id,
        date=date,
        ip_address=ip,
        hostname=host_name,
        hostname_source=host_name_source,
    )

    return CommandResults(
        readable_output=f"New asset has been created with ID {response_data['id']}.",
        outputs_prefix="Nexpose.Asset",
        outputs_key_field="id",
        outputs={"id": response_data["id"]},
        raw_response=response_data,
    )


def create_assets_report_command(
    client: Client,
    assets: str,
    template: str | None = None,
    name: str | None = None,
    report_format: str | None = None,
    download_immediately: str | None = None,
) -> dict | CommandResults:
    """
    Create a report about specific assets.

    Args:
        client (Client): Client to use for API requests.
        assets (str): List of assets to include in the report.
        template (str | None, optional): ID of report template to use.
            Defaults to None (will result in using the first available template)
        name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default.
        report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF.
        download_immediately: (str | None, optional) = Whether to download the report automatically after creation.
            Defaults to True.
    """
    download_immediately_bool = None

    asset_ids_list: list[str] = argToList(assets)

    if download_immediately is not None:
        download_immediately_bool = argToBoolean(download_immediately)

    scope = {"assets": [int(asset_id) for asset_id in asset_ids_list]}

    return create_report(
        client=client,
        scope=scope,
        template_id=template,
        report_name=name,
        report_format=report_format,
        download_immediately=download_immediately_bool,
    )


def create_scan_report_command(
    client: Client,
    scan: str,
    template: str | None = None,
    name: str | None = None,
    report_format: str | None = None,
    download_immediately: str | None = None,
) -> dict | CommandResults:
    """
    Create a report about specific sites.

    Args:
        client (Client): Client to use for API requests.
        scan (str): ID of the scan to create a report on.
        template (str | None, optional): ID of report template to use.
            Defaults to None (will result in using the first available template)
        name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default.
        report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF.
        download_immediately: (str | None, optional) = Whether to download the report automatically after creation.
            Defaults to True.
    """
    download_immediately_bool = None

    if download_immediately is not None:
        download_immediately_bool = argToBoolean(download_immediately)

    scope = {"scan": arg_to_number(scan, required=True)}

    return create_report(
        client=client,
        scope=scope,
        template_id=template,
        report_name=name,
        report_format=report_format,
        download_immediately=download_immediately_bool,
    )


def create_scan_schedule_command(
    client: Client,
    on_scan_repeat: str,
    start: str,
    site_id: str | None = None,
    site_name: str | None = None,
    excluded_asset_groups: str | None = None,
    excluded_targets: str | None = None,
    included_asset_groups: str | None = None,
    included_targets: str | None = None,
    duration_days: str | None = None,
    duration_hours: str | None = None,
    duration_minutes: str | None = None,
    enabled: str | None = None,
    frequency: str | None = None,
    interval_time: str | None = None,
    scan_name: str | None = None,
    date_of_month: int | None = None,
    scan_template_id: str | None = None,
) -> CommandResults:
    """
    Create a new site scan schedule.

    Args:
        client (Client): Client to use for API requests.
        on_scan_repeat (str): The desired behavior of a repeating scheduled scan
            when the previous scan was paused due to reaching its maximum duration.
        start (str): The scheduled start date and time formatted in ISO 8601 format.
        site_id (str | None, optional): ID of the site to create a scheduled scan for.
        site_name (str | None, optional): Name of the site to create a scheduled scan for.
            Can be used instead of "site_id".
        excluded_asset_groups (str | None, optional): Asset groups to exclude from the scan.
        excluded_targets (str | None, optional): Addresses to exclude from the scan. Each address is a string that
            can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation.
        included_asset_groups (str | None, optional): Asset groups to include in the scan.
        included_targets (str | None, optional): Addresses to include in the scan.  Each address is a string that
            can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation.
        duration_days (str | None, optional): Maximum duration of the scan in days.
            Can be used along with `duration_hours` and `duration_minutes`.
        duration_hours (str | None, optional): Maximum duration of the scan in hours.
            Can be used along with `duration_days` and `duration_minutes`.
        duration_minutes (str | None, optional): Maximum duration of the scan in minutes.
            Can be used along with `duration_days` and `duration_hours`.
        enabled (str | None, optional): A flag indicating whether the scan schedule is enabled.
           Defaults to None, which results in using True.
        frequency (str | None, optional): Frequency for the schedule to repeat.
            Required if using other repeat settings.
        interval_time (str | None, optional): The interval time the schedule should repeat.
            Required if using other repeat settings.
        date_of_month(str | None, optional): Specifies the schedule repeat day of the interval month.
            Required and used only if frequency is set to `DATE_OF_MONTH`.
        scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule.
            If not explicitly set in the schedule, the scan name will be generated prior to the scan launching.
        scan_template_id (str | None, optional): ID of the scan template to use.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    excluded_asset_groups_list = None
    excluded_targets_list = None
    frequency_enum = None
    included_asset_groups_list = None
    included_targets_list = None

    if excluded_asset_groups is not None:
        excluded_asset_groups_list = [int(asset_id) for asset_id in argToList(excluded_asset_groups)]

    if excluded_targets is not None:
        excluded_targets_list = argToList(excluded_targets)

    if frequency is not None:
        frequency_enum = RepeatFrequencyType[frequency]

    if included_asset_groups is not None:
        included_asset_groups_list = [int(asset_id) for asset_id in argToList(included_asset_groups)]

    if included_targets is not None:
        included_targets_list = argToList(included_targets)

    duration_days_int = arg_to_number(duration_days, required=False)
    duration_hours_int = arg_to_number(duration_hours, required=False)
    duration_minutes_int = arg_to_number(duration_minutes, required=False)
    interval_int = arg_to_number(interval_time, required=False)
    date_of_month_int = arg_to_number(date_of_month, required=False)

    if enabled is not None:
        enabled_bool = argToBoolean(enabled)

    else:
        enabled_bool = True

    duration = generate_duration_time(
        days=duration_days_int,
        hours=duration_hours_int,
        minutes=duration_minutes_int,
    )

    response_data = client.create_site_scan_schedule(
        site_id=site.id,
        enabled=enabled_bool,
        repeat_behaviour=on_scan_repeat,
        start_date=start,
        excluded_asset_groups=excluded_asset_groups_list,
        excluded_targets=excluded_targets_list,
        included_asset_groups=included_asset_groups_list,
        included_targets=included_targets_list,
        duration=duration,
        frequency=frequency_enum,
        interval=interval_int,
        date_of_month=date_of_month_int,
        scan_name=scan_name,
        scan_template_id=scan_template_id,
    )

    return CommandResults(
        readable_output=f"New scheduled scan has been created with ID {response_data['id']}.",
        outputs_prefix="Nexpose.ScanSchedule",
        outputs_key_field="id",
        outputs={"id": response_data["id"]},
        raw_response=response_data,
    )


def create_shared_credential_command(
    client: Client,
    name: str,
    site_assignment: str,
    service: str,
    database: str | None = None,
    description: str | None = None,
    domain: str | None = None,
    host_restriction: str | None = None,
    http_realm: str | None = None,
    notes_id_password: str | None = None,
    ntlm_hash: str | None = None,
    oracle_enumerate_sids: str | None = None,
    oracle_listener_password: str | None = None,
    oracle_sid: str | None = None,
    password: str | None = None,
    port_restriction: str | None = None,
    sites: str | None = None,
    authentication_type: str | None = None,
    community_name: str | None = None,
    privacy_password: str | None = None,
    privacy_type: str | None = None,
    ssh_key_pem: str | None = None,
    ssh_permission_elevation: str | None = None,
    ssh_permission_elevation_password: str | None = None,
    ssh_permission_elevation_username: str | None = None,
    ssh_private_key_password: str | None = None,
    use_windows_authentication: str | None = None,
    username: str | None = None,
) -> CommandResults:
    """
    Create a new shared credential.

    Args:
        client (Client): Client to use for API requests.
        name (str): Name of the credential.
        site_assignment (str): Site assignment configuration for the credential.
        service (str): Credential service type.
        database (str | None, optional): Database name.
        description (str | None, optional): Description for the credential.
        domain (str | None, optional): Domain address.
        host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
        http_realm (str | None, optional): HTTP realm.
        notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating.
        ntlm_hash (str | None, optional): NTLM password hash.
        oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate
            SIDs from the environment.
        oracle_listener_password (str | None, optional): The Oracle Net Listener password.
            Used to enumerate SIDs from the environment.
        oracle_sid (str | None, optional): Oracle database name.
        password (str | None, optional): Password for the credential.
        port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
            on a specific port. Can be used only if `host_restriction` is used.
        sites (str | None, optional): List of site IDs for the shared credential that are explicitly assigned
            access to the shared scan credential, allowing it to use the credential during a scan.
        authentication_type (str | None, optional): SNMPv3 authentication type for the credential.
        community_name (str | None, optional): SNMP community for authentication.
        privacy_password (str | None, optional): SNMPv3 privacy password to use.
        privacy_type (str | None, optional): SNMPv3 Privacy protocol to use.
        ssh_key_pem (str | None, optional): PEM formatted private key.
        ssh_permission_elevation (str | None, optional): Elevation type to use for scans.
        ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
        ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
        ssh_private_key_password (str | None, optional): Password for the private key.
        use_windows_authentication (str | None, optional): Whether to use Windows authentication.
        username (str | None, optional): Username for the credential.
    """
    oracle_enumerate_sids_bool = None
    sites_list = None
    snmpv3_authentication_type_enum = None
    snmpv3_privacy_type_enum = None
    ssh_permission_elevation_enum = None
    use_windows_authentication_bool = None

    if oracle_enumerate_sids is not None:
        oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids)

    if sites is not None:
        sites_list = [int(item) for item in argToList(sites)]

    if authentication_type is not None:
        snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type]

    if privacy_type is not None:
        snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type]

    if ssh_permission_elevation is not None:
        ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation]

    if use_windows_authentication is not None:
        use_windows_authentication_bool = argToBoolean(use_windows_authentication)

    response_data = client.create_shared_credential(
        name=name,
        site_assignment=SharedCredentialSiteAssignment[site_assignment],
        service=CredentialService[service],
        database_name=database,
        description=description,
        domain=domain,
        host_restriction=host_restriction,
        http_realm=http_realm,
        notes_id_password=notes_id_password,
        ntlm_hash=ntlm_hash,
        oracle_enumerate_sids=oracle_enumerate_sids_bool,
        oracle_listener_password=oracle_listener_password,
        oracle_sid=oracle_sid,
        password=password,
        port_restriction=port_restriction,
        sites=sites_list,
        snmp_community_name=community_name,
        snmpv3_authentication_type=snmpv3_authentication_type_enum,
        snmpv3_privacy_password=privacy_password,
        snmpv3_privacy_type=snmpv3_privacy_type_enum,
        ssh_key_pem=ssh_key_pem,
        ssh_permission_elevation=ssh_permission_elevation_enum,
        ssh_permission_elevation_password=ssh_permission_elevation_password,
        ssh_permission_elevation_username=ssh_permission_elevation_username,
        ssh_private_key_password=ssh_private_key_password,
        use_windows_authentication=use_windows_authentication_bool,
        username=username,
    )

    return CommandResults(
        readable_output=f"New shared credential has been created with ID {response_data['id']}.",
        outputs_prefix="Nexpose.SharedCredential",
        outputs_key_field="id",
        outputs={"id": response_data["id"]},
        raw_response=response_data,
    )


def create_site_command(
    client: Client,
    name: str,
    description: str | None = None,
    assets: str | None = None,
    importance: str | None = None,
    template_id: str | None = None,
) -> CommandResults:
    """
    Create a new site.

    Args:
        client (Client): Client to use for API requests.
        name (str): Name of the site. Must be unique.
        description (str | None, optional): Description of the site. Defaults to None.
        assets (str | None, optional): List of asset IDs to be included in site scans. Defaults to None.
        importance (str | None, optional): Importance of the site.
            Defaults to None (results in using API's default - "normal").
        template_id (str | None, optional): The identifier of a scan template.
            Defaults to None (results in using default scan template).
    """
    assets_list = None

    if assets is not None:
        assets_list = argToList(assets)

    response_data = client.create_site(
        name=name, description=description, assets=assets_list, site_importance=importance, template_id=template_id
    )

    return CommandResults(
        readable_output=f"New site has been created with ID {response_data['id']}.",
        outputs_prefix="Nexpose.Site",
        outputs_key_field="Id",
        outputs={"Id": response_data["id"]},
        raw_response=response_data,
    )


def create_sites_report_command(
    client: Client,
    sites: str | None = None,
    site_names: str | None = None,
    template: str | None = None,
    name: str | None = None,
    report_format: str | None = None,
    download_immediately: str | None = None,
) -> dict | CommandResults:
    """
    Create a report about specific sites.

    Args:
        client (Client): Client to use for API requests.
        sites (str | None, optional): List of site IDs to create the report about.
        site_names (str | None, optional): List of site names to create the report about.
        template (str | None, optional): ID of report template to use.
            Defaults to None (will result in using the first available template)
        name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default.
        report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF.
        download_immediately: (str | None, optional) = Whether to download the report automatically after creation.
            Defaults to True.
    """
    sites_list = [Site(site_id=site_id, client=client) for site_id in argToList(sites)]
    sites_list.extend([Site(site_name=site_name, client=client) for site_name in argToList(site_names)])

    if len(sites_list) == 0:
        raise Exception("At least one site ID or site name must be provided.")

    download_immediately_bool = None

    if download_immediately is not None:
        download_immediately_bool = argToBoolean(download_immediately)

    scope = {"sites": [int(site.id) for site in sites_list]}

    return create_report(
        client=client,
        scope=scope,
        template_id=template,
        report_name=name,
        report_format=report_format,
        download_immediately=download_immediately_bool,
    )


def create_site_scan_credential_command(
    client: Client,
    name: str,
    service: str,
    site_id: str | None = None,
    site_name: str | None = None,
    authentication_type: str | None = None,
    community_name: str | None = None,
    database: str | None = None,
    description: str | None = None,
    domain: str | None = None,
    host_restriction: str | None = None,
    http_realm: str | None = None,
    notes_id_password: str | None = None,
    ntlm_hash: str | None = None,
    oracle_enumerate_sids: str | None = None,
    oracle_listener_password: str | None = None,
    oracle_sid: str | None = None,
    password: str | None = None,
    port_restriction: str | None = None,
    privacy_password: str | None = None,
    privacy_type: str | None = None,
    ssh_key_pem: str | None = None,
    ssh_permission_elevation: str | None = None,
    ssh_permission_elevation_password: str | None = None,
    ssh_permission_elevation_username: str | None = None,
    ssh_private_key_password: str | None = None,
    use_windows_authentication: str | None = None,
    username: str | None = None,
) -> CommandResults:
    """
    Create a new site scan credential.

    Args:
        client (Client): Client to use for API requests.
        name (str): Name of the credential.
        service (str): Credential service type.
        site_id (str | None, optional): ID of a site to create the credential for.
        site_name (str | None, optional): Name of a site to create the credential for. Can be used instead of "site_id".
        authentication_type (str): SNMPv3 authentication type for the credential.
        community_name (str | None, optional): SNMP community for authentication.
        database (str | None, optional): Database name.
        description (str | None, optional): Description for the credential.
        domain (str | None, optional): Domain address.
        host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
        http_realm (str | None, optional): HTTP realm.
        notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating.
        ntlm_hash (str | None, optional): NTLM password hash.
        oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate
            SIDs from the environment.
        oracle_listener_password (str | None, optional): The Oracle Net Listener password.
            Used to enumerate SIDs from the environment.
        oracle_sid (str | None, optional): Oracle database name.
        password (str | None, optional): Password for the credential.
        port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
            on a specific port. Can be used only if `host_restriction` is used.
        privacy_password (str | None, optional): SNMPv3 privacy password to use.
        privacy_type (str | None, optional): SNMPv3 Privacy protocol to use.
        ssh_key_pem (str | None, optional): PEM formatted private key.
        ssh_permission_elevation (str | None, optional): Elevation type to use for scans.
        ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
        ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
        ssh_private_key_password (str | None, optional): Password for the private key.
        use_windows_authentication (str | None, optional): Whether to use Windows authentication.
        username (str | None, optional): Username for the credential.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    oracle_enumerate_sids_bool = None
    snmpv3_authentication_type_enum = None
    snmpv3_privacy_type_enum = None
    ssh_permission_elevation_enum = None
    use_windows_authentication_bool = None

    if oracle_enumerate_sids is not None:
        oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids)

    if authentication_type is not None:
        snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type]

    if privacy_type is not None:
        snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type]

    if ssh_permission_elevation is not None:
        ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation]

    if use_windows_authentication is not None:
        use_windows_authentication_bool = argToBoolean(use_windows_authentication)

    response_data = client.create_site_scan_credential(
        site_id=site.id,
        name=name,
        service=CredentialService[service],
        database_name=database,
        description=description,
        domain=domain,
        host_restriction=host_restriction,
        http_realm=http_realm,
        notes_id_password=notes_id_password,
        ntlm_hash=ntlm_hash,
        oracle_enumerate_sids=oracle_enumerate_sids_bool,
        oracle_listener_password=oracle_listener_password,
        oracle_sid=oracle_sid,
        password=password,
        port_restriction=port_restriction,
        snmp_community_name=community_name,
        snmpv3_authentication_type=snmpv3_authentication_type_enum,
        snmpv3_privacy_password=privacy_password,
        snmpv3_privacy_type=snmpv3_privacy_type_enum,
        ssh_key_pem=ssh_key_pem,
        ssh_permission_elevation=ssh_permission_elevation_enum,
        ssh_permission_elevation_password=ssh_permission_elevation_password,
        ssh_permission_elevation_username=ssh_permission_elevation_username,
        ssh_private_key_password=ssh_private_key_password,
        use_windows_authentication=use_windows_authentication_bool,
        username=username,
    )

    return CommandResults(
        readable_output=f"New site scan credential has been created with ID {response_data['id']}.",
        outputs_prefix="Nexpose.SiteScanCredential",
        outputs_key_field="id",
        outputs={"id": response_data["id"]},
        raw_response=response_data,
    )


def create_vulnerability_exception_command(
    client: Client,
    vulnerability_id: str,
    scope_type: str,
    state: str,
    reason: str,
    scope_id: str | None = None,
    expires: str | None = None,
    comment: str | None = None,
) -> CommandResults:
    """
    Create a vulnerability exception.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_id (str): ID of the vulnerability to create the exception for.
        scope_type (str): The type of the exception scope.
        state (str): The state of the vulnerability exception.
        reason (str): The reason the vulnerability exception was submitted.
        scope_id (int): ID of the chosen `scope_type` (site ID, asset ID, etc.).
            Required if `scope_type` is anything other than `Global`
        expires (str | None, optional): The date and time the vulnerability exception is set to expire.
        comment (str | None, optional): A comment from the submitter as to why the exception was submitted.
    """
    scope_type_enum = VulnerabilityExceptionScopeType[scope_type]

    if scope_type_enum != VulnerabilityExceptionScopeType.GLOBAL and scope_id is None:
        raise ValueError(
            f'"scope_id" must be set when using scopes different than "{VulnerabilityExceptionScopeType.GLOBAL.value}".'
        )

    response_data = client.create_vulnerability_exception(
        vulnerability_id=vulnerability_id,
        scope_type=scope_type_enum,
        state=state,
        reason=reason,
        scope_id=int(scope_id) if scope_id is not None else None,
        expires=expires,
        comment=comment,
    )

    return CommandResults(
        readable_output=f"New vulnerability exception has been created with ID {response_data['id']!s}.",
        outputs_prefix="Nexpose.VulnerabilityException",
        outputs_key_field="id",
        outputs={"id": response_data["id"]},
        raw_response=response_data,
    )


def delete_asset_command(client: Client, asset_id: str) -> CommandResults:
    """
    Delete an asset.

    Args:
        client (Client): Client to use for API requests.
        asset_id (str): ID of the asset to delete.
    """
    response_data = client.delete_asset(asset_id=asset_id)

    return CommandResults(
        readable_output=f"Asset {asset_id} has been deleted.",
        raw_response=response_data,
    )


def delete_scan_schedule_command(
    client: Client, schedule_id: str, site_id: str | None = None, site_name: str | None = None
) -> CommandResults:
    """
    Delete a scheduled scan.

    Args:
        client (Client): Client to use for API requests.
        schedule_id (str): ID of the scheduled scan to delete.
        site_id (str | None, optional): ID of the site to delete the scheduled scan from.
        site_name (str | None, optional): Name of the site to delete the scheduled scan from.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    response_data = client.delete_scan_schedule(
        site_id=site.id,
        scheduled_scan_id=schedule_id,
    )

    return CommandResults(
        readable_output=f"Scheduled scan with ID {schedule_id} has been deleted.",
        raw_response=response_data,
    )


def delete_site_command(client: Client, site_id: str | None = None, site_name: str | None = None) -> CommandResults:
    """
    Delete a site.

    Args:
        client (Client): Client to use for API requests.
        site_id (str | None, optional): ID of a site to delete.
        site_name (str | None, optional): Name of a site to delete. Can be used instead of "site_id".
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    response_data = client.delete_site(site.id)

    return CommandResults(
        readable_output=f"Site ID {site.id} has been deleted.",
        outputs_prefix="Nexpose.Report",
        outputs_key_field=["ID", "InstanceID"],
        raw_response=response_data,
    )


def delete_shared_credential_command(client: Client, shared_credential_id: str) -> CommandResults:
    """
    Delete a shared credential.

    Args:
        client (Client): Client to use for API requests.
        shared_credential_id (str): ID of the shared credential to delete.
    """
    response_data = client.delete_shared_credential(shared_credential_id)

    return CommandResults(
        readable_output=f"Shared credential with ID {shared_credential_id} has been deleted.",
        raw_response=response_data,
    )


def delete_site_scan_credential_command(
    client: Client, credential_id: str, site_id: str | None = None, site_name: str | None = None
) -> CommandResults:
    """
    Delete a site scan credential.

    Args:
        client (Client): Client to use for API requests.
        credential_id (str): ID of the site scan credential to delete.
        site_id (str | None, optional): ID of the site to delete the site scan credential from.
        site_name (str | None, optional): Name of the site to delete the site scan credential from.
            Can be used instead of "site_id".
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    response_data = client.delete_site_scan_credential(
        site_id=site.id,
        site_credential_id=credential_id,
    )

    return CommandResults(
        readable_output=f"Site scan credential with ID {credential_id} has been deleted.",
        raw_response=response_data,
    )


def delete_vulnerability_exception_command(client: Client, vulnerability_exception_id: str) -> CommandResults:
    """
    Delete a vulnerability exception.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_exception_id (str): ID of the vulnerability exception to delete.
    """
    response_data = client.delete_vulnerability_exception(vulnerability_exception_id)

    return CommandResults(
        readable_output=f"Vulnerability exception with ID {vulnerability_exception_id} has been deleted.",
        raw_response=response_data,
    )


def download_report_command(
    client: Client, report_id: str, instance_id: str, name: str | None = None, report_format: str | None = None
) -> dict:
    """
    Download a report file.

    Note:
        Not sure why there's a report_format parameter, as the format is set when generating the report,
        and all the parameter seems to do here, is just change the file extension
        (which is obviously not how file conversion works).
        This function currently remains as it is, since removing it might break client's using it for some reason.

    Args:
        client (Client): Client to use for API requests.
        report_id (str): ID of the report to download.
        instance_id (str): ID of the report instance.
        name (str | None, optional): Name to give the generated report file.
            Defaults to None (results in using a "report <date>" format as a name).
        report_format (str | None, optional): File format to use for the generated report.
            Defaults to None (results in using PDF).

    Returns:
        dict: A dict generated by `CommonServerPython.fileResult` representing a War Room entry.
    """
    if name is None:
        name = f"report {datetime.now()!s}"

    if not report_format:
        report_format = "pdf"

    report_data = client.download_report(report_id=report_id, instance_id=instance_id)

    return fileResult(
        filename=f"{name}.{report_format.lower()}",
        data=report_data,
        file_type=entryTypes["entryInfoFile"],
    )


def get_asset_tags_command(client: Client, asset_id: str) -> CommandResults | list[CommandResults]:
    """
    Retrieve tags associated to an asset.

    Args:
        client (Client): Client to use for API requests.
        asset_id (str): ID of the asset to retrieve information about.
    """
    tags = []

    try:
        tag_raw_data = client.get_asset_tags(asset_id)

    except DemistoException as e:
        if e.res is not None and e.res.status_code is not None and e.res.status_code == 404:
            return CommandResults(readable_output="Asset not found.")
    for tag in tag_raw_data.get("resources", []):
        tag_output = generate_new_dict(
            data=tag,
            name_mapping={
                "type": "Type",
                "riskModifier": "RiskModifier",
                "name": "Name",
                "created": "CreatedTime",
            },
            include_none=True,
        )
        tags.append(tag_output)

    readable_output = tableToMarkdown(
        name=f"Nexpose Asset Tags for Asset {asset_id}",
        t=tags,
        headers=["Type", "Name", "Risk Modifier", "Created Time"],
    )

    result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Nexpose.AssetTag",
        outputs=tags,
        outputs_key_field="type",
        raw_response=tag_raw_data,
    )

    return result


def get_asset_command(client: Client, asset_id: str) -> CommandResults | list[CommandResults]:
    """
    Retrieve information about an asset.

    Args:
        client (Client): Client to use for API requests.
        asset_id (str): ID of the asset to retrieve information about.
    """
    hr_asset_headers = [
        "AssetId",
        "Addresses",
        "Hardware",
        "Aliases",
        "HostType",
        "Site",
        "OperatingSystem",
        "CPE",
        "LastScanDate",
        "LastScanId",
        "RiskScore",
    ]

    hr_service_headers = [
        "Name",
        "Port",
        "Product",
        "Protocol",
    ]

    hr_software_headers = [
        "Software",
        "Version",
    ]

    hr_users_headers = [
        "FullName",
        "Name",
        "UserId",
    ]

    hr_vulnerability_headers = [
        "Id",
        "Title",
        "Malware",
        "Exploit",
        "CVSS",
        "Risk",
        "PublishedOn",
        "ModifiedOn",
        "Severity",
        "Instances",
    ]

    try:
        asset_data = client.get_asset(asset_id)

    except DemistoException as e:
        if e.res is not None and e.res.status_code is not None and e.res.status_code == 404:
            return CommandResults(readable_output="Asset not found.")

        raise e

    asset_output = generate_new_dict(
        data=asset_data,
        name_mapping={
            "id": "AssetId",
            "ip": "Address",
            "addresses.ip": "Addresses",
            "addresses.mac": "Hardware",
            "hostNames.name": "Aliases",
            "type": "HostType",
            "Site": "Site",
            "os": "OperatingSystem",
            "vulnerabilities.total": "Vulnerabilities",
            "cpe.v2.3": "CPE",
            "riskScore": "RiskScore",
        },
        include_none=True,
    )

    site = client.find_asset_site(asset_data["id"])

    if site is not None:
        asset_output["Site"] = site.name

    asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset_data)
    asset_output["Software"] = None
    asset_output["Service"] = None
    asset_output["User"] = None

    if asset_data.get("software"):
        asset_output["Software"] = generate_new_dict(
            data=asset_data["software"],
            name_mapping={
                "description": "Software",
                "version": "Version",
            },
            include_none=True,
        )

    if asset_data.get("services"):
        asset_output["Service"] = generate_new_dict(
            data=asset_data["services"],
            name_mapping={
                "name": "Name",
                "port": "Port",
                "product": "Product",
                "protocol": "Protocol",
            },
            include_none=True,
        )

    if asset_data.get("users"):
        asset_output["User"] = generate_new_dict(
            data=asset_data["users"],
            name_mapping={
                "name": "Name",
                "fullName": "FullName",
                "id": "UserId",
            },
            include_none=True,
        )

    vulnerabilities = client.get_asset_vulnerabilities(asset_id=str(asset_data["id"]))
    asset_output["Vulnerability"] = []
    cve_indicators: list[CommandResults] = []

    for vulnerability in vulnerabilities:
        extra_info = client.get_vulnerability(vulnerability["id"])
        vulnerability_output = {
            "Id": vulnerability["id"],
            "Title": extra_info["title"],
            "Malware": extra_info["malwareKits"],
            "Exploit": extra_info["exploits"],
            "CVSS": extra_info["cvss"]["v2"]["score"],
            "Risk": extra_info["riskScore"],
            "PublishedOn": extra_info["published"],
            "ModifiedOn": extra_info["modified"],
            "Severity": extra_info["severity"],
            "Instances": vulnerability["instances"],
        }

        asset_output["Vulnerability"].append(vulnerability_output)

        if "cves" in extra_info:
            for cve in extra_info["cves"]:
                if "v3" in extra_info["cvss"]:
                    cvss_info = extra_info["cvss"]["v3"]
                    cvss_version = "3"

                else:
                    cvss_info = extra_info["cvss"]["v2"]
                    cvss_version = "2"

                cve_indicators.append(
                    CommandResults(
                        readable_output=tableToMarkdown(cve, vulnerability_output, hr_vulnerability_headers, removeNull=True),
                        indicator=Common.CVE(
                            id=cve,
                            cvss=None,  # type: ignore
                            cvss_score=cvss_info.get("score"),
                            cvss_vector=cvss_info.get("vector"),
                            cvss_version=cvss_version,
                            description=extra_info["description"]["text"],
                            modified=extra_info["modified"],
                            published=extra_info["published"],
                        ),
                    )
                )

    readable_output = tableToMarkdown(
        name=f"Nexpose Asset {asset_data['id']!s}", t=asset_output, headers=hr_asset_headers, removeNull=True
    )

    if asset_output.get("Vulnerability"):
        readable_output += tableToMarkdown(
            name="Vulnerabilities", t=asset_output["Vulnerability"], headers=hr_vulnerability_headers, removeNull=True
        )

    if asset_output.get("Software"):
        readable_output += tableToMarkdown(
            name="Software", t=asset_output["Software"], headers=hr_software_headers, removeNull=True
        )

    if asset_output.get("Service"):
        readable_output += tableToMarkdown(
            name="Services", t=asset_output["Service"], headers=hr_service_headers, removeNull=True
        )

    if asset_output.get("User"):
        readable_output += tableToMarkdown(name="Users", t=asset_output["User"], headers=hr_users_headers, removeNull=True)

    result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Nexpose.Asset",
        outputs=asset_output,
        outputs_key_field="AssetId",
        indicator=Common.Endpoint(
            id=asset_output.get("AssetId"),
            hostname=asset_output.get("Aliases"),
            ip_address=asset_output.get("Addresses"),
            os=asset_output.get("OperatingSystem"),
            vendor=VENDOR_NAME,
        ),
        raw_response=asset_data,
    )

    if cve_indicators:
        return [*cve_indicators, result]

    return result


def get_assets_command(
    client: Client, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None
) -> CommandResults | list[CommandResults]:
    """
    Retrieve a list of all assets.

    Args:
        client (Client): Client to use for API requests.
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    hr_headers = [
        "AssetId",
        "Address",
        "Name",
        "Site",
        "Exploits",
        "Malware",
        "OperatingSystem",
        "Vulnerabilities",
        "RiskScore",
        "Assessed",
        "LastScanDate",
        "LastScanId",
    ]

    assets_data = client.get_assets(page_size=page_size_int, page=page_int, sort=sort, limit=limit_int)

    if not assets_data:
        return CommandResults(readable_output="No assets found", raw_response=assets_data)

    results = []

    for asset_data in assets_data:
        asset_output = generate_new_dict(
            data=asset_data,
            name_mapping={
                "id": "AssetId",
                "ip": "Address",
                "addresses.ip": "Addresses",
                "hostName": "Name",
                "Site": "Site",
                "vulnerabilities.exploits": "Exploits",
                "vulnerabilities.malwareKits": "Malware",
                "os": "OperatingSystem",
                "vulnerabilities.total": "Vulnerabilities",
                "riskScore": "RiskScore",
                "assessedForVulnerabilities": "Assessed",
            },
            include_none=True,
        )

        site = client.find_asset_site(asset_data["id"])

        if site is not None:
            asset_output["Site"] = site.name

        asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset_data)

        results.append(
            CommandResults(
                outputs_prefix="Nexpose.Asset",
                outputs_key_field="Id",
                outputs=asset_output,
                readable_output=tableToMarkdown(f"Nexpose Asset {asset_data['id']!s}", asset_output, hr_headers, removeNull=True),
                raw_response=asset_data,
                indicator=Common.Endpoint(
                    id=asset_data["id"],
                    hostname=asset_data.get("hostName"),
                    ip_address=asset_data.get("ip"),
                    mac_address=asset_data.get("mac"),
                    os=asset_data.get("os"),
                    vendor=VENDOR_NAME,
                ),
            )
        )

    return results


def get_asset_vulnerability_command(
    client: Client, asset_id: str, vulnerability_id: str
) -> CommandResults | list[CommandResults]:
    """
    Retrieve information about vulnerability findings on an asset.

    Args:
        client (Client): Client to use for API requests.
        asset_id (str): ID of the asset to retrieve information about.
        vulnerability_id (str): ID of the vulnerability to look for
    """
    hr_vulnerability_headers = [
        "Id",
        "Title",
        "Severity",
        "RiskScore",
        "CVSS",
        "CVSSV3",
        "Published",
        "Added",
        "Modified",
        "CVSSScore",
        "CVSSV3Score",
        "Categories",
        "CVES",
    ]

    hr_results_headers = [
        "Port",
        "Protocol",
        "Since",
        "Proof",
        "Status",
    ]

    hr_solutions_headers = [
        "Type",
        "Summary",
        "Steps",
        "Estimate",
        "AdditionalInformation",
    ]

    try:
        vulnerability_data = client.get_asset_vulnerability(
            asset_id=asset_id,
            vulnerability_id=vulnerability_id,
        )

    # A 404 error is returned when the asset or vulnerability could not be found,
    # or if the asset is not vulnerable to this vulnerability.
    # This code section is to separate the different errors and return a different message for each case.
    except DemistoException as e:
        if e.res is not None and e.res.status_code is not None and e.res.status_code == 404:
            try:
                client.get_asset(asset_id)

            except DemistoException as e2:
                if e2.res is not None and e2.res.status_code is not None and e2.res.status_code == 404:
                    raise ValueError("Asset not found.")

            try:
                client.get_vulnerability(vulnerability_id)

            except DemistoException as e2:
                if e2.res is not None and e2.res.status_code is not None and e2.res.status_code == 404:  # type: ignore
                    raise ValueError("Vulnerability not found.")

            return CommandResults(readable_output=f'Asset {asset_id} is not vulnerable to "{vulnerability_id}".')

        raise e

    # Add extra info about vulnerability
    vulnerability_extra_data = client.get_vulnerability(vulnerability_id=vulnerability_id)
    vulnerability_data.update(deepcopy(vulnerability_extra_data))

    vulnerability_outputs = generate_new_dict(
        data=vulnerability_extra_data,
        name_mapping={
            "id": "Id",
            "title": "Title",
            "severity": "Severity",
            "riskScore": "RiskScore",
            "cvss.v2.vector": "CVSS",
            "cvss.v3.vector": "CVSSV3",
            "published": "Published",
            "added": "Added",
            "modified": "Modified",
            "cvss.v2.score": "CVSSScore",
            "cvss.v3.score": "CVSSV3Score",
            "categories": "Categories",
            "cves": "CVES",
        },
        include_none=True,
    )

    results_output: list = []

    if vulnerability_data.get("results"):
        results_output = generate_new_dict(
            data=vulnerability_data["results"],
            name_mapping={
                "port": "Port",
                "protocol": "Protocol",
                "since": "Since",
                "proof": "Proof",
                "status": "Status",
            },
            include_none=True,
        )

    # Remove HTML tags
    for result in results_output:
        result["Proof"] = re.sub("<.*?>", "", result["Proof"])

    # Add solutions data
    solutions_output: list = []
    solutions = client.get_asset_vulnerability_solution(asset_id, vulnerability_id)
    vulnerability_data["solutions"] = solutions

    if solutions and solutions.get("resources"):
        solutions_output = generate_new_dict(
            data=solutions["resources"],
            name_mapping={
                "type": "Type",
                "summary.text": "Summary",
                "steps.text": "Steps",
                "estimate": "Estimate",
                "additionalInformation.text": "AdditionalInformation",
            },
            include_none=True,
        )

        for idx, _val in enumerate(solutions_output):
            solutions_output[idx]["Estimate"] = readable_duration_time(solutions_output[idx]["Estimate"])

    vulnerability_outputs["Check"] = results_output
    vulnerability_outputs["Solution"] = solutions_output

    vulnerabilities_md = tableToMarkdown(
        f"Vulnerability {vulnerability_id!s}", vulnerability_outputs, hr_vulnerability_headers, removeNull=True
    )
    results_md = tableToMarkdown("Checks", results_output, hr_results_headers, removeNull=True) if len(results_output) > 0 else ""
    solutions_md = (
        tableToMarkdown("Solutions", solutions_output, hr_solutions_headers, removeNull=True)
        if solutions_output is not None
        else ""
    )

    indicators: list = []

    if vulnerability_data.get("cves"):
        for cve in vulnerability_data["cves"]:
            if "v3" in vulnerability_data["cvss"]:
                cvss_info = vulnerability_data["cvss"]["v3"]
                cvss_version = "3"

            else:
                cvss_info = vulnerability_data["cvss"]["v2"]
                cvss_version = "2"

            indicators.append(
                Common.CVE(
                    id=cve,
                    cvss=None,  # type: ignore
                    cvss_score=cvss_info.get("score"),
                    cvss_vector=cvss_info.get("vector"),
                    cvss_version=cvss_version,
                    description=vulnerability_data["description"]["text"],
                    modified=vulnerability_data["modified"],
                    published=vulnerability_data["published"],
                )
            )

    if len(indicators) == 0:
        indicators = [None]

    results = []

    for indicator in indicators:
        results.append(
            CommandResults(
                outputs_prefix="Nexpose.Asset",
                outputs_key_field="AssetId",
                outputs={
                    "AssetId": asset_id,
                    "Vulnerability": [vulnerability_outputs],
                },
                readable_output=vulnerabilities_md + results_md + solutions_md,
                indicator=indicator,
            )
        )

    return results


def get_generated_report_status_command(client: Client, report_id: str, instance_id: str) -> CommandResults:
    """
    Retrieve information about a generated report's status.

    Args:
        client (Client): Client to use for API requests.
        report_id (str): ID of the report to retrieve information about.
        instance_id (str): ID of the report instance to retrieve information about.
    """
    response = client.get_report_history(report_id, instance_id)

    context = {
        "ID": report_id,
        "InstanceID": instance_id,
        "Status": response.get("status", "unknown"),
    }

    hr = tableToMarkdown("Report Generation Status", context)

    return CommandResults(
        readable_output=hr,
        outputs_prefix="Nexpose.Report",
        outputs=context,
        outputs_key_field=["ID", "InstanceID"],
        raw_response=response,
    )


def get_report_templates_command(client: Client) -> CommandResults:
    """
    Retrieve information about all available report templates.

    Args:
        client (Client): Client to use for API requests.
    """
    hr_headers = ["Id", "Name", "Description", "Type"]

    report_templates_data = client.get_report_templates()

    if not report_templates_data.get("resources"):
        return CommandResults(
            readable_output="No templates found",
            raw_response=report_templates_data,
        )

    report_templates_output = generate_new_dict(
        data=report_templates_data["resources"],
        name_mapping={
            "id": "Id",
            "name": "Name",
            "description": "Description",
            "type": "Type",
        },
        include_none=True,
    )

    return CommandResults(
        outputs_prefix="Nexpose.Template",
        outputs_key_field="Id",
        outputs=report_templates_output,
        readable_output=tableToMarkdown("Nexpose Templates", report_templates_output, hr_headers, removeNull=True),
        raw_response=report_templates_data,
    )


def get_scan_command(client: Client, scan_ids: str) -> list[CommandResults]:
    """
    Retrieve information about a specific or multiple scans.

    Args:
        client (Client): Client to use for API requests.
        scan_ids (str | list): ID of the scan to retrieve.
    """
    scan_ids_list = argToList(scan_ids)

    results = []

    for scan_id in scan_ids_list:
        try:
            scan_data = client.get_scan(scan_id)

        except DemistoException as e:
            if e.res is not None and e.res.status_code is not None and e.res.status_code == 404:
                scan_entry = CommandResults(readable_output=f"Scan for ID {scan_id} was not found.")

            else:
                raise e

        else:
            scan_entry = get_scan_entry(scan_data)

        results.append(scan_entry)

    return results


def get_scans_command(
    client: Client,
    active: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    sort: str | None = None,
    limit: str | None = None,
) -> CommandResults:
    """
    Retrieve a list of all scans.

    Args:
        client (Client): Client to use for API requests.
        active (str | None, optional): Whether to return active scans or not. Defaults to False.
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    scans_data = client.get_scans(
        active=argToBoolean(active) if active is not None else None,
        page_size=page_size_int,
        page=page_int,
        sort=sort,
        limit=limit_int,
    )

    if not scans_data:
        return CommandResults(
            readable_output="No scans found",
            raw_response=scans_data,
        )

    normalized_scans = [normalize_scan_data(scan) for scan in scans_data]

    scan_hr = tableToMarkdown(
        name="Nexpose Scans",
        t=normalized_scans,
        headers=[
            "Id",
            "ScanType",
            "ScanName",
            "StartedBy",
            "Assets",
            "TotalTime",
            "Completed",
            "Status",
            "Message",
        ],
        removeNull=True,
    )

    return CommandResults(
        outputs_prefix="Nexpose.Scan",
        outputs_key_field="Id",
        outputs=normalized_scans,
        readable_output=scan_hr,
        raw_response=scans_data,
    )


def get_sites_command(
    client: Client, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None
) -> CommandResults:
    """
    Retrieve a list of sites.

    Args:
        client (Client): Client to use for API requests.
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    hr_headers = ["Id", "Name", "Assets", "Vulnerabilities", "Risk", "Type", "LastScan"]

    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    sites_data = client.get_sites(
        page_size=page_size_int,
        page=page_int,
        sort=sort,
        limit=limit_int,
    )

    if not sites_data:
        return CommandResults(
            readable_output="No sites found",
            raw_response=sites_data,
        )

    sites_output = generate_new_dict(
        data=sites_data,
        name_mapping={
            "id": "Id",
            "name": "Name",
            "assets": "Assets",
            "vulnerabilities.total": "Vulnerabilities",
            "riskScore": "Risk",
            "type": "Type",
            "lastScanTime": "LastScan",
        },
        include_none=True,
    )

    return CommandResults(
        outputs_prefix="Nexpose.Site",
        outputs_key_field="Id",
        outputs=sites_output,
        readable_output=tableToMarkdown("Nexpose Sites", sites_output, hr_headers, removeNull=True),
        raw_response=sites_data,
    )


def list_scan_schedule_command(
    client: Client,
    site_id: str | None = None,
    site_name: str | None = None,
    schedule_id: str | None = None,
    limit: str | None = None,
) -> CommandResults:
    """
    Retrieve information about scan schedules for a specific site or a specific scan schedule.

    Args:
        client (Client): Client to use for API requests.
        site_id (str | None, optional): ID of a site to retrieve scan schedules from.
        site_name (str | None, optional): Name of a site to retrieve scan schedules from.
            Can be used instead of "site_id".
        schedule_id (str): ID of a specific scan schedule to retrieve.
            Defaults to None (Results in getting all scan schedules for the site).
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    hr_headers = [
        "Enable",
        "StartDate",
        "Name",
        "MaxDuration",
        "Repeat",
        "NextStart",
    ]

    limit_int = arg_to_number(limit, required=False)

    if not schedule_id:
        scan_schedules_data = client.get_scan_schedules(site_id=site.id)

        if limit_int is not None and limit_int < len(scan_schedules_data):
            scan_schedules_data = scan_schedules_data[:limit_int]

    else:
        scan_schedules_data = [
            client.get_scan_schedule(
                site_id=site.id,
                schedule_id=schedule_id,
            )
        ]

    if not scan_schedules_data:
        return CommandResults(
            readable_output="No scan schedules were found for the site.",
            raw_response=scan_schedules_data,
        )

    hr_outputs = generate_new_dict(
        data=scan_schedules_data,
        name_mapping={
            "id": "Id",
            "enabled": "Enable",
            "start": "StartDate",
            "scanName": "Name",
            "duration": "MaxDuration",
            "repeat.every": "Repeat",
            "nextRuntimes": "NextStart",
        },
    )

    for scan_schedule in hr_outputs:
        if scan_schedule.get("MaxDuration"):
            scan_schedule["MaxDuration"] = readable_duration_time(scan_schedule["MaxDuration"])
        if scan_schedule.get("Repeat"):
            scan_schedule["Repeat"] = "every " + scan_schedule["Repeat"]

    return CommandResults(
        outputs_prefix="Nexpose.ScanSchedule",
        outputs_key_field="id",
        outputs=scan_schedules_data,
        readable_output=tableToMarkdown("Nexpose Scan Schedules", hr_outputs, hr_headers, removeNull=True),
        raw_response=scan_schedules_data,
    )


def list_shared_credential_command(client: Client, credential_id: str | None = None, limit: str | None = None) -> CommandResults:
    """
    Retrieve information about all or a specific vulnerability.

    Args:
        client (Client): Client to use for API requests.
        credential_id (str | None, optional): ID of a specific shared credential to retrieve.
            Defaults to None (Results in getting all vulnerabilities).
        limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit.
            Defaults to None.
    """
    hr_headers = [
        "Id",
        "Name",
        "Service",
        "Domain",
        "UserName",
        "AvailableToSites",
    ]

    limit_int = arg_to_number(limit, required=False)

    if not credential_id:
        shared_credentials_data = client.get_shared_credentials()

        if limit_int is not None and limit_int < len(shared_credentials_data):
            shared_credentials_data = shared_credentials_data[:limit_int]

    else:
        shared_credentials_data = [client.get_shared_credential(credential_id)]

    if not shared_credentials_data:
        return CommandResults(
            readable_output="No shared credentials were found.",
            raw_response=shared_credentials_data,
        )

    shared_credentials_hr = generate_new_dict(
        data=shared_credentials_data,
        name_mapping={
            "id": "Id",
            "name": "Name",
            "account.service": "Service",
            "account.domain": "Domain",
            "account.username": "UserName",
        },
    )

    for shared_credential in shared_credentials_hr:
        if shared_credential.get("sites"):
            shared_credential["AvailableToSites"] = len(shared_credential["sites"])

    return CommandResults(
        outputs_prefix="Nexpose.SharedCredential",
        outputs_key_field="id",
        outputs=shared_credentials_data,
        readable_output=tableToMarkdown("Nexpose Shared Credentials", shared_credentials_hr, hr_headers, removeNull=True),
        raw_response=shared_credentials_data,
    )


def list_assigned_shared_credential_command(
    client: Client, site_id: str | None = None, site_name: str | None = None, limit: str | None = None
) -> CommandResults:
    """
    Retrieve information about shared credentials for a specific site.

    Args:
        client (Client): Client to use for API requests.
        site_id (str | None, optional): ID of a site to retrieve shared credentials from.
        site_name (str | None, optional): Name of a site to retrieve shared credentials from.
            Can be used instead of "site_id".
        limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    hr_headers = [
        "Id",
        "Name",
        "Service",
        "Enabled",
    ]

    limit_int = arg_to_number(limit, required=False)

    response_data = client.get_assigned_shared_credentials(site_id=site.id)

    if not response_data:
        site_id = site.name if site.name else site.id

        return CommandResults(
            readable_output=f'No assigned shared credentials were found for site "{site_id}".',
            raw_response=response_data,
        )

    if limit_int:
        response_data = response_data[:limit_int]

    assigned_shared_credentials_hr = generate_new_dict(
        data=response_data,
        name_mapping={
            "id": "Id",
            "name": "Name",
            "service": "Service",
            "enabled": "Enabled",
        },
    )

    return CommandResults(
        outputs_prefix="Nexpose.AssignedSharedCredential",
        outputs_key_field="id",
        outputs=response_data,
        readable_output=tableToMarkdown(
            "Nexpose Assigned Shared Credentials", assigned_shared_credentials_hr, hr_headers, removeNull=True
        ),
        raw_response=response_data,
    )


def list_site_scan_credential_command(
    client: Client,
    site_id: str | None = None,
    site_name: str | None = None,
    credential_id: str | None = None,
    limit: str | None = None,
) -> CommandResults:
    """
    Retrieve information about all or a specific scan credential.

    Args:
        client (Client): Client to use for API requests.
        site_id (str | None, optional): ID of a site to retrieve scan credentials from.
        site_name (str | None, optional): Name of a site to retrieve scan credentials from.
            Can be used instead of "site_id".
        credential_id (str | None, optional): ID of a specific scan credential to retrieve.
        limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit.
            Defaults to None.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    hr_headers = [
        "Id",
        "Enabled",
        "Name",
        "Service",
        "UserName",
        "RestrictToHostName",
        "RestrictToPort",
    ]

    limit_int = arg_to_number(limit, required=False)

    if credential_id is not None:
        site_scan_credentials_data = client.get_site_scan_credential(site_id=site.id, credential_id=credential_id)
        site_scan_credentials_data = [site_scan_credentials_data]

    else:
        site_scan_credentials_data = client.get_site_scan_credentials(site_id=site.id)

    if not site_scan_credentials_data:
        site_id = site.name if site.name else site.id
        return CommandResults(
            readable_output=f'No site scan credentials were found for site "{site_id}".',
            raw_response=site_scan_credentials_data,
        )

    if limit_int and len(site_scan_credentials_data) > limit_int:
        site_scan_credentials_data = site_scan_credentials_data[:limit_int]

    site_scan_credentials_hr = generate_new_dict(
        data=site_scan_credentials_data,
        name_mapping={
            "id": "Id",
            "enabled": "Enabled",
            "name": "Name",
            "account.service": "Service",
            "account.username": "UserName",
            "hostRestriction": "RestrictToHostName",
            "portRestriction": "RestrictToPort",
        },
    )

    return CommandResults(
        outputs_prefix="Nexpose.SiteScanCredential",
        outputs_key_field="id",
        outputs=site_scan_credentials_data,
        readable_output=tableToMarkdown("Nexpose Site Scan Credentials", site_scan_credentials_hr, hr_headers, removeNull=True),
        raw_response=site_scan_credentials_data,
    )


def list_vulnerability_command(
    client: Client,
    vulnerability_id: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    sort: str | None = None,
    limit: str | None = None,
) -> CommandResults:
    """
    Retrieve information about all or a specific vulnerability.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_id (str | None, optional): ID of a specific vulnerability to retrieve.
            Defaults to None (Results in getting all vulnerabilities).
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    hr_headers = [
        "Title",
        "MalwareKits",
        "Exploits",
        "CVSS",
        "CVSSv3",
        "Risk",
        "PublishedOn",
        "ModifiedOn",
        "Severity",
    ]

    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    if not vulnerability_id:
        vulnerabilities_data = client.get_vulnerabilities(
            page_size=page_size_int,
            page=page_int,
            sort=sort,
            limit=limit_int,
        )

    else:
        vulnerabilities_data = [client.get_vulnerability(vulnerability_id)]

    if not vulnerabilities_data:
        return CommandResults(
            readable_output="No vulnerability exceptions were found.",
            raw_response=vulnerabilities_data,
        )

    vulnerabilities_hr = generate_new_dict(
        data=vulnerabilities_data,
        name_mapping={
            "title": "Title",
            "malwareKits": "MalwareKits",
            "exploits": "Exploits",
            "cvss.v2.score": "CVSS",
            "cvss.v3.score": "CVSSv3",
            "riskScore": "Risk",
            "published": "PublishedOn",
            "modified": "ModifiedOn",
            "severity": "Severity",
        },
    )

    return CommandResults(
        outputs_prefix="Nexpose.Vulnerability",
        outputs_key_field="id",
        outputs=vulnerabilities_data,
        readable_output=tableToMarkdown("Nexpose Vulnerabilities", vulnerabilities_hr, hr_headers, removeNull=True),
        raw_response=vulnerabilities_data,
    )


def list_vulnerability_exceptions_command(
    client: Client,
    vulnerability_exception_id: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    sort: str | None = None,
    limit: str | None = None,
) -> CommandResults:
    """
    Retrieve information about all or a specific vulnerability exception.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_exception_id (str | None, optional): ID of a specific vulnerability exception to retrieve.
            Defaults to None (Results in getting all vulnerability exceptions).
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    hr_headers = [
        "Id",
        "Vulnerability",
        "ExceptionScope",
        "Reason",
        "ReportedBy",
        "ReportedOn",
        "ReviewStatus",
        "ReviewedOn",
        "ExpiresOn",
    ]

    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    if not vulnerability_exception_id:
        vulnerability_exceptions_data = client.get_vulnerability_exceptions(
            page_size=page_size_int,
            page=page_int,
            sort=sort,
            limit=limit_int,
        )

    else:
        vulnerability_exceptions_data = [client.get_vulnerability_exception(vulnerability_exception_id)]

    if not vulnerability_exceptions_data:
        return CommandResults(
            readable_output="No vulnerability exceptions were found.",
            raw_response=vulnerability_exceptions_data,
        )

    hr_vulnerability_exceptions_data = generate_new_dict(
        data=vulnerability_exceptions_data,
        name_mapping={
            "id": "Id",
            "scope.vulnerability": "Vulnerability",
            "scope.type": "ExceptionScope",
            "submit.reason": "Reason",
            "submit.name": "ReportedBy",
            "state": "ReviewStatus",
            "review.date": "ReviewedOn",
            "expires": "ExpiresOn",
        },
    )

    return CommandResults(
        outputs_prefix="Nexpose.VulnerabilityException",
        outputs_key_field="id",
        outputs=vulnerability_exceptions_data,
        readable_output=tableToMarkdown(
            "Nexpose Vulnerability Exceptions", hr_vulnerability_exceptions_data, hr_headers, removeNull=True
        ),
        raw_response=vulnerability_exceptions_data,
    )


def search_assets_command(
    client: Client,
    query: str | None = None,
    ip_address_is: str | None = None,
    host_name_is: str | None = None,
    risk_score_higher_than: str | None = None,
    vulnerability_title_contains: str | None = None,
    site_id_in: str | None = None,
    site_name_in: str | None = None,
    match: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    sort: str | None = None,
    limit: str | None = None,
) -> CommandResults | list[CommandResults]:
    """
    Retrieve a list of all assets with access permissions that match the provided search filters.

    Args:
        client (Client): Client to use for API requests.
        query (str | None, optional): String based filters to use separated by ';'. Defaults to None.
        ip_address_is (str | None, optional): IP address(es) to filter for. Defaults to None.
        host_name_is (str | None, optional): Hostname(s) to filter for. Defaults to None.
        risk_score_higher_than (str | None, optional): Filter for risk scores that are higher than the provided value.
            Defaults to None.
        vulnerability_title_contains (str | None, optional): Filter for vulnerability titles that contain the provided value.
            Defaults to None. Defaults to None.
        site_id_in (str | None, optional): Filter for assets that are under a specific site(s).
            Defaults to None.
        site_name_in (str | None, optional): Filter for assets that are under a specific site(s).
            Defaults to None.
        match (str | None, optional): Determine if the filters should match all or any of the filters.
            Can be either "all" or "any". Defaults to None (Results in using MATCH_DEFAULT_VALUE).
        page_size (str | None, optional): Number of scans to return per page when using pagination.
            Defaults to DEFAULT_PAGE_SIZE.
        page (str | None, optional): Specific pagination page to retrieve. Defaults to None.
            Defaults to None.
        sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None.
        limit (str | None, optional): Limit the number of scans to return. None means to not use a limit.
            Defaults to None.
    """
    # sites: list[Site] = []

    hr_headers = [
        "AssetId",
        "Address",
        "Name",
        "Site",
        "Exploits",
        "Malware",
        "OperatingSystem",
        "RiskScore",
        "Assessed",
        "LastScanDate",
        "LastScanId",
    ]

    page_size_int = arg_to_number(page_size, required=False)
    page_int = arg_to_number(page, required=False)
    limit_int = arg_to_number(limit, required=False)

    if not match:
        match = MATCH_DEFAULT_VALUE

    filters_data = parse_asset_filters(
        client=client,
        ip_address_is=ip_address_is,
        host_name_is=host_name_is,
        risk_score_higher_than=risk_score_higher_than,
        vulnerability_title_contains=vulnerability_title_contains,
        site_id_in=site_id_in,
        site_name_in=site_name_in,
        query=query,
    )

    assets = client.search_assets(
        filters=convert_asset_search_filters(filters_data),
        match=match,
        page_size=page_size_int,
        page=page_int,
        sort=sort,
        limit=limit_int,
    )

    if not assets:
        return CommandResults(readable_output="No assets were found")

    results = []

    for asset in assets:
        asset_output = generate_new_dict(
            data=asset,
            name_mapping={
                "id": "AssetId",
                "ip": "Address",
                "addresses.ip": "Addresses",
                "hostName": "Name",
                "Site": "Site",
                "vulnerabilities.exploits": "Exploits",
                "vulnerabilities.malwareKits": "Malware",
                "os": "OperatingSystem",
                "vulnerabilities.total": "Vulnerabilities",
                "riskScore": "RiskScore",
                "assessedForVulnerabilities": "Assessed",
            },
            include_none=True,
        )

        site = client.find_asset_site(asset["id"])

        if site is not None:
            asset_output["Site"] = site.name

        asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset)

        results.append(
            CommandResults(
                outputs_prefix="Nexpose.Asset",
                outputs_key_field="Id",
                outputs=asset_output,
                readable_output=tableToMarkdown(f"Nexpose Asset {asset['id']!s}", asset_output, hr_headers, removeNull=True),
                raw_response=asset,
                indicator=Common.Endpoint(
                    id=asset["id"],
                    hostname=asset.get("hostName"),
                    ip_address=asset.get("ip"),
                    mac_address=asset.get("mac"),
                    os=asset.get("os"),
                    vendor=VENDOR_NAME,
                ),
            )
        )

    return results


def set_assigned_shared_credential_status_command(
    client: Client, credential_id: str, enabled: bool, site_id: str | None = None, site_name: str | None = None
) -> CommandResults:
    """
    Enable or disable a shared credential.

    Args:
        client (Client): Client to use for API requests.
        credential_id (str): ID of the shared credential to enable or disable.
        enabled (bool): Whether to enable or disable the shared credential.
        site_id (Site): ID of a site to use for API requests.
        site_name (Site): Name of a site to use for API requests. Can be used instead of "site_id".

    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    response_data = client.set_assigned_shared_credential_status(
        site_id=site.id,
        shared_credential_id=credential_id,
        enabled=enabled,
    )

    return CommandResults(
        readable_output=f'Shared credential "{credential_id}" enablement has been set to "{str(enabled).lower()}".',
        raw_response=response_data,
    )


def start_assets_scan_command(
    client: Client, ip_addresses: str | None = None, hostnames: str | None = None, name: str | None = None
) -> CommandResults:
    """
    Start a scan on the provided assets.

    Note:
        Both `ip_addresses` and `hostnames` are optional, but at least one of them must be provided.

    Args:
        client (Client): Client to use for API requests.
        ip_addresses (str | None, optional): IP(s) of assets to scan. Defaults to None
        hostnames (str | None, optional): Hostname(s) of assets to scan. Defaults to None
        name (str | None): Name to set for the new scan.
            Defaults to None (Results in using a "scan <date>" format).
    """
    if ip_addresses is None and hostnames is None:
        raise ValueError('At least one of "ips" and "hostnames" must be provided.')

    ip_addresses_list = None
    hostnames_list = None
    asset_filter = ""

    if ip_addresses is not None:
        ip_addresses_list = argToList(ip_addresses)
        asset_filter = "ip-address is " + ip_addresses_list[0]

    if hostnames is not None:
        hostnames_list = argToList(hostnames)
        asset_filter = "host-name is " + hostnames_list[0]

    if not name:
        name = f"scan {datetime.now()}"

    asset_data = client.search_assets(filters=convert_asset_search_filters(asset_filter), match="all")

    if not asset_data:
        return CommandResults(
            readable_output="Could not find assets.",
            raw_response=asset_data,
        )

    site = client.find_asset_site(asset_data[0]["id"])

    if site is None:
        return CommandResults(
            readable_output="Could not find site.",
            raw_response=site,
        )

    hosts = []

    if ip_addresses_list:
        hosts.extend(ip_addresses_list)

    if hostnames_list:
        hosts.extend(hostnames_list)

    scan_response = client.start_site_scan(site_id=site.id, scan_name=name, hosts=hosts)

    if "id" not in scan_response:
        return CommandResults(
            readable_output="Could not start scan.",
            raw_response=scan_response,
        )

    return get_scan_entry(client.get_scan(scan_response["id"]))


def start_site_scan_command(
    client: Client, site_id: str | None = None, site_name: str | None = None, hosts: str | None = None, name: str | None = None
) -> CommandResults:
    """
    Start a scan for a specific site.

    Args:
        client (Client): Client to use for API requests.
        site_id (str | None, optional): ID of a site to start a scan on.
        site_name (str | None, optional): Name of a site to start a scan on. Can be used instead of "site_id".
        hosts (str | None): Hosts to scan. Defaults to None (Results in scanning all hosts).
        name (str | None): Name to set for the new scan.
            Defaults to None (Results in using a "scan <date>" format).
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    scan_response = client.start_site_scan(
        site_id=site.id,
        scan_name=name if name else f"scan {datetime.now()}",
        hosts=argToList(hosts) if hosts else None,  # type: ignore[arg-type]
    )

    if not scan_response or "id" not in scan_response:
        return CommandResults(
            readable_output="Could not start scan",
            raw_response=scan_response,
        )

    scan_data = client.get_scan(scan_response["id"])
    return get_scan_entry(scan_data)


def update_scan_command(client: Client, scan_id: str, scan_status: ScanStatus) -> CommandResults:
    """
    Update status for a specific scan.

    Args:
        client (Client): Client to use for API requests.
        scan_id (str): ID of the scan to update.
        scan_status (ScanStatus): Status to set the scan to.
    """
    response = client.update_scan_status(scan_id, scan_status)

    return CommandResults(
        readable_output=f'Successfully updated scan status to "{scan_status.value}"',
        raw_response=response,
    )


def update_shared_credential_command(
    client: Client,
    shared_credential_id: str,
    name: str,
    site_assignment: str,
    service: str,
    authentication_type: str | None = None,
    community_name: str | None = None,
    database: str | None = None,
    description: str | None = None,
    domain: str | None = None,
    host_restriction: str | None = None,
    http_realm: str | None = None,
    notes_id_password: str | None = None,
    ntlm_hash: str | None = None,
    oracle_enumerate_sids: str | None = None,
    oracle_listener_password: str | None = None,
    oracle_sid: str | None = None,
    password: str | None = None,
    port_restriction: str | None = None,
    sites: str | None = None,
    privacy_password: str | None = None,
    privacy_type: str | None = None,
    ssh_key_pem: str | None = None,
    ssh_permission_elevation: str | None = None,
    ssh_permission_elevation_password: str | None = None,
    ssh_permission_elevation_username: str | None = None,
    ssh_private_key_password: str | None = None,
    use_windows_authentication: str | None = None,
    username: str | None = None,
) -> CommandResults:
    """
    Update an existing shared credential.

    Args:
        client (Client): Client to use for API requests.
        shared_credential_id (str): ID of the shared credential to update.
        name (str): Name of the credential.
        site_assignment (str): Site assignment configuration for the credential.
        service (str): Credential service type.
        authentication_type (str): SNMPv3 authentication type for the credential.
        community_name (str | None, optional): SNMP community for authentication.
        database (str | None, optional): Database name.
        description (str | None, optional): Description for the credential.
        domain (str | None, optional): Domain address.
        host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
        http_realm (str | None, optional): HTTP realm.
        notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating.
        ntlm_hash (str | None, optional): NTLM password hash.
        oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate
            SIDs from the environment.
        oracle_listener_password (str | None, optional): The Oracle Net Listener password.
            Used to enumerate SIDs from the environment.
        oracle_sid (str | None, optional): Oracle database name.
        password (str | None, optional): Password for the credential.
        port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
            on a specific port. Can be used only if `host_restriction` is used.
        sites (str | None, optional): List of site IDs for the shared credential that are explicitly assigned
            access to the shared scan credential, allowing it to use the credential during a scan.
        privacy_password (str | None, optional): SNMPv3 privacy password to use.
        privacy_type (str | None, optional): SNMPv3 Privacy protocol to use.
        ssh_key_pem (str | None, optional): PEM formatted private key.
        ssh_permission_elevation (str | None, optional): Elevation type to use for scans.
        ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
        ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
        ssh_private_key_password (str | None, optional): Password for the private key.
        use_windows_authentication (str | None, optional): Whether to use Windows authentication.
        username (str | None, optional): Username for the credential.
    """
    oracle_enumerate_sids_list = None
    sites_list = None
    snmpv3_authentication_type_enum = None
    snmpv3_privacy_type_enum = None
    ssh_permission_elevation_enum = None
    use_windows_authentication_bool = None

    if oracle_enumerate_sids is not None:
        oracle_enumerate_sids_list = argToBoolean(oracle_enumerate_sids)

    if sites is not None:
        sites_list = [int(item) for item in argToList(sites)]

    if authentication_type is not None:
        snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type]

    if privacy_type is not None:
        snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type]

    if ssh_permission_elevation is not None:
        ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation]

    if use_windows_authentication is not None:
        use_windows_authentication_bool = argToBoolean(use_windows_authentication)

    response_data = client.update_shared_credential(
        shared_credential_id=shared_credential_id,
        name=name,
        site_assignment=SharedCredentialSiteAssignment[site_assignment],
        service=CredentialService[service],
        database_name=database,
        description=description,
        domain=domain,
        host_restriction=host_restriction,
        http_realm=http_realm,
        notes_id_password=notes_id_password,
        ntlm_hash=ntlm_hash,
        oracle_enumerate_sids=oracle_enumerate_sids_list,
        oracle_listener_password=oracle_listener_password,
        oracle_sid=oracle_sid,
        password=password,
        port_restriction=port_restriction,
        sites=sites_list,
        snmp_community_name=community_name,
        snmpv3_authentication_type=snmpv3_authentication_type_enum,
        snmpv3_privacy_password=privacy_password,
        snmpv3_privacy_type=snmpv3_privacy_type_enum,
        ssh_key_pem=ssh_key_pem,
        ssh_permission_elevation=ssh_permission_elevation_enum,
        ssh_permission_elevation_password=ssh_permission_elevation_password,
        ssh_permission_elevation_username=ssh_permission_elevation_username,
        ssh_private_key_password=ssh_private_key_password,
        use_windows_authentication=use_windows_authentication_bool,
        username=username,
    )

    return CommandResults(
        readable_output=f"Shared credential with ID {shared_credential_id} has been updated.", raw_response=response_data
    )


def update_site_scan_credential_command(
    client: Client,
    credential_id: str,
    name: str,
    service: str,
    site_id: str | None = None,
    site_name: str | None = None,
    authentication_type: str | None = None,
    community_name: str | None = None,
    database: str | None = None,
    description: str | None = None,
    domain: str | None = None,
    host_restriction: str | None = None,
    http_realm: str | None = None,
    notes_id_password: str | None = None,
    ntlm_hash: str | None = None,
    oracle_enumerate_sids: str | None = None,
    oracle_listener_password: str | None = None,
    oracle_sid: str | None = None,
    password: str | None = None,
    port_restriction: str | None = None,
    privacy_password: str | None = None,
    privacy_type: str | None = None,
    ssh_key_pem: str | None = None,
    ssh_permission_elevation: str | None = None,
    ssh_permission_elevation_password: str | None = None,
    ssh_permission_elevation_username: str | None = None,
    ssh_private_key_password: str | None = None,
    use_windows_authentication: str | None = None,
    username: str | None = None,
) -> CommandResults:
    """
    Update an existing site scan credential.

    Args:
        client (Client): Client to use for API requests.
        credential_id (str): ID of the site scan credential to update.
        name (str): Name of the credential.
        service (str): Credential service type.
        site_id (str | None, optional): ID of a site to update the site scan credential for.
        site_name (str | None, optional): Name of a site to update the site scan credential for.
            Can be used instead of "site_id".
        authentication_type (str | None, optional): SNMPv3 authentication type for the credential.
        community_name (str | None, optional): SNMP community for authentication.
        database (str | None, optional): Database name.
        description (str | None, optional): Description for the credential.
        domain (str | None, optional): Domain address.
        host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to.
        http_realm (str | None, optional): HTTP realm.
        notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating.
        ntlm_hash (str | None, optional): NTLM password hash.
        oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate
            SIDs from the environment.
        oracle_listener_password (str | None, optional): The Oracle Net Listener password.
            Used to enumerate SIDs from the environment.
        oracle_sid (str | None, optional): Oracle database name.
        password (str | None, optional): Password for the credential.
        port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate
            on a specific port. Can be used only if `host_restriction` is used.
        privacy_password (str | None, optional): SNMPv3 privacy password to use.
        privacy_type (str | None, optional): SNMPv3 Privacy protocol to use.
        ssh_key_pem (str | None, optional): PEM formatted private key.
        ssh_permission_elevation (str | None, optional): Elevation type to use for scans.
        ssh_permission_elevation_password (str | None, optional): Password to use for elevation.
        ssh_permission_elevation_username (str | None, optional): Username to use for elevation.
        ssh_private_key_password (str | None, optional): Password for the private key.
        use_windows_authentication (str | None, optional): Whether to use Windows authentication.
        username (str | None, optional): Username for the credential.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    oracle_enumerate_sids_bool = None
    snmpv3_authentication_type_enum = None
    snmpv3_privacy_type_enum = None
    ssh_permission_elevation_enum = None
    use_windows_authentication_bool = None

    if oracle_enumerate_sids is not None:
        oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids)

    if authentication_type is not None:
        snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type]

    if privacy_type is not None:
        snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type]

    if ssh_permission_elevation is not None:
        ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation]

    if use_windows_authentication is not None:
        use_windows_authentication_bool = argToBoolean(use_windows_authentication)

    response_data = client.update_site_scan_credential(
        site_id=site.id,
        credential_id=credential_id,
        name=name,
        service=CredentialService[service],
        database_name=database,
        description=description,
        domain=domain,
        host_restriction=host_restriction,
        http_realm=http_realm,
        notes_id_password=notes_id_password,
        ntlm_hash=ntlm_hash,
        oracle_enumerate_sids=oracle_enumerate_sids_bool,
        oracle_listener_password=oracle_listener_password,
        oracle_sid=oracle_sid,
        password=password,
        port_restriction=port_restriction,
        snmp_community_name=community_name,
        snmpv3_authentication_type=snmpv3_authentication_type_enum,
        snmpv3_privacy_password=privacy_password,
        snmpv3_privacy_type=snmpv3_privacy_type_enum,
        ssh_key_pem=ssh_key_pem,
        ssh_permission_elevation=ssh_permission_elevation_enum,
        ssh_permission_elevation_password=ssh_permission_elevation_password,
        ssh_permission_elevation_username=ssh_permission_elevation_username,
        ssh_private_key_password=ssh_private_key_password,
        use_windows_authentication=use_windows_authentication_bool,
        username=username,
    )

    return CommandResults(
        readable_output=f"Site scan credential with ID {credential_id} has been updated.",
        raw_response=response_data,
    )


def update_scan_schedule_command(
    client: Client,
    schedule_id: int,
    on_scan_repeat: str,
    start: str,
    site_id: str | None = None,
    site_name: str | None = None,
    excluded_asset_groups: str | None = None,
    excluded_targets: str | None = None,
    included_asset_groups: str | None = None,
    included_targets: str | None = None,
    duration_days: str | None = None,
    duration_hours: str | None = None,
    duration_minutes: str | None = None,
    enabled: str | None = None,
    frequency: str | None = None,
    interval: str | None = None,
    scan_name: str | None = None,
    date_of_month: str | None = None,
    scan_template_id: str | None = None,
) -> CommandResults:
    """
    Update a site scan schedule.

    Args:
        client (Client): Client to use for API requests.
        schedule_id (str): ID of the scan schedule to update.
        on_scan_repeat (str): The desired behavior of a repeating scheduled scan
            when the previous scan was paused due to reaching its maximum duration.
        start (str): The scheduled start date and time formatted in ISO 8601 format.
        site_id (str | None, optional): ID of a site to create a scheduled scan for.
        site_name (str | None, optional): Name of a site to create a scheduled scan for.
            Can be used instead of "site_id".
        excluded_asset_groups (str | None, optional): Asset groups to exclude from the scan.
        excluded_targets (str | None, optional): Addresses to exclude from the scan. Each address is a string that
            can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation.
        included_asset_groups (str | None, optional): Asset groups to include in the scan.
        included_targets (str | None, optional): Addresses to include in the scan.  Each address is a string that
            can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation.
        duration_days (str | None, optional): Maximum duration of the scan in days.
            Can be used along with `duration_hours` and `duration_minutes`.
        duration_hours (str | None, optional): Maximum duration of the scan in hours.
            Can be used along with `duration_days` and `duration_minutes`.
        duration_minutes (str | None, optional): Maximum duration of the scan in minutes.
            Can be used along with `duration_days` and `duration_hours`.
        enabled (str | None, optional): A flag indicating whether the scan schedule is enabled.
           Defaults to None, which results in using True.
        frequency (str | None, optional): Frequency for the schedule to repeat.
        interval (str | None, optional): The interval time the schedule should repeat.
            Required if frequency is set to any value other than `DATE_OF_MONTH`.
        date_of_month(str | None, optional): Specifies the schedule repeat day of the interval month.
            Required and used only if frequency is set to `DATE_OF_MONTH`.
        scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule.
            If not explicitly set in the schedule, the scan name will be generated prior to the scan launching.
        scan_template_id (str | None, optional): ID of the scan template to use.
    """
    site = Site(
        site_id=site_id,
        site_name=site_name,
        client=client,
    )

    excluded_asset_groups_list = None
    excluded_targets_list = None
    frequency_enum = None
    included_asset_groups_list = None
    included_targets_list = None

    if excluded_asset_groups is not None:
        excluded_asset_groups_list = [int(asset_id) for asset_id in argToList(excluded_asset_groups)]

    if excluded_targets is not None:
        excluded_targets_list = argToList(excluded_targets)

    if frequency is not None:
        frequency_enum = RepeatFrequencyType[frequency]

    if included_asset_groups is not None:
        included_asset_groups_list = [int(asset_id) for asset_id in argToList(included_asset_groups)]

    if included_targets is not None:
        included_targets_list = argToList(included_targets)

    duration_days_int = arg_to_number(duration_days, required=False)
    duration_hours_int = arg_to_number(duration_hours, required=False)
    duration_minutes_int = arg_to_number(duration_minutes, required=False)
    interval_int = arg_to_number(interval, required=False)
    date_of_month_int = arg_to_number(date_of_month, required=False)

    if enabled is not None:
        enabled_bool = argToBoolean(enabled)

    else:
        enabled_bool = True

    duration = generate_duration_time(
        days=duration_days_int,
        hours=duration_hours_int,
        minutes=duration_minutes_int,
    )

    response_data = client.update_scan_schedule(
        site_id=site.id,
        scan_schedule_id=schedule_id,
        enabled=enabled_bool,
        repeat_behaviour=on_scan_repeat,
        start_date=start,
        excluded_asset_groups=excluded_asset_groups_list,
        excluded_targets=excluded_targets_list,
        included_asset_groups=included_asset_groups_list,
        included_targets=included_targets_list,
        duration=duration,
        frequency=frequency_enum,
        interval=interval_int,
        date_of_month=date_of_month_int,
        scan_name=scan_name,
        scan_template_id=scan_template_id,
    )

    return CommandResults(
        readable_output=f"Scan schedule {schedule_id} has been updated.",
        raw_response=response_data,
    )


def update_vulnerability_exception_expiration_command(
    client: Client, vulnerability_exception_id: str, expiration: str
) -> CommandResults:
    """
    Update the expiration date of a vulnerability exception.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_exception_id (str): ID of the vulnerability exception to update.
        expiration (str): Expiration date to set for the vulnerability exception,
            formatted in ISO 8601 format.
    """
    response = client.update_vulnerability_exception_expiration(
        vulnerability_exception_id=vulnerability_exception_id,
        expiration_date=expiration,
    )

    return CommandResults(
        readable_output=f"Successfully updated expiration date of vulnerability exception {vulnerability_exception_id}.",
        raw_response=response,
    )


def update_vulnerability_exception_status_command(client: Client, vulnerability_exception_id: str, status: str) -> CommandResults:
    """
    Update the status of a vulnerability exception.

    Args:
        client (Client): Client to use for API requests.
        vulnerability_exception_id (str): ID of the vulnerability exception to update.
        status (str): Status to set for the vulnerability exception.
    """
    response = client.update_vulnerability_exception_status(
        vulnerability_exception_id=vulnerability_exception_id,
        status=status,
    )

    return CommandResults(
        readable_output=f"Successfully updated status of vulnerability exception {vulnerability_exception_id}.",
        raw_response=response,
    )


def create_tag_command(
    client: Client,
    name: str,
    type: str,
    color: str,
    ip_address_is: str | None = None,
    host_name_is: str | None = None,
    risk_score_higher_than: str | None = None,
    vulnerability_title_contains: str | None = None,
    site_id_in: str | None = None,
    site_name_in: str | None = None,
    query: str | None = None,
    match: str | None = None,
):
    """
    Create a tag.

    Args:
        client (Client): Client to use for API requests.
        name (str): The tag name.
        type (str): The tag type.
        color (str): The tag color - relevant only for "custom" type.
        ip_address_is (str, optional): A specific IP address to search for.
        host_name_is (str, optional): A specific host name to search for.
        risk_score_higher_than (str, optional): A minimum risk score to use as a filter.
        vulnerability_title_contains (str, optional): A string to search for in vulnerability titles.
        site_id_in (str, optional): Site IDs to filter for. Can be a comma-separated list.
        site_name_in (str, optional): Site names to filter for. Can be a comma-separated list.
        query (str, optional): Additional queries to use as a filter, in the format: {field} {operator} {value}.
                                Multiple queries can be specified, separated by a ";" separator.
        match (str, optional): Operator to determine how to match filters.
                                "All" requires all filters to match, "Any" requires only one filter to match.

    Returns:
        CommandResults: Results of the tag creation.
    """
    validate_input(type, VALID_TAG_TYPES, "type", True)
    validate_input(color, VALID_TAG_COLORS, "color", False)

    if type.lower() != "custom" and color.lower() != "default":
        raise DemistoException("color argument is only relevant for “custom” type.")

    filters_data = parse_asset_filters(
        client=client,
        ip_address_is=ip_address_is,
        host_name_is=host_name_is,
        risk_score_higher_than=risk_score_higher_than,
        site_id_in=site_id_in,
        site_name_in=site_name_in,
        vulnerability_title_contains=vulnerability_title_contains,
        query=query,
    )

    filters = convert_asset_search_filters(filters_data)
    res = client.create_tag(name=name, type=type, color=color, filters=filters, match=match)

    return CommandResults(
        outputs_prefix="Nexpose.Tag",
        outputs_key_field="id",
        outputs=res,
        readable_output=f"A new tag '{name}' created successfully with ID: {res['id']}",
        raw_response=res,
    )


def delete_tag_command(client: Client, id: str):
    """
    Delete a tag by ID.

    Args:
        client (Client): Client to use for API requests.
        id (str): The tag ID.

    Returns:
        CommandResults: Results of the tag deletion.
    """
    id_int = arg_to_number(id, arg_name="id", required=True)
    client.delete_tag(id_int)  # type: ignore[arg-type]
    return CommandResults(readable_output=f"Tag: {id_int} was deleted successfully")


def get_list_tag_command(
    client: Client,
    id: str | None = None,
    name: str | None = None,
    type: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    limit: str | None = None,
):
    """
    Get a list of tags or a tag by ID.

    Args:
        client (Client): Client to use for API requests.
        id (str, optional): Get tag by ID.
        name (str, optional): Filters the returned tags to only those containing the value within their name.
        type (str, optional): Filters the returned tags to only those of this type.
        page_size (str, optional): Number of records to retrieve in each API call when pagination is used.
        page (str, optional): A specific page to retrieve when pagination is used. Page indexing starts at 0.
        limit (str, optional): A number of records to limit the response to.

    Returns:
        CommandResults: Results of the tags retrieval.
    """
    validate_input(type, VALID_TAG_TYPES, "type", False)

    if id_int := arg_to_number(id, required=False):
        tags = client.get_tag_by_id(id=id_int)
    else:
        page_size_int = arg_to_number(page_size, required=False)
        page_int = arg_to_number(page, required=False)
        limit_int = arg_to_number(limit, required=False)
        tags = client.get_tags_list(name=name, type=type, page_size=page_size_int, page=page_int, limit=limit_int)

    headers = ["id", "color", "created", "name", "riskmodifier", "source", "type"]
    return CommandResults(
        outputs_prefix="Nexpose.Tag",
        outputs_key_field="id",
        outputs=tags,
        readable_output=tableToMarkdown(
            "Tags list",
            remove_dict_key(deepcopy(tags), "searchCriteria"),
            headers=headers,
            headerTransform=string_to_table_header,
        ),
        raw_response=tags,
    )


def update_tag_search_criteria_command(
    client: Client,
    tag_id: str,
    overwrite: str,
    ip_address_is: str | None = None,
    host_name_is: str | None = None,
    risk_score_higher_than: str | None = None,
    vulnerability_title_contains: str | None = None,
    site_id_in: str | None = None,
    site_name_in: str | None = None,
    query: str | None = None,
    match: str | None = None,
):
    """
    Update the search criteria of a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.
        overwrite (str): Whether to overwrite the original search values or append new conditions to the existing search.
        ip_address_is (str, optional): A specific IP address to search for.
        host_name_is (str, optional): A specific host name to search for.
        risk_score_higher_than (str, optional): A minimum risk score to use as a filter.
        vulnerability_title_contains (str, optional): A string to search for in vulnerability titles.
        site_id_in (str, optional): Site IDs to filter for. Can be a comma-separated list.
        site_name_in (str, optional): Site names to filter for. Can be a comma-separated list.
        query (str, optional): Additional queries to use as a filter, following the Search Criteria API standard.
        match (str, optional): Operator to determine how to match filters.
            "All" requires all filters to match, "Any" requires only one filter to match.

    Returns:
        CommandResults: Results of the search criteria update.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)

    filters_data = parse_asset_filters(
        client=client,
        ip_address_is=ip_address_is,
        host_name_is=host_name_is,
        risk_score_higher_than=risk_score_higher_than,
        site_name_in=site_name_in,
        site_id_in=site_id_in,
        vulnerability_title_contains=vulnerability_title_contains,
        query=query,
    )

    filters = convert_asset_search_filters(filters_data)

    if not argToBoolean(overwrite):
        tag_data = client.get_tag_by_id(tag_id_int)  # type: ignore[arg-type]
        old_filters = tag_data.get("searchCriteria", {}).get("filters", [])
        filters.extend(old_filters)

    client.update_tag_search_criteria(tag_id_int, filters, match)  # type: ignore[arg-type]
    return CommandResults(readable_output=f"Tag {tag_id_int} search criteria were updated successfully")


def get_list_tag_asset_group_command(client: Client, tag_id: str):
    """
    Get a list of asset groups for a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.

    Returns:
        CommandResults: Results of the asset groups retrieval.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)

    res = client.send_http_request("GET", f"/tags/{tag_id_int}/asset_groups")
    asset_groups_ids = res.get("resources", [])
    return CommandResults(
        outputs_prefix="Nexpose.TagAssetGroup",
        outputs=asset_groups_ids,
        readable_output=tableToMarkdown(f"Tag {tag_id_int} asset groups.", asset_groups_ids, headers=["Asset groups IDs"]),
        raw_response=res,
    )


def add_tag_asset_group_command(client: Client, tag_id: str, asset_group_ids: str):
    """
    Add existing asset groups to a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.
        asset_group_ids (str): The asset group IDs to add. Can be a comma-separated list.

    Returns:
        CommandResults: Results of the asset groups addition.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)
    asset_group_ids_list = argToList(asset_group_ids, transform=int)

    old_asset_groups = client.send_http_request("GET", f"/tags/{tag_id_int}/asset_groups")
    all_asset_group = list(set(old_asset_groups.get("resources", []) + asset_group_ids_list))

    client.send_http_request("PUT", f"/tags/{tag_id_int}/asset_groups", all_asset_group)
    return CommandResults(readable_output=f"Asset groups '{asset_group_ids}' were added successfully")


def remove_tag_asset_group_command(client: Client, tag_id: str, asset_group_id: str):
    """
    Remove an asset group from a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.
        asset_group_id (str): The asset group ID to remove.

    Returns:
        CommandResults: Results of the asset group removal.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)
    asset_group_id_int = arg_to_number(asset_group_id, arg_name="asset_group_id", required=True)

    client.send_http_request("DELETE", f"/tags/{tag_id_int}/asset_groups/{asset_group_id_int}")
    return CommandResults(readable_output=f"Asset group {asset_group_id_int} was removed from tag {tag_id_int} successfully")


def get_list_tag_asset_command(client: Client, tag_id: str):
    """
    Get a list of assets for a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.

    Returns:
        CommandResults: Results of the assets retrieval.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)

    res = client.send_http_request("GET", f"/tags/{tag_id_int}/assets")
    resources = res.get("resources", [])
    return CommandResults(
        outputs_prefix="Nexpose.TagAsset",
        outputs_key_field="id",
        outputs=resources,
        readable_output=tableToMarkdown(f"Tag {tag_id_int} assets", resources, headerTransform=string_to_table_header),
        raw_response=res,
    )


def add_tag_asset_command(client: Client, tag_id: str, asset_id: str):
    """
    Add an existing asset to a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.
        asset_id (str): The asset ID to add.

    Returns:
        CommandResults: Results of the asset addition.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)
    asset_id_int = arg_to_number(asset_id, arg_name="asset_id", required=True)

    client.send_http_request("PUT", f"/tags/{tag_id_int}/assets/{asset_id_int}")
    return CommandResults(readable_output=f"Asset {asset_id_int} was added in tag {tag_id_int} successfully")


def remove_tag_asset_command(client: Client, tag_id: str, asset_id: str):
    """
    Remove an asset from a tag.

    Args:
        client (Client): Client to use for API requests.
        tag_id (str): The tag ID.
        asset_id (str): The asset ID to remove.

    Returns:
        CommandResults: Results of the asset removal.
    """
    tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True)
    asset_id_int = arg_to_number(asset_id, arg_name="asset_id", required=True)

    client.send_http_request("DELETE", f"/tags/{tag_id_int}/assets/{asset_id_int}")
    return CommandResults(readable_output=f"Asset {asset_id_int} was removed from tag {tag_id_int} successfully")


def add_site_asset_command(
    client: Client, target_type: str, site_id: str, assets: str | None = None, asset_group_ids: str | None = None
):
    """
    Add assets or asset groups to a site's included/excluded assets.

    Args:
        client (Client): Client to use for API requests.
        target_type (str): Type of target, either "included" or "excluded".
        site_id (str): The site ID.
        assets (str, optional): The assets to add. Can be a comma-separated list.
        asset_group_ids (str, optional): The asset group IDs to add. Can be a comma-separated list.

    Returns:
        CommandResults: Results of the assets or asset groups addition/exclusion.
    """
    site_id_int = arg_to_number(site_id, arg_name="site_id", required=True)

    if assets_list := argToList(assets):
        client.send_http_request("POST", f"/sites/{site_id_int}/{target_type}_targets", assets_list)
        added_assets = f"assets {', '.join(assets_list)}"

    elif asset_group_ids_list := argToList(asset_group_ids, transform=int):
        client.send_http_request("PUT", f"/sites/{site_id_int}/{target_type}_asset_groups", asset_group_ids_list)
        added_assets = f"asset group IDs {asset_group_ids}"

    else:
        raise DemistoException("Must provide at least one Asset ID or Asset Group ID")

    return CommandResults(readable_output=f"Added assets- {added_assets} to site ID - {site_id_int}.")


def remove_site_asset_command(
    client: Client, target_type: str, site_id: str, assets: str | None = None, asset_group_ids: str | None = None
):
    """
    Remove assets or asset groups from a site's included/excluded assets.

    Args:
        client (Client): Client to use for API requests.
        target_type (str): Type of target, either "included" or "excluded".
        site_id (str): The site ID.
        assets (str, optional): The assets to remove. Can be a comma-separated list.
        asset_group_ids (str, optional): The asset group IDs to remove. Can be a comma-separated list.

    Returns:
        CommandResults: Results of the assets or asset groups removal.
    """
    site_id_int = arg_to_number(site_id, arg_name="site_id", required=True)

    if assets_list := argToList(assets):
        client.send_http_request("DELETE", f"/sites/{site_id_int}/{target_type}_targets", assets_list)
        removed_assets = f"assets {', '.join(assets_list)}"

    elif asset_group_ids_list := argToList(asset_group_ids, transform=int):
        client.send_http_request("DELETE", f"/sites/{site_id_int}/{target_type}_asset_groups", asset_group_ids_list)
        removed_assets = f"asset group IDs {asset_group_ids}"

    else:
        raise DemistoException("Must provide at least one assets or asset_group_ids")

    return CommandResults(readable_output=f"Removed assets-{removed_assets} from site ID {site_id_int}.")


def list_site_assets_command(client: Client, site_id: str, asset_type: str, target_type: str):
    """
    List included or excluded assets or asset groups for a site.

    Args:
        client (Client): Client to use for API requests.
        site_id (str): The site ID.
        asset_type (str): Type of asset, either "assets" or "asset_groups".
        target_type (str): Type of target, either "included" or "excluded".

    Returns:
        CommandResults: Results of the assets or asset groups retrieval.
    """
    site_id_int = arg_to_number(site_id, arg_name="site_id", required=True)

    if asset_type == "assets":
        res = client.send_http_request("GET", f"/sites/{site_id_int}/{target_type}_targets")
        output_prefix = f"Nexpose.{target_type.capitalize()}Asset"
        readable_title = f"{target_type.capitalize()} Asset list for site ID {site_id_int}"
    elif asset_type == "asset_groups":
        res = client.send_http_request("GET", f"/sites/{site_id_int}/{target_type}_asset_groups")
        output_prefix = f"Nexpose.{target_type.capitalize()}AssetGroup"
        readable_title = f"{target_type.capitalize()} Asset group list for site ID {site_id_int}"
    else:
        raise ValueError("Invalid asset_type. Expected 'assets' or 'asset_groups'.")

    outputs = dict(**res, site_id=site_id_int)
    readable_results = res.get("resources") if asset_type == "asset_groups" else res

    return CommandResults(
        outputs_prefix=output_prefix,
        outputs_key_field="id",
        outputs=outputs,
        readable_output=tableToMarkdown(
            readable_title, readable_results, headerTransform=string_to_table_header, removeNull=True
        ),
        raw_response=outputs,
    )


def create_asset_group_command(
    client: Client,
    name: str,
    description: str,
    type: str,
    match: str | None = None,
    ip_address_is: str | None = None,
    host_name_is: str | None = None,
    risk_score_higher_than: str | None = None,
    vulnerability_title_contains: str | None = None,
    site_id_in: str | None = None,
    site_name_in: str | None = None,
    query: str | None = None,
):
    """
    Creates a new asset group in Nexpose.

    Args:
        client (Client): Client to use for API requests.
        name (str): The name of the asset group.
        description (str): The description of the asset group.
        type (str): The type of the asset group, valid values: "dynamic" or "static".
        match (str, optional): The match criteria for the asset group.
        ip_address_is (str, optional): Filter by IP address.
        host_name_is (str, optional): Filter by host name.
        risk_score_higher_than (str, optional): Filter by risk score higher than the specified value.
        vulnerability_title_contains (str, optional): Filter by vulnerability title.
        site_id_in (str, optional): Filter by site ID.
        site_name_in (str, optional): Filter by site name.
        query (str, optional): Additional queries to use as a filter, in the format: {field} {operator} {value}.
                                Multiple queries can be specified, separated by a ";" separator.

    Returns:
        CommandResults: The results of the command execution.
    """
    filters_data = parse_asset_filters(
        client=client,
        ip_address_is=ip_address_is,
        host_name_is=host_name_is,
        risk_score_higher_than=risk_score_higher_than,
        site_id_in=site_id_in,
        site_name_in=site_name_in,
        vulnerability_title_contains=vulnerability_title_contains,
        query=query,
    )

    validate_input(type, VALID_ASSET_GROUP_TYPES, "type", False)

    if type == "Dynamic" and not filters_data:
        raise DemistoException("You must add filters to create a Dynamic asset group.")

    filters = convert_asset_search_filters(filters_data)

    res = client.create_asset_group(name=name, description=description, type=type, filters=filters, match=match)

    return CommandResults(
        outputs_prefix="Nexpose.AssetGroup",
        outputs_key_field="id",
        outputs=res,
        readable_output=f"A new asset group {name} created successfully with ID: {res['id']}",
        raw_response=res,
    )


def get_list_asset_group_command(
    client: Client,
    group_id: str | None = None,
    group_name: str | None = None,
    type: str | None = None,
    page_size: str | None = None,
    page: str | None = None,
    limit: str | None = None,
    sort: str | None = None,
):
    """
    Get a list of asset groups or a asset group by ID.

    Args:
        client (Client): Client to use for API requests.
        id (str, optional): Get asset group by ID.
        name (str, optional): Filters the returned asset groups to only those containing the value within their name.
        type (str, optional): Filters the returned asset groups to only those of this type.
        page_size (str, optional): Number of records to retrieve in each API call when pagination is used.
        page (str, optional): A specific page to retrieve when pagination is used. Page indexing starts at 0.
        limit (str, optional): A number of records to limit the response to.
        sort (str, optional): The sorting criteria for the results.

    Returns:
        CommandResults: Results of the asset groups retrieval.
    """
    validate_input(type, VALID_ASSET_GROUP_TYPES, "type", False)

    if id_int := arg_to_number(group_id, required=False):
        asset_groups = client.get_asset_group_by_id(id=id_int)
    else:
        page_size_int = arg_to_number(page_size, required=False)
        page_int = arg_to_number(page, required=False)
        limit_int = arg_to_number(limit, required=False)

        asset_groups = client.get_asset_groups(
            name=group_name, type=type, page_size=page_size_int, page=page_int, limit=limit_int, sort=sort
        )

    return CommandResults(
        outputs_prefix="Nexpose.AssetGroup",
        outputs_key_field="id",
        outputs=asset_groups,
        readable_output=tableToMarkdown(
            "Asset groups list", remove_dict_key(deepcopy(asset_groups), "searchCriteria"), headerTransform=string_to_table_header
        ),
        raw_response=asset_groups,
    )


##############################################################################
# ASYNC FUNCTIONS
##############################################################################


async def create_report_config_from_template(client: InsightVMClient, event_type: str) -> str:
    """
    Creates a new report configuration asynchronously.

    This function waits (blocks the calling workflow) until the API
    responds, ensuring the report is created before moving on.
    """
    endpoint = "/api/3/reports"

    # Define the payload
    payload = {
        "format": "sql-query",
        "name": f"PA_Asset_Collector_{int(time.time())}",
        "query": BASE_QUERY.get(event_type),
        "version": "2.3.0",
    }
    log(event_type, "Creating report config.")
    response = await client.http_request(method="POST", endpoint=endpoint, payload=payload)

    try:
        # The Location header contains the URL of the new report
        report_url = response.headers.get("Location")
        response.release()

        if not report_url:
            raise DemistoException(f"Failed to retrieve report URL from Location header. Status: {response.status}")

        report_id = report_url.split("/")[-1]

        return report_id

    except Exception as e:
        # Ensure connection is closed even if parsing fails
        response.release()
        raise DemistoException(f"Error creating report configuration: {e}")


async def generate_report(client: InsightVMClient, report_id: str, event_type: str) -> str:
    """Triggers the generation of a report."""
    endpoint = f"/api/3/reports/{report_id}/generate"

    # Use the client's http_request method for POST
    response = await client.http_request("POST", endpoint, payload={})

    # Handle response logic
    data = await response.json()
    await response.release()
    instance_id = str(data.get("id"))
    log(event_type, f"Report generation started. Instance ID: {instance_id}")
    return instance_id


async def check_status_of_report(client: InsightVMClient, report_id: str, instance_id: str, event_type: str) -> str:
    """Checks the status of a report instance with non-blocking waits."""
    while True:
        endpoint = f"/api/3/reports/{report_id}/history/{instance_id}"

        # Use the client's http_request method for GET
        response = await client.http_request("GET", endpoint)

        status_data = await response.json()
        await response.release()
        status = status_data.get("status", "unknown").lower()

        log(event_type, f"Current report status: '{status}' for {report_id=} and {instance_id=}")

        if status == "complete":
            return instance_id

        if status in ["failed", "aborted"]:
            log(event_type, f"Report {instance_id} status is '{status}'. Re-triggering report generation")
            instance_id = await generate_report(client, report_id, event_type)

        if status in ["generated", "running", "unknown"]:
            log(event_type, f"Report {instance_id} still processing. Waiting 60 seconds")

        await asyncio.sleep(REPORT_DOWNLOAD_WAIT_TIME)


async def stream_report(
    client: "InsightVMClient", report_id: str, instance_id: str, event_type: str
) -> AsyncGenerator[str, None]:  # noqa: E501
    """
    Asynchronously streams the content of a Rapid7 InsightVM report.
    Yields decoded text lines (strings) to be consumed by the parser.
    """
    endpoint = f"/api/3/reports/{report_id}/history/{instance_id}/output"
    log(event_type, f"Starting report download stream from: {endpoint}")

    # The report `/output` endpoint serves CSV, not JSON. The client's default
    # "Accept: application/json" header causes the server to respond with HTTP 406
    # (Not Acceptable), surfacing as "Failed to parse CSV header on line 1".
    # Explicitly request CSV (falling back to any type) for this download.
    response = await client.http_request("GET", endpoint, headers={"Accept": "text/csv, */*"})

    buffer = b""  # Buffer to hold partial lines across chunks
    total_lines = 0

    try:
        content_stream = response.content

        # Asynchronously iterate over chunks as they arrive
        async for chunk in content_stream.iter_any():
            buffer += chunk

            # Split the buffer into lines (including the delimiter in the result)
            lines = buffer.splitlines(keepends=True)

            # Update the buffer with the last (potentially incomplete) line
            # Check if the buffer ended with a newline before popping the last element
            if buffer.endswith(b"\n"):
                buffer = b""
            else:
                buffer = lines.pop()

            # Yield all complete lines found
            for line_bytes in lines:
                total_lines += 1
                # CRITICAL: Decode the byte string (bytes) into a text string (str).
                # The .strip() is REMOVED to preserve any trailing whitespace/newlines
                # for the CSV parser (though newlines are usually handled by splitlines).
                yield line_bytes.decode("utf-8")

        # Process any final content left in the buffer
        if buffer:
            total_lines += 1
            yield buffer.decode("utf-8")

        log(event_type, f"Finished streaming report. Received {total_lines} lines.")

    except (Exception, asyncio.CancelledError) as e:
        # Log how far the download got before failing so a stalled/cut-off stream is diagnosable.
        # asyncio.CancelledError (raised on timeout cancellation) derives from BaseException and has an
        # empty str(), so it must be caught explicitly and its type logged, or the failure vanishes.
        log(event_type, f"Report download stream stopped after {total_lines} lines with {type(e).__name__}: {e}")
        demisto.error(traceback.format_exc())
        raise
    finally:
        # Crucial: Always ensure the response object is released/closed
        await response.release()


async def stream_and_parse_report(
    client: "InsightVMClient",
    report_id: str,
    instance_id: str,
    event_integration_context: dict,
    event_type: str,
    batch_size: int = DEFAULT_BATCH_SIZE,
):
    # --- Checkpoint Setup ---
    # Retrieve checkpoints:
    last_sent_line_raw = event_integration_context.get("last_sent_line", 0)
    total_records_ingested = event_integration_context.get("total_records_ingested", 0)
    header: Optional[List[str]] = None
    # The start line for the *next* batch we will build
    # NOTE: raw line numbers are 1-based and inclusive.
    start_line_for_batch_raw = last_sent_line_raw + 1
    pending_tasks: set[asyncio.Task] = set()

    log(event_type, f"--- Starting Data Stream (Resuming from line: {start_line_for_batch_raw}) ---")
    # --- Lookahead Stream Setup ---
    try:
        stream_iterator = stream_report(client, report_id, instance_id, event_type)
    except Exception as e:
        raise DemistoException(f"Failed to initialize stream: {e}")

    line_to_process: Optional[str] = None

    log(event_type, f"--- Starting Data Stream for report instance {instance_id} ---")

    try:
        # INITIAL LINE READ: Get the header of the report
        try:
            # Fetch the very first line (Line 1)
            header_line = await anext(stream_iterator)

            # Parse the header immediately
            header = next(csv.reader(io.StringIO(header_line)))
            log(event_type, f"CSV Header parsed: {header}")

        except StopAsyncIteration:
            log(event_type, "Report was empty (no header found).")
            return
        except Exception as e:
            raise DemistoException(f"Failed to parse CSV header on line 1: {e}")

        try:
            # Try to fetch Line 2 (the first data line)
            line_to_process = await anext(stream_iterator)
        except StopAsyncIteration:
            # Report has a header but no data
            line_to_process = None

        # --- EXECUTION PHASE ---
        if line_to_process:
            await process_data_stream(
                stream_iterator=stream_iterator,
                line_to_process=line_to_process,
                header=header,
                last_sent_line_raw=last_sent_line_raw,
                total_records_ingested=total_records_ingested,
                batch_size=batch_size,
                event_type=event_type,
                pending_tasks=pending_tasks,
                event_integration_context=event_integration_context,
                instance_id=instance_id,
            )
        else:
            log(event_type, "No data rows to process.")

    except (Exception, asyncio.CancelledError) as e:
        error_message = f"Error during streaming or sending events ({type(e).__name__}): {e}"
        log(event_type, error_message)
        demisto.error(traceback.format_exc())
        raise DemistoException(error_message) from e


async def process_data_stream(
    stream_iterator: Any,
    line_to_process: Optional[str],
    header: List[str],
    last_sent_line_raw: int,
    total_records_ingested: int,
    batch_size: int,
    event_type: str,
    pending_tasks: set,
    event_integration_context: dict,
    instance_id: str,
) -> None:
    """
    Consumes the stream iterator, processes data rows, and triggers batch submission.
    """

    # Initialize Loop State
    # We are starting at Line 1 because the header (Line 1) was already processed outside.
    current_line_count = 1
    batch_counter = 0
    current_batch: List[str] = []
    current_run_data_records = 0
    snapshot_id = event_integration_context.get("snapshot_id", "")

    if not snapshot_id:
        snapshot_id = str(round(time.time() * 1000))
        log(event_type, f"Did not get snapshot_id from context, will use {snapshot_id=}")
        update_integration_context_by_event_type(event_type, {"snapshot_id": snapshot_id})

    # The start line for the *next* batch we will build
    # NOTE: raw line numbers are 1-based and inclusive.
    start_line_for_batch_raw = last_sent_line_raw + 1

    # --- MAIN LOOP ---
    while line_to_process is not None:
        current_line_count += 1
        current_line = line_to_process

        # --- Try to read the NEXT line (Lookahead) ---
        next_line = await _get_next_line(stream_iterator)

        # 1. Skip Checkpoint
        if _should_skip_line(current_line_count, last_sent_line_raw):
            line_to_process = next_line
            continue

        # 2. Parse Data Row and Append to Batch
        data_row = _parse_csv_row(current_line, header, current_line_count, event_type)

        if data_row:
            current_batch.append(data_row)
            current_run_data_records += 1  # Count of data records in the current run

        else:
            # if the line was malformed, we continue to the next line
            line_to_process = next_line
            continue

        # 3. BATCHING AND LOOKAHEAD CHECK
        is_last_batch = next_line is None

        # Send batch if it's full OR if the stream has ended
        if _is_last_batch(len(current_batch), batch_size, is_last_batch):
            batch_counter += 1

            start_line_for_batch_raw, total_records_ingested = await handle_batch_submission(
                current_batch=current_batch,
                start_line_for_batch_raw=start_line_for_batch_raw,
                total_records_ingested=total_records_ingested,
                current_run_data_records=current_run_data_records,
                is_last_batch=is_last_batch,
                event_type=event_type,
                pending_tasks=pending_tasks,
                snapshot_id=snapshot_id,
                batch_counter=batch_counter,
                instance_id=instance_id,
            )

            current_run_data_records = 0  # Reset count for the new batch
            current_batch = []

            log(event_type, f"Batch task created. Continuing stream. (Current line: {current_line_count})")

            # Backpressure: don't let the producer race arbitrarily far ahead of the senders (XSUP-69895).
            # Once too many batch tasks are in flight, wait for some to complete before reading more of the
            # stream. This bounds memory and in-flight connections, and surfaces a failed send promptly.
            while len(pending_tasks) >= MAX_IN_FLIGHT_BATCH_TASKS:
                done, _ = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED)
                for finished in done:
                    # Re-raise the first task exception so the workflow aborts instead of continuing to
                    # stream a report whose events are no longer being ingested.
                    if exc := finished.exception():
                        raise exc

        # Move processing to the next line:
        line_to_process = next_line

    if pending_tasks:
        log(event_type, f"Waiting for {len(pending_tasks)} final submission task(s) to finish.")
        results = await asyncio.gather(*pending_tasks, return_exceptions=True)

        for res in results:
            if isinstance(res, Exception):
                raise res


def _should_skip_line(current_line_count: int, last_sent_line_raw: int) -> bool:
    """
    Determines if the current line was already processed in a previous run.
    """
    return current_line_count <= last_sent_line_raw


def _is_last_batch(len_current_batch: int, batch_size: int, is_last_batch: bool):
    """
    Determines if this is the last batch of events to send.
    """
    return len_current_batch >= batch_size or is_last_batch


async def _get_next_line(stream_iterator: Any) -> Optional[str]:
    """
    Safely retrieves the next line from the iterator.
    Returns None if the stream has ended.
    """
    try:
        return await anext(stream_iterator)
    except StopAsyncIteration:
        return None


def _parse_csv_row(current_line: str, header: List[str], current_line_count: int, event_type: str) -> Optional[str]:
    """
    Parses a CSV string into a JSON string.
    Returns None if the row is malformed or invalid.
    """
    try:
        # Parse the CSV line
        data_row = next(csv.reader(io.StringIO(current_line)))

        # Validate column count matches header
        if not header or len(data_row) != len(header):
            # Only log if it's not just an empty trailing line
            if "".join(data_row).strip():
                log(
                    event_type,
                    f"Skipping malformed row {current_line_count}. " f"Got {len(data_row)} cols, expected {len(header)}.",
                )
            return None

        # Success: Zip headers with data and dump to JSON
        record_dict = dict(zip(header, data_row))
        return json.dumps(record_dict)

    except Exception as e:
        log(event_type, f"Error parsing CSV line {current_line_count}: {e}. Skipping record.")
        return None


async def handle_batch_submission(
    current_batch: List[str],
    start_line_for_batch_raw: int,
    total_records_ingested: int,
    current_run_data_records: int,
    is_last_batch: bool,
    event_type: str,
    pending_tasks: set,
    snapshot_id: str,
    batch_counter: int,
    instance_id: str,
) -> tuple[int, int]:
    """
    Calculates batch metrics, determines the finality indicator, spawns the
    background send_events task, and returns the updated state for the next batch.

    Returns:
        tuple[int, int]: (next_start_line_raw, new_total_records_ingested)
    """

    batch_size_actual = len(current_batch)
    # NOTE: raw line numbers are 1-based and inclusive.
    # Example: if start=500 and batch size=100 → end=599 → next start=600.
    total_lines_after_batch_raw = start_line_for_batch_raw + batch_size_actual - 1

    # Calculate the NEW cumulative total records ingested after this batch
    new_total_records = total_records_ingested + current_run_data_records

    # --- FINAL COUNT LOGIC ---
    if is_last_batch:
        # If this is the last batch, the indicator MUST be the final total record count.
        lines_to_send_indicator = new_total_records
        log(
            event_type,
            f"DEBUG: End of report number {instance_id} reached. Sending Final Report Total: \
        {lines_to_send_indicator}",
        )
    else:
        # Otherwise, send the placeholder (1) to indicate it's a middle batch.
        lines_to_send_indicator = 1

    # --- END FINAL COUNT LOGIC ---
    # Create the asynchronous task for concurrent submission

    try:
        log(event_type, f"Sending {batch_size_actual} {event_type} to xsiam")
        task = asyncio.create_task(
            process_and_send_events_to_xsiam(
                current_batch,
                total_lines_after_batch_raw,  # Raw line number for resuming stream (e.g., 1001)
                batch_counter,
                new_total_records,  # Cumulative data records processed (e.g., 1000)
                event_type,
                snapshot_id,
                lines_to_send_indicator,
            )
        )

        # Manage the task set
        pending_tasks.add(task)
        task.add_done_callback(pending_tasks.discard)
    except Exception as e:
        log(event_type, f"CRITICAL ERROR: Failed to schedule batch submission task: {e}")
        if "task" in locals() and not task.done():
            task.cancel()
            log(event_type, "Safe guard: Orphaned task cancelled.")

    # Reset batch trackers for the next iteration
    start_line_for_batch_raw = total_lines_after_batch_raw + 1

    return start_line_for_batch_raw, new_total_records


async def delete_report_instance(client: InsightVMClient, report_id: str, instance_id: str, event_type: str):
    """Deletes a specific report instance (a single run)."""
    endpoint = f"/api/3/reports/{report_id}/history/{instance_id}"
    demisto.debug(f"Deleting report instance ID: {instance_id} for {event_type}.")
    try:
        await client.http_request("DELETE", endpoint)
        demisto.debug(f"Report instance {instance_id} for {event_type} deleted successfully.")
    except Exception as e:
        # Expected if resource is already gone (e.g., Status 404)
        demisto.debug(f"Warning: Could not guarantee deletion of report {instance_id} instance. {e}")


async def delete_report_configuration(client: InsightVMClient, report_id: str, event_type: str):
    """Deletes the entire report configuration."""
    endpoint = f"/api/3/reports/{report_id}"
    demisto.debug(f"Deleting report configuration ID: {report_id} for {event_type}.")
    try:
        await client.http_request("DELETE", endpoint)
        demisto.debug(f"Report {report_id} for {event_type} configuration deleted successfully.")
    except Exception as e:
        # Expected if resource is already gone (e.g., Status 404)
        demisto.debug(f"Warning: Could not guarantee deletion of report configuration. {e}")


def _apply_collector_changes(collector_context: Dict[str, Dict[str, Any]], collector_type: str, changes: Dict[str, Any]) -> None:
    """
    Helper function to apply changes to a specific collector's context dict
    with concurrency safety logic.

    Modifies 'collector_context' in place.
    """
    # 1. Ensure the sub-dictionary exists for this collector type
    if collector_type not in collector_context:
        collector_context[collector_type] = {}

    current_sub_dict = collector_context[collector_type]

    # Keys that must strictly increase during normal operation
    MONITORED_KEYS = {"last_sent_line", "total_records_ingested"}

    # 2. Determine 'finish_current_report_stream' state for logic decisions
    # Priority: Argument in 'changes' > Persisted value in Context > Default False
    if "finish_current_report_stream" in changes:
        finish_current_report_stream = changes["finish_current_report_stream"]
    else:
        finish_current_report_stream = current_sub_dict.get("finish_current_report_stream", False)

    # 3. Iterate through ALL keys in the changes dictionary
    for key, new_value in changes.items():
        # LOGIC A: Handle Monitored Counters (prevent race conditions)
        if key in MONITORED_KEYS and not finish_current_report_stream:
            old_value = current_sub_dict.get(key, 0)

            if isinstance(new_value, int | float) and isinstance(old_value, int | float):
                if new_value > old_value:
                    current_sub_dict[key] = new_value
            else:
                # Fallback for unexpected types
                current_sub_dict[key] = new_value

        # LOGIC B: Handle Everything Else
        # This includes IDs, standard keys, AND the 'finish_current_report_stream' key itself.
        else:
            current_sub_dict[key] = new_value


def update_integration_context_by_event_type(event_type: str, changes: Dict[str, Any]) -> None:
    """
    Retrieves the current integration context, updates the specified event's
    state with the provided changes, and saves the updated context.

    Args:
        collector_type: The top-level key to update ('asset' or 'vulnerability').
        changes: A dictionary of {key: value} updates to apply.
    """

    # 1. Get the current context
    current_context = get_integration_context()

    # 2. Apply the update using the helper function
    _apply_collector_changes(current_context, event_type, changes)

    # 3. Set the updated context back to the platform
    set_integration_context(current_context)
    log(event_type, f"State checkpoint saved. New line: {current_context[event_type]}")  # noqa: E501


#################################################
# copy from akamai
#################################################


async def process_and_send_events_to_xsiam(
    events: list[str],
    total_lines_after_batch_raw: int | None,
    batch_counter: int,
    new_total_records: int,
    event_type: str,
    snapshot_id: str,
    items_count: int = 1,
):
    """Send the events to CSP's send_events_to_xsiam.
    save the offset after the execution is done and update the module health.

    Args:
        events (list[str]): The list of json serialized events.
        should_skip_decode_events (bool): Whether we should skip serializing and decoding events or not.
        offset (str | None): The offset hash.
        counter (int): The current execution number.
    """
    log(event_type, f"Running in interval = {batch_counter}. got {len(events)} lines, moving to processing data.")  # noqa: E501
    processed_events = events

    size_in_bytes = sum(len(line.encode("utf-8")) for line in events)

    tasks = send_events_to_xsiam_rapid7(
        processed_events,
        VENDOR.get(event_type),
        PRODUCT.get(event_type),
        should_update_health_module=False,
        chunk_size=size_in_bytes,
        send_events_asynchronously=True,
        url_key="host",
        data_format="json",
        data_size_expected_to_split_evenly=True,
        counter=batch_counter,
        items_count=items_count,
        snapshot_id=snapshot_id,
    )
    log(event_type, f"Running in interval = {batch_counter}. Finished executing send_events_to_xsiam, waiting for tasks to end.")
    await asyncio.gather(*tasks)
    log(event_type, f"Running in interval = {batch_counter}. Finished gathering all tasks.")
    log(event_type, f"Running in interval = {batch_counter}. Updating module health.")

    update_integration_context_by_event_type(
        event_type,
        {"last_sent_line": total_lines_after_batch_raw, "total_records_ingested": new_total_records},
    )
    demisto.updateModuleHealth({"assetsPulled": len(processed_events)})
    log(event_type, f"Running in interval = {batch_counter}. Finished updating module health.")


def rapid7_send_data_to_xsiam(
    data,
    vendor,
    product,
    data_format=None,
    url_key="url",
    num_of_attempts=3,
    chunk_size=XSIAM_EVENT_CHUNK_SIZE,
    data_type=ASSETS,
    should_update_health_module=True,
    add_proxy_to_request=False,
    snapshot_id="",
    items_count=None,
    send_events_asynchronously=False,
    data_size_expected_to_split_evenly=False,
    counter=0,
):  # pragma: no cover
    """
    Send the supported fetched data types into the XDR data-collector private api.

    :type data: ``Union[str, list]``
    :param data: The data to send to XSIAM server. Should be of the following:
        1. List of strings or dicts where each string or dict represents an event or asset.
        2. String containing raw events separated by a new line.

    :type vendor: ``str``
    :param vendor: The vendor corresponding to the integration that originated the data.

    :type product: ``str``
    :param product: The product corresponding to the integration that originated the data.

    :type data_format: ``str``
    :param data_format: Should only be filled in case the 'events' parameter contains a string of raw
        events in the format of 'leef' or 'cef'. In other cases the data_format will be set automatically.

    :type url_key: ``str``
    :param url_key: The param dict key where the integration url is located at. the default is 'url'.

    :type num_of_attempts: ``int``
    :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes)

    :type chunk_size: ``int``
    :param chunk_size: Advanced - The maximal size of each chunk size we send to API. Limit of 9 MB will be inforced.

    :type data_type: ``str``
    :param data_type: Type of data to send to Xsiam, events or assets.

    :type should_update_health_module: ``bool``
    :param should_update_health_module: whether to trigger the health module showing how many events were sent to xsiam
        This can be useful when using send_data_to_xsiam in batches for the same fetch.

    :type add_proxy_to_request: ``bool``
    :param add_proxy_to_request: whether to add proxy to the send evnets request.

    :type snapshot_id: ``str``
    :param snapshot_id: the snapshot id.

    :type items_count: ``str``
    :param items_count: the asset snapshot items count.

    :type send_events_asynchronously: ``bool``
    :param send_events_asynchronously: whether to use asyncio to send the events to xsiam asynchronously or not.
    Note that when set to True, the debug should be done from the integration itself.

    :type data_size_expected_to_split_evenly: ``bool``
    :param data_size_expected_to_split_evenly: whether the events should be about the same size or not.
    Use this to split data to chunks faster.

    :return: Either None if running regularly or a list of asyncio task objects if running asynchronously:.
    In case of running asynchronously:, the list of tasks will hold the number of events sent and can be accessed by:
    await asyncio.gather(*tasks)
    :rtype: ``List[Task]`` or ``None``
    """
    data_size = 0
    params = demisto.params()
    url = params.get(url_key)
    calling_context = demisto.callingContext.get("context", {})
    instance_name = calling_context.get("IntegrationInstance", "")
    collector_name = calling_context.get("IntegrationBrand", "")
    if not items_count:
        items_count = len(data) if isinstance(data, list) else 1
    if data_type not in DATA_TYPES:
        demisto.debug(f"data type must be one of these values: {DATA_TYPES}")
        return None

    if not data:
        demisto.debug(
            f"send_data_to_xsiam function received no {data_type}, skipping the API call to send {data_type} (data_type) to XSIAM"
        )  # noqa: E501
        demisto.updateModuleHealth({f"{data_type}Pulled": data_size})
        return None

    # only in case we have data to send to XSIAM we continue with this flow.
    # Correspond to case 1: List of strings or dicts where each string or dict represents an one event or asset or snapshot.
    if isinstance(data, list):
        # In case we have list of dicts we set the data_format to json and parse each dict to a stringify each dict.
        demisto.debug(f"Sending {len(data)} {data_type} (data type) to XSIAM")
        if isinstance(data[0], dict):
            data = [json.dumps(item) for item in data]
            data_format = "json"
        # Separating each event with a new line
        data = "\n".join(data)
    elif not isinstance(data, str):
        raise DemistoException(f"Unsupported type: {type(data)} for the {data_type} parameter. Should be a string or list.")
    if not data_format:
        data_format = "text"

    xsiam_api_token = demisto.getLicenseCustomField("Http_Connector.token")
    xsiam_domain = demisto.getLicenseCustomField("Http_Connector.url")
    xsiam_url = f"https://api-{xsiam_domain}"
    headers = remove_empty_elements(
        {
            "authorization": xsiam_api_token,
            "format": data_format,
            "product": product,
            "vendor": vendor,
            "content-encoding": "gzip",
            "collector-name": collector_name,
            "instance-name": instance_name,
            "final-reporting-device": url,
            "collector-type": ASSETS if data_type == ASSETS else EVENTS,
        }
    )
    if data_type == ASSETS:
        if not snapshot_id:
            snapshot_id = str(round(time.time() * 1000))
            log(data_type, f"Did not get snapshot_id, will send {snapshot_id=}")

        # We are setting a time stamp ahead of the instance name since snapshot-ids must be configured in ascending
        # alphabetical order such that first_snapshot < second_snapshot etc.
        headers["snapshot-id"] = snapshot_id + instance_name
        headers["total-items-count"] = str(items_count)

    header_msg = f"Error sending new {data_type} into XSIAM.\n"

    def data_error_handler(res):
        """
        Internal function to parse the XSIAM API errors
        """
        try:
            response = res.json()
            error = res.reason
            if response.get("error").lower() == "false":
                xsiam_server_err_msg = response.get("error")
                error += ": " + xsiam_server_err_msg

        except ValueError:
            if res.text:
                error = f"\n{res.text}"
            else:
                error = "Received empty response from the server"

        api_call_info = (
            "Parameters used:\n"
            f"\tURL: {xsiam_url}\n"
            f"\tHeaders: {json.dumps(headers, indent=8)}\n\n"
            f"Response status code: {res.status_code}\n"
            f"Error received:\n\t{error}"
        )

        demisto.error(header_msg + api_call_info)
        raise DemistoException(header_msg + error, DemistoException)

    client = BaseClient(base_url=xsiam_url, proxy=add_proxy_to_request)
    if data_size_expected_to_split_evenly:
        data_chunks = split_data_by_slices(data, chunk_size)
    else:
        data_chunks = split_data_to_chunks(data, chunk_size)

    def send_events(data_chunk):
        chunk_size = len(data_chunk)
        data_chunk = "\n".join(data_chunk)
        zipped_data = gzip.compress(data_chunk.encode("utf-8"))  # type: ignore[AttributeError,attr-defined]
        xsiam_api_call_with_retries(
            client=client,
            events_error_handler=data_error_handler,
            error_msg=header_msg,
            headers=headers,
            num_of_attempts=num_of_attempts,
            xsiam_url=xsiam_url,
            zipped_data=zipped_data,
            is_json_response=True,
            data_type=data_type,
        )
        return chunk_size

    async def send_events_async(data_chunk):
        chunk_size = len(data_chunk)
        data_chunk = "\n".join(data_chunk)
        zipped_data = gzip.compress(data_chunk.encode("utf-8"))  # type: ignore[AttributeError,attr-defined]
        _ = await xsiam_api_call_async_with_retries(
            headers=headers, num_of_attempts=num_of_attempts, xsiam_url=xsiam_url, zipped_data=zipped_data, data_type=data_type
        )
        return chunk_size

    if send_events_asynchronously:
        demisto.info(f"Running in interval = {counter}. Sending events to xsiam asynchronously.")
        all_chunks = list(data_chunks)
        demisto.info(f"Running in interval = {counter}. Finished appending all data_chunks to a list.")
        tasks = [asyncio.create_task(send_events_async(chunk)) for chunk in all_chunks]

        demisto.info(f"Finished submiting {len(tasks)} tasks for the {counter} time")
        return tasks
    else:
        demisto.info("Sending events to xsiam synchronously.")
        for chunk in data_chunks:
            data_size += send_events(chunk)

        if should_update_health_module:
            demisto.updateModuleHealth({f"{data_type}Pulled": data_size})
    return None


def split_data_by_slices(data, target_chunk_size):  # pragma: no cover
    """
    Splits a string/list of data into chunks of an approximately specified size.
    The actual size can be lower. the slicing is based on the assumption that all entries have the same size.

    :type data: ``list`` or a ``string``
    :param data: A list of data or a string delimited with \n  to split to chunks.
    :type target_chunk_size: ``int``
    :param target_chunk_size: The maximum size of each chunk. The maximal size allowed is 9MB.

    :return: An iterable of lists where each list contains events with approx size of chunk size.
    :rtype: ``collections.Iterable[list]``
    """
    target_chunk_size = min(target_chunk_size, XSIAM_EVENT_CHUNK_SIZE_LIMIT)
    if isinstance(data, str):
        data = data.split("\n")

    # Handle empty list case immediately
    if not data:
        return []

    entry_size = sys.getsizeof(data[0])
    num_of_entries_per_chunk = target_chunk_size // entry_size
    step_size = max(1, num_of_entries_per_chunk)

    for i in range(0, len(data), step_size):
        chunk = data[i : i + step_size]
        yield chunk


# Lazily-initialized concurrency guard for send-to-XSIAM requests. It is created on first use inside the
# running event loop (rather than at import time) so it binds to the correct loop across asyncio.run calls.
_XSIAM_SEND_SEMAPHORE: "asyncio.Semaphore | None" = None


def get_xsiam_send_semaphore() -> "asyncio.Semaphore":
    """Return a process-wide semaphore that caps concurrent send-to-XSIAM requests (XSUP-69895)."""
    global _XSIAM_SEND_SEMAPHORE
    if _XSIAM_SEND_SEMAPHORE is None:
        _XSIAM_SEND_SEMAPHORE = asyncio.Semaphore(MAX_CONCURRENT_XSIAM_SENDS)
    return _XSIAM_SEND_SEMAPHORE


async def xsiam_api_call_async_with_retries(
    xsiam_url,
    zipped_data,
    headers,
    num_of_attempts,
    data_type=ASSETS,
):  # pragma: no cover
    """
    Send the fetched events or assets into the XDR data-collector private api.

    Concurrency is bounded by a shared semaphore (MAX_CONCURRENT_XSIAM_SENDS) so that, no matter how many
    batch tasks the stream loop spawns, only a limited number of HTTP requests are in flight at once. This
    prevents overloading the ingestion endpoint, which previously caused truncated responses
    (aiohttp.TransferEncodingError) and connection timeouts (XSUP-69895).

    Retries cover transient transport failures (connection errors/timeouts, truncated payloads,
    asyncio timeouts) and retryable status codes (429 + 5xx) using exponential backoff. Non-retryable
    HTTP errors (e.g. 4xx other than 429) fail immediately.

    :type xsiam_url: ``str``
    :param xsiam_url: The URL of XSIAM to send the api request.
    :type zipped_data: ``bytes``
    :param zipped_data: encoded events
    :type headers: ``dict``
    :param headers: headers for the request
    :type num_of_attempts: ``int``
    :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes).
    :type data_type: ``str``
    :param data_type: events or assets
    :return: Response object or DemistoException
    :rtype: ``requests.Response`` or ``DemistoException``
    """
    status_code = None
    attempt_num = 1
    response = None
    header_msg = f"Error sending new {data_type} into XSIAM.\n"
    timeout = aiohttp.ClientTimeout(total=XSIAM_SEND_TIMEOUT_SECONDS)
    # Bound the number of concurrent in-flight send requests across the whole run.
    async with get_xsiam_send_semaphore():
        while status_code != 200 and attempt_num < num_of_attempts + 1:
            demisto.debug(f"Sending {data_type} into xsiam, attempt number {attempt_num}")
            is_last_attempt = attempt_num >= num_of_attempts
            # On the last attempt we want any error to propagate (raise) instead of being retried.
            ok_codes = (200, 429) if not is_last_attempt else None
            try:
                async with aiohttp.ClientSession(timeout=timeout) as session:  # noqa: SIM117
                    async with session.post(urljoin(xsiam_url, "/logs/v1/xsiam"), data=zipped_data, headers=headers) as response:
                        try:
                            response.raise_for_status()  # This raises an exception for non-2xx status codes
                            status_code = response.status
                        except aiohttp.ClientResponseError as e:
                            # 429 (and, when not the last attempt, 5xx) are retryable; everything else is fatal.
                            is_retryable_status = e.status == 429 or 500 <= e.status < 600
                            if ok_codes and is_retryable_status:
                                demisto.debug(f"Got retryable status code {e.status} on attempt {attempt_num}, will retry.")
                                status_code = e.status
                            else:
                                demisto.debug(
                                    f"XSIAM API returned a non-retryable error. Status: {e.status}, "
                                    f"Message: {e.message}, Request URL: {e.request_info}"
                                )
                                serializable_headers = dict(e.headers) if e.headers else {}
                                api_call_info = (
                                    "Parameters used:\n"
                                    f"\tURL: {xsiam_url}\n"
                                    f"\tHeaders: {json.dumps(serializable_headers, indent=8)}\n\n"
                                    f"Response status code: {e.status}\n"
                                    f"Error received:\n\t{e.message}\n"
                                    f"additional request info: \n\t{e.request_info}"
                                )
                                demisto.error(header_msg + api_call_info)
                                demisto.updateModuleHealth(header_msg + e.message, is_error=True)
                                raise
            except (
                aiohttp.ClientConnectionError,
                aiohttp.ClientPayloadError,
                asyncio.TimeoutError,
            ) as e:
                # Transient transport-level failures: truncated responses (TransferEncodingError is a
                # ClientPayloadError), connection resets/timeouts, and request timeouts. These previously
                # bypassed the retry loop entirely and aborted the whole fetch (XSUP-69895).
                if is_last_attempt:
                    demisto.error(f"{header_msg}Transport error after {attempt_num} attempts: {e!r}")
                    demisto.updateModuleHealth(f"{header_msg}{e!r}", is_error=True)
                    raise
                demisto.debug(f"Got retryable transport error on attempt {attempt_num}: {e!r}. Will retry.")
                status_code = None

            demisto.debug(f"received status code: {status_code}")
            if status_code != 200:
                # Exponential backoff (1s, 2s, 4s, ... capped at 30s) to let the endpoint recover.
                await asyncio.sleep(min(2 ** (attempt_num - 1), 30))
            attempt_num += 1
    return response


def send_events_to_xsiam_rapid7(
    events,
    vendor,
    product,
    data_format=None,
    url_key="url",
    num_of_attempts=3,
    chunk_size=XSIAM_EVENT_CHUNK_SIZE,
    should_update_health_module=True,
    add_proxy_to_request=False,
    send_events_asynchronously=True,
    data_size_expected_to_split_evenly=False,
    counter=0,
    items_count=1,
    snapshot_id="",
):  # pragma: no cover
    """
    Send the fetched events into the XDR data-collector private api.

    :type events: ``Union[str, list]``
    :param events: The events to send to XSIAM server. Should be of the following:
        1. List of strings or dicts where each string or dict represents an event.
        2. String containing raw events separated by a new line.

    :type vendor: ``str``
    :param vendor: The vendor corresponding to the integration that originated the events.

    :type product: ``str``
    :param product: The product corresponding to the integration that originated the events.

    :type data_format: ``str``
    :param data_format: Should only be filled in case the 'events' parameter contains a string of raw
        events in the format of 'leef' or 'cef'. In other cases the data_format will be set automatically.

    :type url_key: ``str``
    :param url_key: The param dict key where the integration url is located at. the default is 'url'.

    :type num_of_attempts: ``int``
    :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes)

    :type chunk_size: ``int``
    :param chunk_size: Advanced - The maximal size of each chunk size we send to API. Limit of 9 MB will be inforced.

    :type should_update_health_module: ``bool``
    :param should_update_health_module: whether to trigger the health module showing how many events were sent to xsiam

    :type add_proxy_to_request :``bool``
    :param add_proxy_to_request: whether to add proxy to the send evnets request.

    :type send_events_asynchronously: ``bool``
    :param send_events_asynchronously: whether to use asyncio to send the events to xsiam asynchronously or not.
    Note that when set to True, the debug should be done from the integration itself.

    :type data_size_expected_to_split_evenly: ``bool``
    :param data_size_expected_to_split_evenly: whether the events should be about the same size or not.
    Use this to split data to chunks faster.

    :return: Either None if running regularly or a list of asyncio task objects if running asynchronously:.
    In case of running asynchronously:, the list of tasks will hold the number of events sent and can be accessed by:
    await asyncio.gather(*tasks)
    :rtype: ``List[Task]`` or ``None``
    """
    return rapid7_send_data_to_xsiam(
        events,
        vendor,
        product,
        data_format,
        url_key,
        num_of_attempts,
        chunk_size,
        data_type="assets",
        should_update_health_module=should_update_health_module,
        add_proxy_to_request=add_proxy_to_request,
        send_events_asynchronously=send_events_asynchronously,
        data_size_expected_to_split_evenly=data_size_expected_to_split_evenly,
        counter=counter,
        items_count=items_count,
        snapshot_id=snapshot_id,
    )


async def run_full_collector_workflow(client: InsightVMClient, event_type: str, batch_size: int = DEFAULT_BATCH_SIZE):
    """
    Orchestrates the full report lifecycle: State Check, Create/Generate, Stream, Cleanup.

    The function signature is adapted to receive the client instance.
    """
    integration_context = get_integration_context()
    log(event_type, f"Got integration_context: {integration_context}")

    # 1. RETRIEVE STATE
    event_integration_context = integration_context.get(event_type, {})
    report_id = event_integration_context.get("report_id", "")
    instance_id = event_integration_context.get("instance_id", "")
    finish_current_report_stream = event_integration_context.get(
        "finish_current_report_stream", False
    )  # if we finished the current report, but the server crashed before removing the instance id and report id from the context
    if not finish_current_report_stream:
        try:
            if report_id and instance_id:
                log(event_type, f"State found in context. Resuming check for Report ID: {report_id}, Instance ID: {instance_id}")
                # If IDs exist, go straight to checking the status of the ongoing report
                instance_id = await check_status_of_report(client, report_id, instance_id, event_type)

            else:
                log(event_type, "No state found. Starting new report generation.")
                if not report_id:
                    raise DemistoException(f"Report ID for {event_type} is missing from context despite setup phase.")

                # 2. GENERATE REPORT
                instance_id = await generate_report(client, report_id, event_type)
                update_integration_context_by_event_type(event_type, {"instance_id": instance_id})

                # 3. CHECK STATUS (Will wait asynchronously until finished or retry if failed)
                instance_id = await check_status_of_report(client, report_id, instance_id, event_type)
                update_integration_context_by_event_type(event_type, {"instance_id": instance_id})

            # 4. STREAM DATA AND SEND EVENTS
            log(event_type, "\n--- Starting Data Streaming Phase ---")

            await stream_and_parse_report(client, report_id, instance_id, event_integration_context, event_type, batch_size)
            update_integration_context_by_event_type(
                event_type,
                {"last_sent_line": 0, "finish_current_report_stream": True, "snapshot_id": "", "total_records_ingested": 0},
            )
            log(event_type, "--- Data Streaming Phase Complete ---")

        except Exception as e:
            log(event_type, f"\nFATAL WORKFLOW ERROR: {e}")
            # Preserve the original exception message to ensure test expectations are met
            raise DemistoException(f"Got the following error: {str(e)}")

    # 5. CLEANUP:
    log(event_type, "\n--- Starting Cleanup Phase ---")

    if report_id and instance_id:
        await delete_report_instance(client, report_id, instance_id, event_type)
    if report_id:
        await delete_report_configuration(client, report_id, event_type)

    # Clear all state markers in context for the next 12-hour run
    update_integration_context_by_event_type(
        event_type, {"instance_id": "", "report_id": "", "finish_current_report_stream": False}
    )

    log(event_type, "--- Cleanup Phase Complete ---")


async def ensure_report_config_exists(client: InsightVMClient, event_type: str, context: dict) -> None:
    """
    Checks if a report configuration exists in the integration context.
    If not, it creates one sequentially and updates the context.
    """
    existing_id = context.get(event_type, {}).get("report_id")

    if existing_id:
        log(event_type, f"Report config already exists (ID: {existing_id}). Skipping creation.")
        return

    log(event_type, "No report config found. Creating new configuration.")

    # We await this call so it finishes completely before moving on
    new_report_id = await create_report_config_from_template(client, event_type)

    update_integration_context_by_event_type(event_type, {"report_id": new_report_id})
    log(event_type, f"Context updated with new Report ID: {new_report_id}")


async def run_all_collectors(
    client: InsightVMClient,
    batch_size: int = DEFAULT_BATCH_SIZE,
):
    """
    Runs the Asset and Vulnerability report collector workflows concurrently,
    using the provided, initialized InsightVMClient.
    """

    try:
        context = get_integration_context()
        await ensure_report_config_exists(client, "asset", context)
        await ensure_report_config_exists(client, "vulnerability", context)

    except Exception as e:
        raise DemistoException(f"FATAL: Failed during report configuration setup. Aborting run. Error: {e}")

    demisto.debug("Starting concurrent execution of Asset and Vulnerability collectors")

    asset_task = run_full_collector_workflow(client=client, batch_size=batch_size, event_type="asset")
    vulnerability_task = run_full_collector_workflow(client=client, batch_size=batch_size, event_type="vulnerability")

    # Run both tasks concurrently. return_exceptions=True ensures one failure
    # doesn't immediately stop the other task.
    results = await asyncio.gather(asset_task, vulnerability_task, return_exceptions=True)
    demisto.debug("\n--- Concurrent Execution Complete ---")

    success = True
    exceptions = []

    for i, result in enumerate(results):
        if isinstance(result, Exception):
            task_name = "Asset Collector" if i == 0 else "Vulnerability Collector"
            exceptions.append(f"!!! ERROR: {task_name} failed: {result}")
            success = False

    if not success:
        # Aggregate all errors into a single failure message
        error_message = "\n".join(exceptions)
        raise DemistoException(f"One or more concurrent collector workflows failed:\n{error_message}")

    demisto.debug("All collector workflows finished successfully.")


async def fetch_assets_command(params: dict, token: str) -> None:
    """Fetch assets command - replaces long-running-execution.

    The platform's fetch-assets scheduler handles re-invocation at the configured interval.
    Each invocation runs the full collector pipeline once.
    """
    demisto.debug("Starting fetch-assets execution")
    async with InsightVMClient(
        base_url=params["server"],
        username=params["credentials"].get("identifier"),
        password=params["credentials"].get("password"),
        token=token,
        verify=not params.get("unsecure"),
    ) as client:
        await run_all_collectors(client, batch_size=DEFAULT_BATCH_SIZE)
    demisto.debug("Finished fetch-assets execution")


def main():  # pragma: no cover
    try:
        args = demisto.args()
        params = demisto.params()
        command = demisto.command()
        handle_proxy()

        # A workaround for fixing compatibility issues when upgrading existing instances that are < 1.2.0.
        # ('token' field was converted from type 0 to type 9)
        token = ""

        if params.get("token", ""):
            if isinstance(params["token"], str):
                token = params["token"]

            elif params["token"].get("identifier"):
                token = params["token"]["identifier"]

        client = Client(
            url=params["server"],
            username=params["credentials"].get("identifier"),
            password=params["credentials"].get("password"),
            token=token,
            verify=not params.get("unsecure"),
            connection_error_retries=arg_to_number(params.get("connection_error_retries", CONNECTION_ERRORS_RETRIES))
            or CONNECTION_ERRORS_RETRIES,
        )

        results: CommandResults | list[CommandResults] | dict | str

        if command == "test-module":
            client.get_assets(page_size=1, limit=1)
            results = "ok"
        elif command == "fetch-assets":
            asyncio.run(fetch_assets_command(params, token))
            return
        elif command == "nexpose-create-asset":
            results = create_asset_command(client=client, **args)
        elif command == "nexpose-create-assets-report":
            results = create_assets_report_command(client=client, report_format=args.pop("format", None), **args)
        elif command == "nexpose-create-scan-report":
            results = create_scan_report_command(client=client, report_format=args.pop("format", None), **args)
        elif command == "nexpose-create-scan-schedule":
            results = create_scan_schedule_command(client=client, **args)
        elif command == "nexpose-create-shared-credential":
            results = create_shared_credential_command(client=client, **args)
        elif command == "nexpose-create-site":
            results = create_site_command(client=client, template_id=args.pop("scanTemplateId", None), **args)
        elif command == "nexpose-create-sites-report":
            results = create_sites_report_command(client=client, report_format=args.pop("format", None), **args)
        elif command == "nexpose-create-site-scan-credential":
            results = create_site_scan_credential_command(client=client, **args)
        elif command == "nexpose-create-vulnerability-exception":
            results = create_vulnerability_exception_command(client=client, **args)
        elif command == "nexpose-delete-asset":
            results = delete_asset_command(client=client, asset_id=args.pop("id"))
        elif command == "nexpose-delete-scan-schedule":
            results = delete_scan_schedule_command(client=client, **args)
        elif command == "nexpose-delete-shared-credential":
            results = delete_shared_credential_command(client=client, shared_credential_id=args.pop("id"))
        elif command == "nexpose-delete-site-scan-credential":
            results = delete_site_scan_credential_command(client=client, **args)
        elif command == "nexpose-delete-vulnerability-exception":
            results = delete_vulnerability_exception_command(client=client, vulnerability_exception_id=args.pop("id"))
        elif command == "nexpose-delete-site":
            results = delete_site_command(client=client, site_id=args.pop("id", None), **args)
        elif command == "nexpose-disable-shared-credential":
            results = set_assigned_shared_credential_status_command(client=client, enabled=False, **args)
        elif command == "nexpose-download-report":
            results = download_report_command(client=client, report_format=args.pop("format"), **args)
        elif command == "nexpose-enable-shared-credential":
            results = set_assigned_shared_credential_status_command(client=client, enabled=True, **args)
        elif command == "nexpose-get-asset":
            results = get_asset_command(client=client, asset_id=args.pop("id"))
        elif command == "nexpose-get-asset-tags":
            results = get_asset_tags_command(client=client, asset_id=args.pop("asset_id"))
        elif command == "nexpose-get-asset-vulnerability":
            results = get_asset_vulnerability_command(
                client=client, asset_id=args.pop("id"), vulnerability_id=args.pop("vulnerabilityId")
            )
        elif command == "nexpose-get-assets":
            results = get_assets_command(client=client, **args)
        elif command == "nexpose-get-report-templates":
            results = get_report_templates_command(client=client)
        elif command == "nexpose-get-report-status":
            results = get_generated_report_status_command(client=client, **args)
        elif command == "nexpose-get-scan":
            results = get_scan_command(client=client, scan_ids=args.pop("id"))
        elif command == "nexpose-get-scans":
            results = get_scans_command(client=client, **args)
        elif command == "nexpose-get-sites":
            results = get_sites_command(client=client, **args)
        elif command == "nexpose-list-assigned-shared-credential":
            results = list_assigned_shared_credential_command(client=client, **args)
        elif command == "nexpose-list-site-scan-credential":
            results = list_site_scan_credential_command(client=client, **args)
        elif command == "nexpose-list-vulnerability":
            results = list_vulnerability_command(client=client, vulnerability_id=args.pop("id", None), **args)
        elif command == "nexpose-list-vulnerability-exceptions":
            results = list_vulnerability_exceptions_command(
                client=client, vulnerability_exception_id=args.pop("id", None), **args
            )
        elif command == "nexpose-list-scan-schedule":
            results = list_scan_schedule_command(client=client, **args)
        elif command == "nexpose-list-shared-credential":
            results = list_shared_credential_command(client=client, credential_id=args.pop("id", None), **args)
        elif command == "nexpose-pause-scan":
            results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.PAUSE)
        elif command == "nexpose-resume-scan":
            results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.RESUME)
        elif command == "nexpose-update-shared-credential":
            results = update_shared_credential_command(client=client, shared_credential_id=args.pop("id"), **args)
        elif command == "nexpose-update-site-scan-credential":
            results = update_site_scan_credential_command(client=client, **args)
        elif command == "nexpose-update-scan-schedule":
            results = update_scan_schedule_command(client=client, **args)
        elif command == "nexpose-update-vulnerability-exception-expiration":
            results = update_vulnerability_exception_expiration_command(
                client=client, vulnerability_exception_id=args.pop("id"), **args
            )
        elif command == "nexpose-update-vulnerability-exception-status":
            results = update_vulnerability_exception_status_command(
                client=client, vulnerability_exception_id=args.pop("id"), **args
            )
        elif command == "nexpose-search-assets":
            results = search_assets_command(
                client=client,
                query=args.pop("query", None),
                ip_address_is=args.pop("ipAddressIs", None),
                host_name_is=args.pop("hostNameIs", None),
                risk_score_higher_than=args.pop("riskScoreHigherThan", None),
                vulnerability_title_contains=args.pop("vulnerabilityTitleContains", None),
                site_id_in=args.pop("siteIdIn", None),
                site_name_in=args.pop("siteNameIn", None),
                **args,
            )
        elif command == "nexpose-start-assets-scan":
            results = start_assets_scan_command(
                client=client, ip_addresses=args.pop("IPs", None), hostnames=args.pop("hostNames", None), **args
            )
        elif command == "nexpose-start-site-scan":
            results = start_site_scan_command(client=client, site_id=args.pop("site", None), **args)
        elif command == "nexpose-stop-scan":
            results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.STOP)
        elif command == "nexpose-create-tag":
            results = create_tag_command(client=client, **args)
        elif command == "nexpose-delete-tag":
            results = delete_tag_command(client=client, **args)
        elif command == "nexpose-list-tag":
            results = get_list_tag_command(client=client, **args)
        elif command == "nexpose-update-tag-search-criteria":
            results = update_tag_search_criteria_command(client=client, **args)
        elif command == "nexpose-list-tag-asset-group":
            results = get_list_tag_asset_group_command(client=client, **args)
        elif command == "nexpose-add-tag-asset-group":
            results = add_tag_asset_group_command(client=client, **args)
        elif command == "nexpose-remove-tag-asset-group":
            results = remove_tag_asset_group_command(client=client, **args)
        elif command == "nexpose-list-tag-asset":
            results = get_list_tag_asset_command(client=client, **args)
        elif command == "nexpose-add-tag-asset":
            results = add_tag_asset_command(client=client, **args)
        elif command == "nexpose-remove-tag-asset":
            results = remove_tag_asset_command(client=client, **args)
        elif command == "nexpose-add-site-included-asset":
            results = add_site_asset_command(client=client, target_type="included", **args)
        elif command == "nexpose-remove-site-included-asset":
            results = remove_site_asset_command(client=client, target_type="included", **args)
        elif command == "nexpose-list-site-included-asset":
            results = list_site_assets_command(client=client, asset_type="assets", target_type="included", **args)
        elif command == "nexpose-list-site-included-asset-group":
            results = list_site_assets_command(client=client, asset_type="asset_groups", target_type="included", **args)
        elif command == "nexpose-add-site-excluded-asset":
            results = add_site_asset_command(client=client, target_type="excluded", **args)
        elif command == "nexpose-remove-site-excluded-asset":
            results = remove_site_asset_command(client=client, target_type="excluded", **args)
        elif command == "nexpose-list-site-excluded-asset":
            results = list_site_assets_command(client=client, asset_type="assets", target_type="excluded", **args)
        elif command == "nexpose-list-site-excluded-asset-group":
            results = list_site_assets_command(client=client, asset_type="asset_groups", target_type="excluded", **args)
        elif command == "nexpose-list-asset-group":
            results = get_list_asset_group_command(client=client, **args)
        elif command == "nexpose-create-asset-group":
            results = create_asset_group_command(client=client, **args)
        else:
            raise NotImplementedError(f"Command {command} not implemented.")

        if isinstance(results, list) and len(results) == 1:
            return_results(results[0])

        else:
            return_results(results)

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


if __name__ in ("__main__", "builtin", "builtins"):  # pragma: no cover
    main()