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 json
from pathlib import Path
from unittest.mock import AsyncMock

import copy
import pytest
from Rapid7_Nexpose import *


@pytest.fixture
def mock_client():
    return Client(
        url="url",
        username="username",
        password="password",
        verify=False,
    )


def load_test_data(folder: str, file_name: str) -> dict:
    """
    A function for loading and returning data from json files within the "test_data" folder.

    Args:
        folder (str): Name of the parent folder of the file within `test_data`.
        file_name (str): Name of a json file to load data from.

    Returns:
        dict: Dictionary data loaded from the json file.
    """
    with open(Path("test_data") / folder / f"{file_name}.json") as f:
        return json.load(f)


def test_connection_errors_recovers(mocker, mock_client):
    """
    Given:
     - Connection Error, ReadTimeout error and a success response

    When:
     - running the _http_request method

    Then:
     - Ensure that success message is printed and recovery for http request happens.
    """
    mocker.patch.object(demisto, "error")
    mocker.patch("Rapid7_Nexpose.time.sleep")
    mocker.patch.object(
        BaseClient,
        "_http_request",
        side_effect=[
            DemistoException(message="error", exception=requests.ConnectionError("error")),
            requests.ReadTimeout("error"),
            "success",
        ],
    )
    assert mock_client._http_request(method="GET", url_suffix="url") == "success"


def test_http_request_no_connection_errors(mocker, mock_client):
    """
    Given:
     - general Http error

    When:
     - running the _http_request method

    Then:
     - Ensure that the exception is raised without triggering the retry mechanism
    """
    mocker.patch.object(demisto, "error")
    sleep_mocker = mocker.patch("Rapid7_Nexpose.time.sleep")
    mocker.patch.object(
        BaseClient,
        "_http_request",
        side_effect=[DemistoException(message="error", exception=requests.exceptions.HTTPError("error"))],
    )
    with pytest.raises(DemistoException):
        assert mock_client._http_request(method="GET", url_suffix="url")

    assert not sleep_mocker.called


# --- Utility Functions Tests ---
@pytest.mark.parametrize(
    "mock_files_prefix, pages, test_input_kwargs, expected_output_context_file",
    [
        ("get_vulnerabilities", 4, {"page_size": 3, "limit": 10}, "get_vulnerabilities_output"),
        ("get_vulnerabilities", 4, {"page_size": 3, "page": 2}, "get_vulnerabilities_specific_page_output"),
    ],
)
def test_client_paged_http_request(
    mocker, mock_client: Client, mock_files_prefix: str, pages: int, test_input_kwargs: dict, expected_output_context_file: str
):
    """
    Given: Valid pagination parameters.
    When: Calling the client_paged_http_request function.
    Then: Ensure the function returns the expected output, considering the pagination parameters.
    """
    mock_data = [load_test_data("paged_http_request", mock_files_prefix + f"_{i}") for i in range(pages)]

    def pagination_side_effect(**kwargs):
        if kwargs.get("params") and kwargs["params"].get("page"):
            return mock_data[int(kwargs["params"]["page"])]

        return mock_data[0]

    mocker.patch.object(BaseClient, "_http_request", side_effect=pagination_side_effect)
    assert mock_client._paged_http_request(**test_input_kwargs) == load_test_data(
        "paged_http_request", f"{expected_output_context_file}"
    )


@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        (
            ["risk-score is-greater-than 1000.5", "vulnerability-title contains 7zip"],
            [
                {"field": "risk-score", "operator": "is-greater-than", "value": 1000.5},
                {"field": "vulnerability-title", "operator": "contains", "value": "7zip"},
            ],
        )
    ],
)
def test_convert_asset_search_filters(test_input: list[str], expected_output: list[dict]):
    """
    Given: A list of filters in a string format.
    When: Calling the convert_asset_search_filters function.
    Then: Ensure the function returns a list of valid dictionaries, where each dictionary represents a filter.
    """
    assert convert_asset_search_filters(test_input) == expected_output


@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        ("2022-01-01T00:00:00Z", strptime("2022-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")),
        ("2022-01-01T00:00:00.000Z", strptime("2022-01-01T00:00:00.000Z", "%Y-%m-%dT%H:%M:%S.%fZ")),
    ],
)
def test_convert_datetime_str(test_input: str, expected_output: struct_time):
    """
    Given: An ISO 8601 formatted date string.
    When: Calling the convert_datetime_str function.
    Then: Ensure the function returns a struct_time object matching the given date string.
    """
    assert convert_datetime_str(test_input) == expected_output


@pytest.mark.parametrize(
    "test_input_kwargs, expected_output",
    [
        (
            {
                "service": CredentialService.CIFSHASH,
                "domain": "Test1",
                "username": "Test2",
                "password": "Test3",
                "ntlm_hash": "Test4",
            },
            {"service": "cifshash", "username": "Test2", "password": "Test3", "domain": "Test1", "ntlmHash": "Test4"},
        ),
        (
            {"service": CredentialService.HTTP, "http_realm": "Test1", "username": "Test2", "password": "Test3"},
            {"service": "http", "username": "Test2", "password": "Test3", "realm": "Test1"},
        ),
        (
            {
                "service": CredentialService.MS_SQL,
                "database_name": "Test1",
                "username": "Test2",
                "password": "Test3",
                "use_windows_authentication": True,
                "domain": "Test4",
            },
            {
                "service": "ms-sql",
                "username": "Test2",
                "password": "Test3",
                "useWindowsAuthentication": True,
                "domain": "Test4",
                "database": "Test1",
            },
        ),
        ({"service": CredentialService.NOTES, "notes_id_password": "Test1"}, {"service": "notes", "notesIDPassword": "Test1"}),
        (
            {
                "service": CredentialService.ORACLE,
                "oracle_sid": "Test1",
                "username": "Test2",
                "password": "Test3",
                "oracle_enumerate_sids": True,
                "oracle_listener_password": "Test4",
            },
            {
                "service": "oracle",
                "username": "Test2",
                "password": "Test3",
                "sid": "Test1",
                "enumerateSids": True,
                "oracleListenerPassword": "Test4",
            },
        ),
        ({"service": CredentialService.SNMP, "snmp_community_name": "Test1"}, {"service": "snmp", "community": "Test1"}),
        (
            {
                "service": CredentialService.SNMPV3,
                "snmpv3_authentication_type": SNMPv3AuthenticationType.SHA,
                "username": "Test1",
                "password": "Test2",
            },
            {"service": "snmpv3", "username": "Test1", "authenticationType": "sha", "password": "Test2"},
        ),
        (
            {
                "service": CredentialService.SSH,
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": SSHElevationType.PRIVILEGED_EXEC,
                "ssh_permission_elevation_username": "Test3",
                "ssh_permission_elevation_password": "Test4",
            },
            {
                "service": "ssh",
                "username": "Test1",
                "password": "Test2",
                "permissionElevation": "privileged-exec",
                "permissionElevationUsername": "Test3",
                "permissionElevationPassword": "Test4",
            },
        ),
        (
            {
                "service": CredentialService.SSH_KEY,
                "ssh_key_pem": "Test1",
                "ssh_private_key_password": "Test2",
                "username": "Test3",
                "ssh_permission_elevation": SSHElevationType.SUDO,
                "ssh_permission_elevation_username": "Test4",
                "ssh_permission_elevation_password": "Test5",
            },
            {
                "service": "ssh-key",
                "username": "Test3",
                "permissionElevation": "sudo",
                "permissionElevationUsername": "Test4",
                "permissionElevationPassword": "Test5",
                "privateKeyPassword": "Test2",
                "pemKey": "Test1",
            },
        ),
    ],
)
def test_create_credential_creation_body(test_input_kwargs: dict, expected_output: dict):
    """
    Given: A dictionary of valid keyword arguments for the create_credential_creation_body function.
    When: Calling the create_credential_creation_body function.
    Then: Ensure the function returns a dictionary representing a valid credential creation / update request body.
    """
    assert create_credential_creation_body(**test_input_kwargs) == expected_output


@pytest.mark.parametrize(
    "test_input_kwargs",
    [
        ({"service": CredentialService.CIFSHASH, "domain": "Test1", "username": "Test2", "password": "Test3"}),
        (
            {
                "service": CredentialService.HTTP,
                "http_realm": "Test1",
                "username": "Test2",
            }
        ),
        (
            {
                "service": CredentialService.MS_SQL,
                "database_name": "Test1",
                "password": "Test3",
                "use_windows_authentication": True,
                "domain": "Test4",
            }
        ),
        (
            {
                "service": CredentialService.ORACLE,
                "oracle_sid": "Test1",
                "username": "Test2",
                "password": "Test3",
                "oracle_enumerate_sids": True,
            }
        ),
        ({"service": CredentialService.SNMP}),
        ({"service": CredentialService.SNMPV3, "username": "Test1", "password": "Test2"}),
        ({"service": CredentialService.SNMPV3, "snmpv3_authentication_type": SNMPv3AuthenticationType.SHA, "username": "Test1"}),
        (
            {
                "service": CredentialService.SNMPV3,
                "snmpv3_authentication_type": SNMPv3AuthenticationType.SHA,
                "username": "Test1",
                "password": "Test2",
                "snmpv3_privacy_type": SNMPv3PrivacyType.AES_256,
            }
        ),
        (
            {
                "service": CredentialService.SSH,
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": SSHElevationType.PRIVILEGED_EXEC,
            }
        ),
        (
            {
                "service": CredentialService.SSH_KEY,
                "ssh_private_key_password": "Test2",
                "username": "Test3",
                "ssh_permission_elevation": SSHElevationType.SUDO,
                "ssh_permission_elevation_username": "Test4",
                "ssh_permission_elevation_password": "Test5",
            }
        ),
    ],
)
def test_create_credential_creation_body_validations(test_input_kwargs: dict):
    """
    Given: A dictionary of invalid keyword arguments for the create_credential_creation_body function.
    When: Calling the create_credential_creation_body function.
    Then: Ensure validation errors are raised.
    """
    with pytest.raises(ValueError):
        create_credential_creation_body(**test_input_kwargs)


@pytest.mark.parametrize(
    "test_input_kwargs, expected_output",
    [
        ({"a": "test", "b": 1, "c": None, "d": 6.1}, {"a": "test", "b": 1, "d": 6.1}),
        ({"a": None, "b": {}, "c": (1, "test")}, {"b": {}, "c": (1, "test")}),
        ({"strict_mode": True, "a": False, "b": {}, "c": (1, "test"), "d": 1}, {"c": (1, "test"), "d": 1}),
    ],
)
def test_find_valid_params(test_input_kwargs: dict, expected_output: dict):
    """
    Given: A dictionary of valid keyword arguments for the find_valid_params function.
    When: Calling the find_valid_params function.
    Then: Ensure the function returns a dictionary containing only the valid keyword arguments.
    """
    assert find_valid_params(**test_input_kwargs) == expected_output


@pytest.mark.parametrize(
    "test_input_kwargs, expected_output",
    [
        ({"years": 1, "months": 8, "weeks": 2, "days": 6}, "P1Y8M2W6D"),
        ({"hours": 16, "minutes": 26, "seconds": 53.4}, "PT16H26M53.4S"),
        ({"years": 4, "months": 3, "weeks": 1, "days": 2, "hours": 12, "minutes": 43, "seconds": 12.5}, "P4Y3M1W2DT12H43M12.5S"),
    ],
)
def test_generate_duration_time(test_input_kwargs: dict, expected_output: str):
    """
    Given: A dictionary of valid keyword arguments for the generate_duration_time function.
    When: Calling the generate_duration_time function.
    Then: Ensure the function returns a string representing a valid duration time matching the given arguments.
    """
    assert generate_duration_time(**test_input_kwargs) == expected_output


@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        ("PT2M16.481S", "2 minutes, 16.481 seconds"),
        ("PT51.316S", "51.316 seconds"),
        ("P3Y6M4DT12H30M5S", "3 years, 6 months, 4 days, 12 hours, 30 minutes, 5 seconds"),
        ("Invalid", None),
    ],
)
def test_readable_duration_time(test_input: str, expected_output: float | None):
    """
    Given: A string representing a valid duration time in ISO 8601 format.
    When: Calling the readable_duration_time function.
    Then: Ensure the function returns a string representing a valid and correct readable duration time.
    """
    if not re.fullmatch(r"P(?:[\d.]+[YMWD]){0,4}T(?:[\d.]+[HMS]){0,3}", test_input):
        with pytest.raises(ValueError):
            readable_duration_time(test_input)

    else:
        assert readable_duration_time(test_input) == expected_output


@pytest.mark.parametrize(
    "test_input_data, test_input_key, expected_output",
    [
        ({"a": "b", "c": "d", "e": "f"}, "a", {"c": "d", "e": "f"}),
        (("a", {1: "b"}), 1, ("a", {})),
        ([1, 2, {"a": "b", "test": "test"}], "test", [1, 2, {"a": "b"}]),
        ({"a": {"b": {"test": "x"}}}, "test", {"a": {"b": {}}}),
    ],
)
def test_remove_dict_key(test_input_data: dict | list | tuple, test_input_key: str, expected_output: dict | list | tuple):
    """
    Given: A dictionary, list or tuple and a key to remove.
    When: Calling the remove_dict_key function.
    Then: Ensure the function returns a dictionary, list or tuple without the given key.
    """
    assert remove_dict_key(test_input_data, test_input_key) == expected_output


@pytest.mark.parametrize(
    "test_input_data, name_mapping, include_none, expected_output",
    [
        ({"a": "b", "c": "d", "e": "f"}, {"a": "A", "e": "E"}, False, {"A": "b", "E": "f"}),
        ({"a": {"b": {"test": "x"}}}, {"a": "A", "test": "TEST"}, True, {"A": {"b": {"test": "x"}}, "TEST": None}),
        ([(1, {"a": {"b": {"a": "a"}}}), 2], {"a": "A"}, False, [(1, {"A": {"b": {"a": "a"}}}), 2]),
        ({"a": {"b": {"test": "x"}}}, {"a.b": "A", "test": "TEST"}, False, {"A": {"test": "x"}}),
        ({}, {"a": "b"}, False, {}),
    ],
)
def test_generate_new_dict(test_input_data: dict | list, name_mapping: dict, include_none: bool, expected_output: dict | list):
    """
    Given: A dictionary, list or tuple and a name-mapping dictionary.
    When: Calling the generate_new_dict function.
    Then: Ensure the function returns a dictionary, list or tuple with new keys
        according to the name-mapping dictionary.
    """
    result = generate_new_dict(test_input_data, name_mapping, include_none)
    assert result == expected_output


@pytest.mark.parametrize(
    "sites_mock_file, site_id, site_name, send_client, expected_output_id",
    [
        ("client_get_sites", "1", "Test 1", True, "1"),
        ("client_get_sites", "2", None, False, "2"),
        ("client_get_sites", None, "Test 3", True, "3"),
        ("client_get_sites", None, "Test 2", False, None),
        ("client_get_sites", None, "This site does not exist", True, None),
        ("client_get_sites", None, None, True, None),
    ],
)
def test_site_init(
    mocker,
    mock_client: Client,
    sites_mock_file: str,
    send_client: bool,
    site_id: str | None,
    site_name: str | None,
    expected_output_id: str | None,
):
    """
    Given: A site ID and a site name
    When: Calling the Site class constructor
    Then: If valid - ensure the class is initialized correctly. If not - ensure an exception is raised.
    """
    sites_api_data = load_test_data("api_mock", sites_mock_file)
    mocker.patch.object(Client, "_paged_http_request", return_value=sites_api_data)
    client = mock_client if send_client else None
    site_names = [site["name"] for site in sites_api_data]

    if site_id is None and site_name is None:
        # Assure an error is raised when neither site id nor site name are provided
        with pytest.raises(ValueError):
            Site(client=client, site_id=site_id, site_name=site_name)

    elif site_id is None and client is None:
        # Assure an error is raised when site name is provided without ID, but without a client
        with pytest.raises(ValueError):
            Site(client=client, site_id=site_id, site_name=site_name)

    elif site_id is None and site_name not in site_names:
        # Assure an error is raised when site name is provided without ID, and a site with that name does not exist
        with pytest.raises(DemistoException):
            Site(client=client, site_id=site_id, site_name=site_name)

    else:
        assert Site(client=client, site_id=site_id, site_name=site_name).id == expected_output_id


# --- Command & Client Functions Tests ---
@pytest.mark.parametrize(
    "scope, template_id, report_name, report_format",
    [
        ({"sites": [1]}, "1", "Test", "pdf"),
    ],
)
def test_client_create_report_config(
    mocker, mock_client: Client, scope: dict, template_id: str, report_name: str, report_format: str
):
    """
    Given: Valid parameters for the create_report_config function.
    When: Calling the create_report_config function.
    Then: Ensure the API call is being called with the correct parameters.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request")
    mock_client.create_report_config(scope=scope, template_id=template_id, report_name=report_name, report_format=report_format)

    http_request.assert_called_with(
        url_suffix="/reports",
        method="POST",
        json_data={
            "scope": scope,
            "template": template_id,
            "name": report_name,
            "format": report_format.lower(),
        },
        resp_type="json",
    )


@pytest.mark.parametrize("api_mock_file, expected_output", [("client_find_asset_site", Site(site_id="1", site_name="Test"))])
def test_client_find_asset_site(mocker, mock_client: Client, api_mock_file: str, expected_output: Site):
    """
    Given: A valid asset ID.
    When: Calling the find_asset_site function.
    Then: Ensure the function returns the correct site.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    returned_site = mock_client.find_asset_site(asset_id="1")
    assert returned_site.id == expected_output.id
    assert returned_site.name == expected_output.name


@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        ("Test 1", "1"),
        ("Test 2", "2"),
        ("Test 3", "3"),
        ("Site-That-Doesn't-Exist", None),
    ],
)
def test_client_find_site_id(mocker, mock_client: Client, test_input: str, expected_output: Union[str, None]):
    """
    Given: A valid site name.
    When: Calling the find_site_id function.
    Then: Ensure the function returns the correct site ID.
    """
    mocker.patch.object(Client, "_paged_http_request", return_value=load_test_data("api_mock", "client_get_sites"))
    assert mock_client.find_site_id(test_input) == expected_output


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_output_context",
    [
        ({"site_id": "1", "date": "2022-01-01T10:00:00Z", "ip": "192.0.2.0"}, {"id": 1}, {"id": 1}),
        (
            {"site_id": "1", "date": "2022-01-01T10:00:00Z", "host_name": "localhost", "host_name_source": "LDAP"},
            {"id": 1},
            {"id": 1},
        ),
        ({"site_id": "1", "date": "2022-01-01T10:00:00Z"}, None, None),
    ],
)
def test_create_asset_command(
    mocker, mock_client: Client, test_input_kwargs: dict, api_mock_data: dict | None, expected_output_context: dict | None
):
    """
    Given: Valid parameters for the create_asset_command function.
    When: Calling the create_asset_command function.
    Then: Ensure the API call is made with the correct parameters.
    """
    mocker.patch.object(Client, "_http_request", return_value=api_mock_data)
    if test_input_kwargs.get("ip") is not None or test_input_kwargs.get("host_name") is not None:
        assert create_asset_command(client=mock_client, **test_input_kwargs).outputs == expected_output_context

    else:
        # Assure an error is raised if neither of ip_address or hostname are provided
        with pytest.raises(ValueError):
            create_asset_command(client=mock_client, **test_input_kwargs)


@pytest.mark.parametrize(
    "report_templates_mock_file, report_config_mock_data, report_mock_data, expected_output_context_file",
    [("client_get_report_templates", {"id": 1}, {"id": 2}, "create_report_commands")],
)
def test_create_report_commands(
    mocker,
    mock_client: Client,
    report_templates_mock_file: str,
    report_config_mock_data: dict,
    report_mock_data: dict,
    expected_output_context_file: str,
):
    """
    Given: Valid parameters for different report creation commands.
    When: Calling the create_report_command function.
    Then: Ensure a valid context output is returned.
    """
    report_templates_data = load_test_data("api_mock", report_templates_mock_file)
    mocker.patch.object(Client, "get_report_templates", return_value=report_templates_data)
    mocker.patch.object(Client, "create_report_config", return_value=report_config_mock_data)
    mocker.patch.object(Client, "_http_request", return_value=report_mock_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    assert (
        create_assets_report_command(client=mock_client, assets="1", name="Test Report", download_immediately="false").outputs
        == expected_output_context
    )
    assert (
        create_scan_report_command(client=mock_client, scan="1", name="Test Report", download_immediately="false").outputs
        == expected_output_context
    )
    assert (
        create_sites_report_command(client=mock_client, sites="1,2,3", name="Test Report", download_immediately="false").outputs
        == expected_output_context
    )


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_post_data, expected_output_context",
    [
        (
            {
                "site_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "week",
                "interval_time": "2",
                "duration_days": "1",
                "duration_hours": "1",
                "duration_minutes": "1",
                "scan_name": "Test",
                "enabled": "true",
                "included_targets": "192.0.2.0,192.0.2.1",
                "included_asset_groups": "1,2",
                "excluded_targets": "192.0.2.2,192.0.2.3",
                "excluded_asset_groups": "3,4",
            },
            {"id": 1},
            {
                "assets": {
                    "excludedAssetGroups": {"assetGroupIDs": [3, 4]},
                    "excludedTargets": {"addresses": ["192.0.2.2", "192.0.2.3"]},
                    "includedAssetGroups": {"assetGroupIDs": [1, 2]},
                    "includedTargets": {"addresses": ["192.0.2.0", "192.0.2.1"]},
                },
                "duration": "P1DT1H1M",
                "enabled": True,
                "onScanRepeat": "restart-scan",
                "repeat": {"every": "week", "interval": 2},
                "scanName": "Test",
                "start": "2050-01-01T10:00:00Z",
            },
            {"id": 1},
        ),
        (
            {
                "site_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
            },
            {"id": 1},
            {"enabled": True, "onScanRepeat": "restart-scan", "start": "2050-01-01T10:00:00Z"},
            {"id": 1},
        ),
        (
            {
                "site_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "week",
                "enabled": "true",
            },
            {"id": 1},
            None,
            None,
        ),
        (
            {
                "site_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "Date-of-month",
                "interval_time": "2",
                "duration_days": "1",
                "duration_hours": "1",
                "duration_minutes": "1",
                "scan_name": "Test",
                "enabled": "true",
                "included_targets": "192.0.2.0,192.0.2.1",
                "included_asset_groups": "1,2",
                "excluded_targets": "192.0.2.2,192.0.2.3",
                "excluded_asset_groups": "3,4",
            },
            {"id": 1},
            None,
            None,
        ),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_create_scan_schedule_command(
    mocker,
    mock_client: Client,
    test_input_kwargs: dict,
    api_mock_data: dict,
    expected_post_data: dict | None,
    expected_output_context: dict,
):
    """
    Given: Valid or invalid parameters for the create_scan_schedule_command function.
    When: Calling the create_scan_schedule_command function.
    Then: If valid - ensure a valid API call is made and a valid context output is returned.
        If invalid - Ensure an  exception is raised.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value=api_mock_data)

    if test_input_kwargs.get("frequency") is not None and (
        test_input_kwargs.get("interval_time") is None
        or (test_input_kwargs["frequency"] == "Date-of-month" and test_input_kwargs.get("date_of_month") is None)
    ):
        with pytest.raises(ValueError):
            create_scan_schedule_command(mock_client, **test_input_kwargs)

    else:
        assert create_scan_schedule_command(mock_client, **test_input_kwargs).outputs == expected_output_context

        http_request.assert_called_with(
            method="POST",
            url_suffix=f"/sites/{test_input_kwargs['site_id']}/scan_schedules",
            json_data=expected_post_data,
            resp_type="json",
        )


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_output_context",
    [
        (
            {
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "FTP",
                "username": "Test1",
                "password": "Test2",
                "host_restriction": "192.0.2.0",
                "port_restriction": "8080",
                "sites": "1,2,3",
            },
            {"id": 1},
            {"id": 1},
        ),
        (
            {
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "SNMPv3",
                "username": "Test1",
                "password": "Test2",
                "authentication_type": "SHA",
                "privacy_type": "AES-256",
                "privacy_password": "123",
            },
            {"id": 1},
            {"id": 1},
        ),
        (
            {
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "Oracle",
                "username": "Test1",
                "password": "Test2",
                "oracle_enumerate_sids": "false",
            },
            {"id": 1},
            {"id": 1},
        ),
        (
            {
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "SSH",
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": "None",
            },
            {"id": 1},
            {"id": 1},
        ),
        (
            {
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "MS-SQL",
                "username": "Test1",
                "password": "Test2",
                "use_windows_authentication": "false",
            },
            {"id": 1},
            {"id": 1},
        ),
    ],
)
def test_create_shared_credential_command(
    mocker, mock_client: Client, test_input_kwargs: dict, api_mock_data: dict, expected_output_context: dict
):
    """
    Given: valid parameters for the create_shared_credential_command function.
    When: Calling the create_shared_credential_command function.
    Then: Ensure a valid context output is returned.
    """
    mocker.patch.object(Client, "_http_request", return_value=api_mock_data)
    assert create_shared_credential_command(client=mock_client, **test_input_kwargs).outputs == expected_output_context


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_post_data, expected_output_context",
    [
        (
            {"name": "Test 1", "description": "Test 2", "assets": "1,2,3", "importance": "very_high"},
            {"id": 1},
            {
                "name": "Test 1",
                "description": "Test 2",
                "importance": "very_high",
                "scan": {"assets": {"includedTargets": {"addresses": ["1", "2", "3"]}}},
            },
            {"Id": 1},
        ),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_create_site(
    mocker,
    mock_client: Client,
    test_input_kwargs: dict,
    api_mock_data: dict,
    expected_post_data: dict,
    expected_output_context: dict,
):
    """
    Given: Valid parameters for the create_site function.
    When: Calling the create_site function.
    Then: Ensure a valid API call is made and a valid context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value=api_mock_data)

    assert create_site_command(client=mock_client, **test_input_kwargs).outputs == expected_output_context

    http_request.assert_called_with(url_suffix="/sites", method="POST", json_data=expected_post_data, resp_type="json")


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_post_data, expected_output_context",
    [
        (
            {
                "site_id": "1",
                "name": "Test",
                "host_restriction": "192.0.2.0",
                "port_restriction": "8080",
                "service": "FTP",
                "username": "Test1",
                "password": "Test2",
            },
            {"id": 1},
            {
                "hostRestriction": "192.0.2.0",
                "name": "Test",
                "portRestriction": "8080",
                "account": {"service": "ftp", "username": "Test1", "password": "Test2"},
            },
            {"id": 1},
        ),
        (
            {
                "site_id": "2",
                "name": "Test",
                "service": "SNMPv3",
                "username": "Test1",
                "password": "Test2",
                "authentication_type": "SHA",
                "privacy_type": "AES-256",
                "privacy_password": "123",
            },
            {"id": 1},
            {
                "name": "Test",
                "account": {
                    "service": "snmpv3",
                    "username": "Test1",
                    "authenticationType": "sha",
                    "password": "Test2",
                    "privacyType": "aes-256",
                    "privacyPassword": "123",
                },
            },
            {"id": 1},
        ),
        (
            {
                "site_id": "3",
                "name": "Test",
                "service": "Oracle",
                "username": "Test1",
                "password": "Test2",
                "oracle_enumerate_sids": "false",
            },
            {"id": 1},
            {
                "name": "Test",
                "account": {
                    "service": "oracle",
                    "username": "Test1",
                    "password": "Test2",
                    "enumerateSids": False,
                    "oracleListenerPassword": None,
                },
            },
            {"id": 1},
        ),
        (
            {
                "site_id": "1",
                "name": "Test",
                "service": "SSH",
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": "None",
            },
            {"id": 1},
            {
                "name": "Test",
                "account": {"service": "ssh", "username": "Test1", "password": "Test2", "permissionElevation": "none"},
            },
            {"id": 1},
        ),
        (
            {
                "site_id": "2",
                "name": "Test",
                "service": "MS-SQL",
                "username": "Test1",
                "password": "Test2",
                "use_windows_authentication": "false",
            },
            {"id": 1},
            {
                "name": "Test",
                "account": {"service": "ms-sql", "username": "Test1", "password": "Test2", "useWindowsAuthentication": False},
            },
            {"id": 1},
        ),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_create_site_scan_credential_command(
    mocker,
    mock_client: Client,
    test_input_kwargs: dict,
    api_mock_data: dict,
    expected_post_data: dict,
    expected_output_context: dict,
):
    """
    Given: Valid parameters for the create_site_scan_credential_command function.
    When: Calling the create_site_scan_credential_command function.
    Then: Ensure a valid API call is made and a valid context output is returned.
    """
    site_id = test_input_kwargs.pop("site_id")
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value=api_mock_data)

    assert (
        create_site_scan_credential_command(client=mock_client, site_id=site_id, **test_input_kwargs).outputs
        == expected_output_context
    )

    http_request.assert_called_with(
        url_suffix=f"/sites/{site_id}/site_credentials", method="POST", json_data=expected_post_data, resp_type="json"
    )


@pytest.mark.parametrize(
    "test_input_kwargs, api_mock_data, expected_output_context",
    [
        (
            {
                "vulnerability_id": "7-zip-cve-2008-6536",
                "scope_type": "Global",
                "state": "Approved",
                "reason": "Acceptable-Risk",
                "comment": "Comment",
            },
            {"id": 1},
            {"id": 1},
        ),
        (
            {
                "vulnerability_id": "7-zip-cve-2008-6536",
                "scope_type": "Site",
                "state": "Approved",
                "reason": "Acceptable-Risk",
                "comment": "Comment",
            },
            None,
            None,
        ),
    ],
)
def test_create_vulnerability_exception_command(
    mocker, mock_client: Client, test_input_kwargs: dict, api_mock_data: dict | None, expected_output_context: dict | None
):
    """
    Given: Valid  or invalid parameters for the create_vulnerability_exception_command function.
    When: Calling the create_vulnerability_exception_command function.
    Then: If valid - ensure a valid context output is returned. If invalid - Ensure an exception is raised.
    """
    mocker.patch.object(Client, "_http_request", return_value=api_mock_data)

    if test_input_kwargs["scope_type"] != "Global" and test_input_kwargs.get("scope_id") is None:
        with pytest.raises(ValueError):
            create_vulnerability_exception_command(client=mock_client, **test_input_kwargs)

    else:
        assert create_vulnerability_exception_command(client=mock_client, **test_input_kwargs).outputs == expected_output_context


@pytest.mark.parametrize(
    "asset_id",
    [
        ("1",),
    ],
)
def test_delete_asset_command(mocker, mock_client: Client, asset_id: str):
    """
    Given: Valid parameters for the delete_asset_command function.
    When: Calling the delete_asset_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_asset_command(client=mock_client, asset_id=asset_id)

    http_request.assert_called_with(
        url_suffix=f"/assets/{asset_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "site_id, schedule_id",
    [
        ("1", "2"),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_delete_scheduled_scan_command(mocker, mock_client: Client, site_id: str, schedule_id: str):
    """
    Given: Valid parameters for the delete_scheduled_scan_command function.
    When: Calling the delete_scheduled_scan_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_scan_schedule_command(client=mock_client, site_id=site_id, schedule_id=schedule_id)

    http_request.assert_called_with(
        url_suffix=f"/sites/{site_id}/scan_schedules/{schedule_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "shared_credential_id",
    [
        ("1",),
    ],
)
def test_delete_shared_credential_command(mocker, mock_client: Client, shared_credential_id: str):
    """
    Given: Valid parameters for the delete_shared_credential_command function.
    When: Calling the delete_shared_credential_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_shared_credential_command(client=mock_client, shared_credential_id=shared_credential_id)

    http_request.assert_called_with(
        url_suffix=f"/shared_credentials/{shared_credential_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "site_id",
    [
        ("1",),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_delete_site_command(mocker, mock_client: Client, site_id: str):
    """
    Given: Valid parameters for the delete_site_command function.
    When: Calling the delete_site_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_site_command(client=mock_client, site_id=site_id)

    http_request.assert_called_with(
        url_suffix=f"/sites/{site_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "site_id, credential_id",
    [
        ("1", "2"),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_delete_site_scan_credential_command(mocker, mock_client: Client, site_id: str, credential_id: str):
    """
    Given: Valid parameters for the delete_site_scan_credential_command function.
    When: Calling the delete_site_scan_credential_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_site_scan_credential_command(client=mock_client, site_id=site_id, credential_id=credential_id)

    http_request.assert_called_with(
        url_suffix=f"/sites/{site_id}/site_credentials/{credential_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "vulnerability_exception_id",
    [
        ("1",),
    ],
)
def test_delete_vulnerability_exception_command(mocker, mock_client: Client, vulnerability_exception_id: str):
    """
    Given: Valid parameters for the delete_vulnerability_exception_command function.
    When: Calling the delete_vulnerability_exception_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = delete_vulnerability_exception_command(client=mock_client, vulnerability_exception_id=vulnerability_exception_id)

    http_request.assert_called_with(
        url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}",
        method="DELETE",
        resp_type="json",
    )

    assert result.outputs is None


def test_download_report_command(mocker, mock_client: Client):
    """
    Given: Valid parameters for the download_report_command function.
    When: Calling the download_report_command function.
    Then: Ensure a valid dictionary is returned matching the expected output.
    """
    mocker.patch.object(Client, "download_report", return_value=b"Test")
    mocker.patch("builtins.open", mocker.mock_open())
    mocker.patch("uuid.uuid4", return_value="RandomUUID4")

    result = download_report_command(client=mock_client, report_id="1", instance_id="latest", name="Test")

    assert result == {"Contents": "", "ContentsFormat": "text", "Type": 9, "File": "Test.pdf", "FileID": "RandomUUID4"}


@pytest.mark.parametrize(
    "asset_mock_file, asset_vulnerability_api_mock_file, vulnerability_api_mock_file, expected_output_context_file",
    [
        (
            "client_get_asset",
            "client_get_asset_vulnerabilities",
            "client_get_vulnerability-certificate-common-name-mismatch",
            "get_asset_command",
        )
    ],
)
def test_get_asset_command(
    mocker,
    mock_client: Client,
    asset_mock_file: str,
    asset_vulnerability_api_mock_file: str,
    vulnerability_api_mock_file: str,
    expected_output_context_file: str,
):
    """
    Given: Valid parameters for the get_asset_command function.
    When: Calling the get_asset_command function.
    Then: Ensure a valid context output is returned.
    """
    asset_mock_data = load_test_data("api_mock", asset_mock_file)
    mocker.patch.object(Client, "get_asset", return_value=asset_mock_data)

    mocker.patch.object(Client, "find_asset_site", return_value=Site(site_id="1", site_name="Test"))

    asset_vulnerability_api_mock_data = load_test_data("api_mock", asset_vulnerability_api_mock_file)
    mocker.patch.object(Client, "get_asset_vulnerabilities", return_value=asset_vulnerability_api_mock_data)

    vulnerability_api_mock_data = load_test_data("api_mock", vulnerability_api_mock_file)
    mocker.patch.object(Client, "get_vulnerability", return_value=vulnerability_api_mock_data)

    result = get_asset_command(client=mock_client, asset_id="1")
    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    if isinstance(result, CommandResults):
        assert result.outputs == expected_output_context

    elif isinstance(result, list):
        assert result[-1].outputs == expected_output_context


@pytest.mark.parametrize("api_mock_file, expected_output_context_file", [("client_get_assets", "get_assets_command")])
def test_get_assets_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_assets_command function.
    When: Calling the get_assets_command function.
    Then: Ensure a valid context output is returned.
    """
    assets_mock_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "get_assets", return_value=assets_mock_data)

    mocker.patch.object(Client, "find_asset_site", return_value=Site(site_id="1", site_name="Test"))

    result = get_assets_command(client=mock_client)
    expected_output_context = load_test_data("expected_context", expected_output_context_file)
    assert [r.outputs for r in result] == expected_output_context


@pytest.mark.parametrize("asset_mock_file, expected_output_context_file", [("client_get_asset_tags", "get_asset_tags_command")])
def test_get_asset_tags_command(mocker, mock_client: Client, asset_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_asset_tags_command function.
    When: Calling the get_asset_tags_command function.
    Then: Ensure a valid context output is returned.
    """
    asset_mock_data = load_test_data("api_mock", asset_mock_file)
    mocker.patch.object(Client, "get_asset_tags", return_value=asset_mock_data)

    result = get_asset_tags_command(client=mock_client, asset_id="1")
    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    if isinstance(result, CommandResults):
        assert result.outputs == expected_output_context

    elif isinstance(result, list):
        assert result[-1].outputs == expected_output_context


@pytest.mark.parametrize(
    "vulnerability_id, asset_vulnerability_mock_file, vulnerability_mock_file, "
    "asset_vulnerability_solution_mock_file, expected_output_context_file",
    [
        (
            "ssl-cve-2011-3389-beast",
            "client_get_asset_vulnerability-ssl-cve-2011-3389-beast",
            "client_get_vulnerability-ssl-cve-2011-3389-beast",
            "client_get_asset_vulnerability_solution-ssl-cve-2011-3389-beast",
            "get_asset_vulnerability_command",
        )
    ],
)
def test_get_asset_vulnerability_command(
    mocker,
    mock_client: Client,
    vulnerability_id: str,
    asset_vulnerability_mock_file: str,
    vulnerability_mock_file: str,
    asset_vulnerability_solution_mock_file: str,
    expected_output_context_file: str,
):
    """
    Given: Valid parameters for the get_asset_vulnerability_command function.
    When: Calling the get_asset_vulnerability_command function.
    Then: Ensure a valid context output is returned.
    """
    asset_vulnerability_data = load_test_data("api_mock", asset_vulnerability_mock_file)
    mocker.patch.object(Client, "get_asset_vulnerability", return_value=asset_vulnerability_data)

    vulnerability_data = load_test_data("api_mock", vulnerability_mock_file)
    mocker.patch.object(Client, "get_vulnerability", return_value=vulnerability_data)

    asset_vulnerability_solution_data = load_test_data("api_mock", asset_vulnerability_solution_mock_file)
    mocker.patch.object(Client, "get_asset_vulnerability_solution", return_value=asset_vulnerability_solution_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    results = get_asset_vulnerability_command(client=mock_client, asset_id="1", vulnerability_id=vulnerability_id)
    assert [result.outputs for result in results] == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file", [("client_get_report_history", "get_generated_report_status_command")]
)
def test_get_generated_report_status_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_generated_report_status_command function.
    When: Calling the get_generated_report_status_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = get_generated_report_status_command(client=mock_client, report_id="1", instance_id="latest")
    assert result.outputs == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file", [("client_get_report_templates", "get_report_templates_command")]
)
def test_get_report_templates_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_report_templates_command function.
    When: Calling the get_report_templates_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = get_report_templates_command(client=mock_client)
    assert result.outputs == expected_output_context


@pytest.mark.parametrize("api_mock_file, ids_input, expected_output_context_file", [("client_get_scan", "1", "get_scan_command")])
def test_get_scan_command(mocker, mock_client: Client, api_mock_file: str, ids_input: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_scan_command function.
    When: Calling the get_scan_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    results = get_scan_command(client=mock_client, scan_ids=ids_input)
    assert [result.outputs for result in results] == expected_output_context


@pytest.mark.parametrize("api_mock_file, expected_output_context_file", [("client_get_scans", "get_scans_command")])
def test_get_scans_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_scans_command function.
    When: Calling the get_scans_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_paged_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = get_scans_command(client=mock_client, active="false")
    assert result.outputs == expected_output_context


@pytest.mark.parametrize("api_mock_file, expected_output_context_file", [("client_get_sites", "get_sites_command")])
def test_get_sites_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the get_sites_command function.
    When: Calling the get_sites_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_paged_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = get_sites_command(client=mock_client)
    assert result.outputs == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file", [("client_get_shared_credentials", "list_shared_credential_command")]
)
def test_list_shared_credential_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the list_shared_credential_command function.
    When: Calling the list_shared_credential_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = list_shared_credential_command(client=mock_client, limit="3")
    assert result.outputs == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file",
    [("client_get_assigned_shared_credentials", "list_assigned_shared_credential_command")],
)
def test_list_assigned_shared_credential_command(
    mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str
):
    """
    Given: Valid parameters for the list_assigned_shared_credential_command function.
    When: Calling the list_assigned_shared_credential_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = list_assigned_shared_credential_command(client=mock_client, site_id="1", limit="3")
    assert result.outputs == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file", [("client_get_vulnerabilities", "list_vulnerability_command")]
)
def test_list_vulnerability_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the list_vulnerability_command function.
    When: Calling the list_vulnerability_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_paged_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = list_vulnerability_command(client=mock_client)
    assert result.outputs == expected_output_context


@pytest.mark.parametrize(
    "api_mock_file, expected_output_context_file",
    [("client_get_vulnerability_exceptions", "list_vulnerability_exceptions_command")],
)
def test_list_vulnerability_exceptions_command(
    mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str
):
    """
    Given: Valid parameters for the list_vulnerability_exceptions_command function.
    When: Calling the list_vulnerability_exceptions_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(Client, "_paged_http_request", return_value=api_data)

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    result = list_vulnerability_exceptions_command(client=mock_client)
    assert result.outputs == expected_output_context


@pytest.mark.parametrize("api_mock_file, expected_output_context_file", [("client_search_assets", "search_assets_command")])
def test_search_assets_command(mocker, mock_client: Client, api_mock_file: str, expected_output_context_file: str):
    """
    Given: Valid parameters for the search_assets_command function.
    When: Calling the search_assets_command function.
    Then: Ensure a valid context output is returned.
    """
    api_data = load_test_data("api_mock", api_mock_file)
    mocker.patch.object(
        Client,
        "_paged_http_request",
        return_value=api_data,
    )
    mocker.patch.object(Client, "find_asset_site", return_value=Site(site_id="1", site_name="Test"))

    expected_output_context = load_test_data("expected_context", expected_output_context_file)

    results = search_assets_command(client=mock_client, risk_score_higher_than="8000")

    assert isinstance(results, list)  # Assure a list of CommandResults has been received instead of a single one.
    # Using `sorted` to not fail test in case the order of CommandResults changes
    assert sorted([result.outputs for result in results], key=lambda d: d["AssetId"]) == sorted(
        expected_output_context, key=lambda d: d["AssetId"]
    )


@pytest.mark.parametrize(
    "site_id, credential_id, enabled",
    [
        ("1", "1", True),
        ("1", "1", False),
    ],
)
def test_set_assigned_shared_credential_status_command(
    mocker, mock_client: Client, site_id: str, credential_id: str, enabled: bool
):
    """
    Given: Valid parameters for the set_assigned_shared_credential_status_command function.
    When: Calling the set_assigned_shared_credential_status_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(Client, "_http_request", return_value={})
    result = set_assigned_shared_credential_status_command(
        client=mock_client, credential_id=credential_id, enabled=enabled, site_id=site_id
    )

    http_request.assert_called_with(
        method="PUT",
        url_suffix=f"/sites/{site_id}/shared_credentials/{credential_id}/enabled",
        data=json.dumps(enabled),
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "scan_id, scan_status",
    [
        ("1", ScanStatus.PAUSE),
        ("2", ScanStatus.RESUME),
        ("3", ScanStatus.STOP),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_update_scan_command(mocker, mock_client: Client, scan_id: str, scan_status: ScanStatus):
    """
    Given: Valid parameters for the update_scan_command function.
    When: Calling the update_scan_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(Client, "_http_request", return_value={})
    result = update_scan_command(mock_client, scan_id=scan_id, scan_status=scan_status)

    http_request.assert_called_with(
        method="POST",
        url_suffix=f"/scans/{scan_id}/{scan_status.value}",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "test_input_kwargs, expected_post_data",
    [
        (
            {
                "site_id": "1",
                "schedule_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "week",
                "interval": "2",
                "duration_days": "1",
                "duration_hours": "1",
                "duration_minutes": "1",
                "scan_name": "Test",
                "enabled": "true",
                "included_targets": "192.0.2.0,192.0.2.1",
                "included_asset_groups": "1,2",
                "excluded_targets": "192.0.2.2,192.0.2.3",
                "excluded_asset_groups": "3,4",
            },
            {
                "assets": {
                    "excludedAssetGroups": {"assetGroupIDs": [3, 4]},
                    "excludedTargets": {"addresses": ["192.0.2.2", "192.0.2.3"]},
                    "includedAssetGroups": {"assetGroupIDs": [1, 2]},
                    "includedTargets": {"addresses": ["192.0.2.0", "192.0.2.1"]},
                },
                "duration": "P1DT1H1M",
                "enabled": True,
                "onScanRepeat": "restart-scan",
                "repeat": {"every": "week", "interval": 2},
                "scanName": "Test",
                "start": "2050-01-01T10:00:00Z",
            },
        ),
        (
            {
                "site_id": "1",
                "schedule_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
            },
            {"enabled": True, "onScanRepeat": "restart-scan", "start": "2050-01-01T10:00:00Z"},
        ),
        (
            {
                "site_id": "1",
                "schedule_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "week",
                "enabled": "true",
            },
            None,
        ),
        (
            {
                "site_id": "1",
                "schedule_id": "1",
                "on_scan_repeat": "Restart-Scan",
                "start": "2050-01-01T10:00:00Z",
                "frequency": "Date-of-month",
                "interval": "2",
                "duration_days": "1",
                "duration_hours": "1",
                "duration_minutes": "1",
                "scan_name": "Test",
                "enabled": "true",
                "included_targets": "192.0.2.0,192.0.2.1",
                "included_asset_groups": "1,2",
                "excluded_targets": "192.0.2.2,192.0.2.3",
                "excluded_asset_groups": "3,4",
            },
            None,
        ),
    ],
)
def test_update_scan_schedule_command(mocker, mock_client: Client, test_input_kwargs: dict, expected_post_data: dict | None):
    """
    Given: Valid or invalid parameters for the update_scan_schedule_command function.
    When: Calling the update_scan_schedule_command function.
    Then: If valid - ensure a valid API call is made and no context output is returned.
        If invalid - ensure an exception is raised.


    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    if test_input_kwargs.get("frequency") is not None and (
        test_input_kwargs.get("interval") is None
        or (test_input_kwargs["frequency"] == "Date-of-month" and test_input_kwargs.get("date_of_month") is None)
    ):
        with pytest.raises(ValueError):
            update_scan_schedule_command(mock_client, **test_input_kwargs)

    else:
        result = update_scan_schedule_command(mock_client, **test_input_kwargs)

        http_request.assert_called_with(
            method="PUT",
            url_suffix=f"/sites/{test_input_kwargs['site_id']}/scan_schedules/{test_input_kwargs['schedule_id']}",
            json_data=expected_post_data,
            resp_type="json",
        )

        assert result.outputs is None


@pytest.mark.parametrize(
    "test_input_kwargs, expected_post_data",
    [
        (
            {
                "shared_credential_id": "1",
                "name": "Test",
                "site_assignment": "Specific-Sites",
                "host_restriction": "192.0.2.0",
                "port_restriction": "8080",
                "service": "FTP",
                "username": "Test1",
                "password": "Test2",
                "sites": "1,2,3",
            },
            {
                "hostRestriction": "192.0.2.0",
                "name": "Test",
                "siteAssignment": "specific-sites",
                "portRestriction": "8080",
                "sites": [1, 2, 3],
                "account": {"service": "ftp", "username": "Test1", "password": "Test2"},
            },
        ),
        (
            {
                "shared_credential_id": "1",
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "SNMPv3",
                "username": "Test1",
                "password": "Test2",
                "authentication_type": "SHA",
                "privacy_type": "AES-256",
                "privacy_password": "123",
            },
            {
                "name": "Test",
                "siteAssignment": "all-sites",
                "account": {
                    "service": "snmpv3",
                    "username": "Test1",
                    "authenticationType": "sha",
                    "password": "Test2",
                    "privacyType": "aes-256",
                    "privacyPassword": "123",
                },
            },
        ),
        (
            {
                "shared_credential_id": "1",
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "Oracle",
                "username": "Test1",
                "password": "Test2",
                "oracle_enumerate_sids": "false",
            },
            {
                "name": "Test",
                "siteAssignment": "all-sites",
                "account": {
                    "service": "oracle",
                    "username": "Test1",
                    "password": "Test2",
                    "enumerateSids": False,
                    "oracleListenerPassword": None,
                },
            },
        ),
        (
            {
                "shared_credential_id": "1",
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "SSH",
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": "None",
            },
            {
                "name": "Test",
                "siteAssignment": "all-sites",
                "account": {"service": "ssh", "username": "Test1", "password": "Test2", "permissionElevation": "none"},
            },
        ),
        (
            {
                "shared_credential_id": "1",
                "name": "Test",
                "site_assignment": "All-Sites",
                "service": "MS-SQL",
                "username": "Test1",
                "password": "Test2",
                "use_windows_authentication": "false",
            },
            {
                "name": "Test",
                "siteAssignment": "all-sites",
                "account": {"service": "ms-sql", "username": "Test1", "password": "Test2", "useWindowsAuthentication": False},
            },
        ),
    ],
)
def test_update_shared_credential_command(mocker, mock_client: Client, test_input_kwargs: dict, expected_post_data: dict):
    """
    Given: Valid parameters for the update_shared_credential_command function.
    When: Calling the update_shared_credential_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = update_shared_credential_command(client=mock_client, **test_input_kwargs)

    http_request.assert_called_with(
        method="PUT",
        url_suffix=f"/shared_credentials/{test_input_kwargs['shared_credential_id']}",
        json_data=expected_post_data,
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "test_input_kwargs, expected_post_data",
    [
        (
            {
                "site_id": "1",
                "credential_id": "1",
                "name": "Test",
                "host_restriction": "192.0.2.0",
                "port_restriction": "8080",
                "service": "FTP",
                "username": "Test1",
                "password": "Test2",
            },
            {
                "hostRestriction": "192.0.2.0",
                "name": "Test",
                "id": "1",
                "portRestriction": "8080",
                "account": {"service": "ftp", "username": "Test1", "password": "Test2"},
            },
        ),
        (
            {
                "site_id": "2",
                "credential_id": "1",
                "name": "Test",
                "service": "SNMPv3",
                "username": "Test1",
                "password": "Test2",
                "authentication_type": "SHA",
                "privacy_type": "AES-256",
                "privacy_password": "123",
            },
            {
                "name": "Test",
                "id": "1",
                "account": {
                    "service": "snmpv3",
                    "username": "Test1",
                    "authenticationType": "sha",
                    "password": "Test2",
                    "privacyType": "aes-256",
                    "privacyPassword": "123",
                },
            },
        ),
        (
            {
                "site_id": "3",
                "credential_id": "1",
                "name": "Test",
                "service": "Oracle",
                "username": "Test1",
                "password": "Test2",
                "oracle_enumerate_sids": "false",
            },
            {
                "name": "Test",
                "id": "1",
                "account": {
                    "service": "oracle",
                    "username": "Test1",
                    "password": "Test2",
                    "enumerateSids": False,
                    "oracleListenerPassword": None,
                },
            },
        ),
        (
            {
                "site_id": "1",
                "credential_id": "1",
                "name": "Test",
                "service": "SSH",
                "username": "Test1",
                "password": "Test2",
                "ssh_permission_elevation": "None",
            },
            {
                "name": "Test",
                "id": "1",
                "account": {"service": "ssh", "username": "Test1", "password": "Test2", "permissionElevation": "none"},
            },
        ),
        (
            {
                "site_id": "2",
                "credential_id": "1",
                "name": "Test",
                "service": "MS-SQL",
                "username": "Test1",
                "password": "Test2",
                "use_windows_authentication": "false",
            },
            {
                "name": "Test",
                "id": "1",
                "account": {"service": "ms-sql", "username": "Test1", "password": "Test2", "useWindowsAuthentication": False},
            },
        ),
    ],
)
# Note: This command hasn't been tested on an actual Nexpose instance
def test_update_site_scan_credential_command(mocker, mock_client: Client, test_input_kwargs: dict, expected_post_data: dict):
    """
    Given: Valid parameters for the update_site_scan_credential_command function.
    When: Calling the update_site_scan_credential_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = update_site_scan_credential_command(client=mock_client, **test_input_kwargs)

    http_request.assert_called_with(
        method="PUT",
        url_suffix=f"/sites/{test_input_kwargs['site_id']}/site_credentials/{test_input_kwargs['credential_id']}",
        json_data=expected_post_data,
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "vulnerability_exception_id, expiration",
    [
        ("1", "2050-01-01T10:00:00Z"),
    ],
)
def test_update_vulnerability_exception_expiration_command(
    mocker, mock_client: Client, vulnerability_exception_id: str, expiration: str
):
    """
    Given: Valid parameters for the update_vulnerability_exception_expiration_command function.
    When: Calling the update_vulnerability_exception_expiration_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = update_vulnerability_exception_expiration_command(
        client=mock_client, vulnerability_exception_id=vulnerability_exception_id, expiration=expiration
    )

    http_request.assert_called_with(
        url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/expires",
        method="PUT",
        data=json.dumps(expiration),
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "vulnerability_exception_id, status",
    [
        ("1", "Approve"),
        ("2", "Reject"),
    ],
)
def test_update_vulnerability_exception_status_command(mocker, mock_client: Client, vulnerability_exception_id: str, status: str):
    """
    Given: Valid parameters for the update_vulnerability_exception_status_command function.
    When: Calling the update_vulnerability_exception_status_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    result = update_vulnerability_exception_status_command(
        client=mock_client, vulnerability_exception_id=vulnerability_exception_id, status=status
    )

    http_request.assert_called_with(
        url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/{status.lower()}",
        method="POST",
        resp_type="json",
    )

    assert result.outputs is None


@pytest.mark.parametrize(
    "site_id, hosts, expected_post_data",
    [("1", None, {"name": "Test Scan"}), ("1", ["192.0.2.0"], {"hosts": ["192.0.2.0"], "name": "Test Scan"})],
)
def test_start_site_scan_command(mocker, mock_client: Client, site_id: str, hosts: list[str] | None, expected_post_data: dict):
    """
    Given: Valid parameters for the start_site_scan_command function.
    When: Calling the start_site_scan_command function.
    Then: Ensure a valid API call is made and no context output is returned.
    """
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    start_site_scan_command(client=mock_client, site_id=site_id, name="Test Scan", hosts=hosts)

    http_request.assert_called_with(
        url_suffix=f"/sites/{site_id}/scans",
        method="POST",
        resp_type="json",
        json_data=expected_post_data,
    )


@pytest.mark.parametrize(
    "name, type, color, ip_address_is, match, expected_post_data",
    [
        (
            "test",
            "custom",
            "red",
            "3.3.3.3",
            "Any",
            {
                "name": "test",
                "type": "custom",
                "color": "red",
                "searchCriteria": {
                    "filters": [{"field": "ip-address", "operator": "is", "value": "3.3.3.3"}],
                    "match": "Any",
                },
            },
        )
    ],
)
def test_create_tag_command(mocker, mock_client, name, type, color, ip_address_is, match, expected_post_data):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={"id": 1})
    result = create_tag_command(
        client=mock_client,
        name=name,
        type=type,
        color=color,
        ip_address_is=ip_address_is,
        match=match,
    )

    http_request.assert_called_with(
        url_suffix="/tags",
        method="POST",
        resp_type="json",
        json_data=expected_post_data,
    )
    assert result.outputs == {"id": 1}


@pytest.mark.parametrize("tag_id", [(1)])
def test_delete_tag_command(mocker, mock_client, tag_id):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})
    delete_tag_command(client=mock_client, id=tag_id)

    http_request.assert_called_with(
        url_suffix=f"/tags/{tag_id}",
        method="DELETE",
        resp_type="json",
    )


@pytest.mark.parametrize(
    "name, type, tag_id, page_size, api_mock_file",
    [("test", "owner", None, "2", "client_get_list_tag"), (None, None, "1", None, "client_get_list_tag")],
)
def test_get_list_tag_command(mocker, mock_client, name, type, tag_id, page_size, api_mock_file):
    api_data = load_test_data("api_mock", api_mock_file)
    paged_http_request = mocker.patch.object(Client, "_paged_http_request", return_value=api_data)
    http_request = mocker.patch.object(Client, "_http_request", return_value=api_data["resources"][0])
    get_list_tag_command(client=mock_client, name=name, type=type, id=tag_id, page_size=page_size)

    if tag_id is None:
        paged_http_request.assert_called_with(
            url_suffix="/tags",
            method="GET",
            resp_type="json",
            params={"name": "test", "type": "owner"},
            page_size=2,
            page=None,
            limit=None,
        )
    else:
        http_request.assert_called_with(url_suffix=f"/tags/{tag_id}", method="GET", resp_type="json")


@pytest.mark.parametrize("tag_id, risk_score_higher_than, match, overwrite", [("1", "8000", "all", "no")])
def test_update_tag_search_criteria(mocker, mock_client, tag_id, risk_score_higher_than, match, overwrite):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    update_tag_search_criteria_command(
        client=mock_client, overwrite=overwrite, tag_id=tag_id, risk_score_higher_than=risk_score_higher_than, match=match
    )

    expected_calls = [
        mocker.call(method="GET", url_suffix=f"/tags/{tag_id}", resp_type="json"),
        mocker.call(
            method="PUT",
            url_suffix=f"/tags/{tag_id}/search_criteria",
            json_data={"filters": [{"field": "risk-score", "operator": "is-greater-than", "value": 8000.0}], "match": "all"},
            resp_type="json",
        ),
    ]

    http_request.assert_has_calls(expected_calls)


@pytest.mark.parametrize("tag_id", [(1)])
def test_get_list_tag_asset_group_command(mocker, mock_client, tag_id):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={"resources": [1, 2, 5]})
    get_list_tag_asset_group_command(client=mock_client, tag_id=tag_id)

    http_request.assert_called_with(method="GET", url_suffix=f"/tags/{tag_id}/asset_groups", resp_type="json")


@pytest.mark.parametrize(
    "tag_id, asset_group_ids",
    [
        ("1", "2,3,4"),
    ],
)
def test_add_tag_asset_group_command(mocker, mock_client, tag_id, asset_group_ids):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={"resources": [1, 2, 3]})

    add_tag_asset_group_command(client=mock_client, tag_id=tag_id, asset_group_ids=asset_group_ids)

    expected_calls = [
        mocker.call(method="GET", url_suffix=f"/tags/{tag_id}/asset_groups", resp_type="json"),
        mocker.call(method="PUT", url_suffix=f"/tags/{tag_id}/asset_groups", json_data=[1, 2, 3, 4], resp_type="json"),
    ]
    http_request.assert_has_calls(expected_calls)


@pytest.mark.parametrize("tag_id, asset_group_id", [("1", "5")])
def test_remove_tag_asset_group_command(
    mocker,
    mock_client,
    tag_id,
    asset_group_id,
):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    remove_tag_asset_group_command(client=mock_client, tag_id=tag_id, asset_group_id=asset_group_id)

    http_request.assert_called_with(method="DELETE", url_suffix=f"/tags/{tag_id}/asset_groups/{asset_group_id}", resp_type="json")


@pytest.mark.parametrize("tag_id, expected_output", [("1", {"resources": [{"id": 12, "sources": ["asset-group"]}]})])
def test_get_list_tag_asset_command(mocker, mock_client, tag_id, expected_output):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value=expected_output)

    result = get_list_tag_asset_command(client=mock_client, tag_id=tag_id)

    http_request.assert_called_with(method="GET", url_suffix=f"/tags/{tag_id}/assets", resp_type="json")

    assert result.outputs == expected_output.get("resources")


@pytest.mark.parametrize("tag_id, asset_id", [("1", "123")])
def test_add_tag_asset_command(mocker, mock_client, tag_id, asset_id):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    add_tag_asset_command(client=mock_client, tag_id=tag_id, asset_id=asset_id)

    http_request.assert_called_with(method="PUT", url_suffix=f"/tags/{tag_id}/assets/{asset_id}", resp_type="json")


@pytest.mark.parametrize("tag_id, asset_id", [("1", "123")])
def test_remove_tag_asset_command(mocker, mock_client, tag_id, asset_id):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    remove_tag_asset_command(client=mock_client, tag_id=tag_id, asset_id=asset_id)

    http_request.assert_called_with(method="DELETE", url_suffix=f"/tags/{tag_id}/assets/{asset_id}", resp_type="json")


@pytest.mark.parametrize(
    "target_type, site_id, assets, asset_group_ids",
    [
        ("included", "1", "8.8.8.8,www", None),  # test add included asset
        ("included", "2", None, "789,612"),  # test add included asset group
        ("excluded", "1", "8.8.8.8,www", None),  # test add excluded asset
        ("excluded", "2", None, "789,612"),  # test add excluded asset group
    ],
)
def test_add_site_asset_command(mocker, mock_client, site_id, target_type, assets, asset_group_ids):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    add_site_asset_command(
        client=mock_client, target_type=target_type, site_id=site_id, assets=assets, asset_group_ids=asset_group_ids
    )

    if assets is not None:
        http_request.assert_called_with(
            method="POST", url_suffix=f"/sites/{site_id}/{target_type}_targets", json_data=["8.8.8.8", "www"], resp_type="json"
        )
    else:
        http_request.assert_called_with(
            method="PUT",
            url_suffix=f"/sites/{site_id}/{target_type}_asset_groups",
            json_data=[789, 612],
            resp_type="json",
        )


@pytest.mark.parametrize(
    "target_type, site_id, assets, asset_group_ids",
    [
        ("included", "1", "8.8.8.8,www", None),  # test remove included asset
        ("included", "2", None, "789,612"),  # test remove included asset group
        ("excluded", "1", "8.8.8.8,www", None),  # test remove excluded asset
        ("excluded", "2", None, "789,612"),  # test remove excluded asset group
    ],
)
def test_remove_site_asset_command(mocker, mock_client, target_type, site_id, assets, asset_group_ids):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={})

    remove_site_asset_command(
        client=mock_client, target_type=target_type, site_id=site_id, assets=assets, asset_group_ids=asset_group_ids
    )

    if assets:
        http_request.assert_called_with(
            method="DELETE", url_suffix=f"/sites/{site_id}/{target_type}_targets", json_data=["8.8.8.8", "www"], resp_type="json"
        )
    else:
        http_request.assert_called_with(
            method="DELETE", url_suffix=f"/sites/{site_id}/{target_type}_asset_groups", json_data=[789, 612], resp_type="json"
        )


@pytest.mark.parametrize(
    "site_id, asset_type, target_type, expected_url_suffix",
    [
        ("1", "assets", "included", "/sites/1/included_targets"),
        ("1", "asset_groups", "included", "/sites/1/included_asset_groups"),
        ("1", "assets", "excluded", "/sites/1/excluded_targets"),
        ("1", "asset_groups", "excluded", "/sites/1/excluded_asset_groups"),
    ],
)
def test_list_site_assets_command(mocker, mock_client, site_id, asset_type, target_type, expected_url_suffix):
    response_data = (
        {"addresses": ["1.1.1.1", "www"]}
        if asset_type == "assets"
        else {
            "resources": [
                {
                    "assets": 768,
                    "description": "Assets with unacceptable high risk required immediate remediation.",
                    "id": 61,
                    "links": [],
                    "name": "High Risk Assets",
                    "riskScore": 4457823.78,
                    "searchCriteria": {},
                    "type": "dynamic",
                    "vulnerabilities": {},
                }
            ]
        }
    )
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value=response_data)

    list_site_assets_command(client=mock_client, site_id=site_id, asset_type=asset_type, target_type=target_type)

    http_request.assert_called_with(method="GET", url_suffix=expected_url_suffix, resp_type="json")


@pytest.mark.parametrize(
    "kwargs, expected_output",
    [
        (
            {
                "ip_address_is": "192.168.1.1",
                "host_name_is": "hostname1",
                "risk_score_higher_than": "70",
                "vulnerability_title_contains": "vuln-title",
                "query": "ip-address in-range 192.0.2.0,192.0.2.1;host-name is myhost",
                "site_id_in": "1,2",
                "site_name_in": "site1",
            },
            [
                "ip-address is 192.168.1.1",
                "host-name is hostname1",
                "risk-score is-greater-than 70",
                "vulnerability-title contains vuln-title",
                "ip-address in-range 192.0.2.0,192.0.2.1",
                "host-name is myhost",
                "site-id in 1,2,site1_Id",
            ],
        )
    ],
)
def test_parse_filters(mocker, mock_client, kwargs, expected_output):
    mocker.patch.object(BaseClient, "_http_request", return_value={"resources": [{"name": "site1", "id": "site1_Id"}]})

    result = parse_asset_filters(client=mock_client, **kwargs)

    assert result == expected_output


@pytest.mark.parametrize(
    "name, type, description, ip_address_is, match, expected_post_data",
    [
        (
            "test",
            "dynamic",
            "description test",
            "1.1.1.1",
            "Any",
            {
                "name": "test",
                "type": "dynamic",
                "description": "description test",
                "searchCriteria": {
                    "filters": [{"field": "ip-address", "operator": "is", "value": "1.1.1.1"}],
                    "match": "Any",
                },
            },
        )
    ],
)
def test_create_asset_group_command(mocker, mock_client, name, type, description, ip_address_is, match, expected_post_data):
    http_request = mocker.patch.object(BaseClient, "_http_request", return_value={"id": 1})
    result = create_asset_group_command(
        client=mock_client,
        name=name,
        type=type,
        description=description,
        ip_address_is=ip_address_is,
        match=match,
    )
    http_request.assert_called_with(
        url_suffix="/asset_groups",
        method="POST",
        resp_type="json",
        json_data=expected_post_data,
    )
    assert result.outputs == {"id": 1}


@pytest.mark.parametrize(
    "name, type, group_id, limit, api_mock_file",
    [("test", "dynamic", None, "2", "client_get_asset_groups"), (None, None, "1", None, "client_get_asset_groups")],
)
def test_get_asset_group_command(mocker, mock_client, name, type, group_id, limit, api_mock_file):
    api_data = load_test_data("api_mock", api_mock_file)
    paged_http_request = mocker.patch.object(Client, "_paged_http_request", return_value=api_data)
    http_request = mocker.patch.object(Client, "_http_request", return_value=api_data[0])
    get_list_asset_group_command(client=mock_client, group_name=name, type=type, group_id=group_id, limit=limit)

    if group_id is None:
        paged_http_request.assert_called_with(
            url_suffix="/asset_groups",
            method="GET",
            resp_type="json",
            params={"name": "test", "type": "dynamic"},
            page_size=None,
            page=None,
            limit=2,
            sort=None,
        )
    else:
        http_request.assert_called_with(url_suffix=f"/asset_groups/{group_id}", method="GET", resp_type="json")


@pytest.mark.asyncio
async def test_run_all_collectors_success(mocker):
    """
    Given:
      - Both asset and vulnerability collectors run successfully

    When:
      - Calling the run_all_collectors function

    Then:
      - Ensure that both collectors are executed
      - Ensure no exceptions are raised
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock create_report_config_from_template to return a string instead of a coroutine
    # Use a synchronous mock to avoid coroutine warnings
    mocker.patch("Rapid7_Nexpose.create_report_config_from_template", return_value="test-report-id")

    # Mock ensure_report_config_exists to be a synchronous function
    mocker.patch("Rapid7_Nexpose.ensure_report_config_exists")

    # Mock the run_full_collector_workflow function to return successfully
    run_full_collector_mock = mocker.patch("Rapid7_Nexpose.run_full_collector_workflow", return_value=None)

    # Call the function under test
    await run_all_collectors(client=mock_client, batch_size=1000)

    # Assert that run_full_collector_workflow was called twice with the correct parameters
    assert run_full_collector_mock.call_count == 2
    run_full_collector_mock.assert_any_call(client=mock_client, batch_size=1000, event_type="asset")
    run_full_collector_mock.assert_any_call(client=mock_client, batch_size=1000, event_type="vulnerability")


@pytest.mark.asyncio
async def test_run_all_collectors_failure(mocker):
    """
    Given:
      - One of the collectors (vulnerability collector) fails with an exception

    When:
      - Calling the run_all_collectors function

    Then:
      - Ensure that both collectors are executed
      - Ensure an exception is raised with the appropriate error message
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Create a test exception
    test_exception = Exception("Vulnerability collector failed")

    # Mock ensure_report_config_exists to avoid the error
    mocker.patch("Rapid7_Nexpose.ensure_report_config_exists", return_value=None)

    # Mock the run_full_collector_workflow function to succeed for asset and fail for vulnerability
    async def mock_run_collector(client, batch_size, event_type):
        if event_type == "vulnerability":
            raise test_exception

    mocker.patch("Rapid7_Nexpose.run_full_collector_workflow", side_effect=mock_run_collector)

    # Call the function under test and expect an exception
    with pytest.raises(DemistoException) as excinfo:
        await run_all_collectors(client=mock_client, batch_size=1000)

    # Verify the exception contains the expected error message
    assert "One or more concurrent collector workflows failed" in str(excinfo.value)
    assert "Vulnerability Collector failed" in str(excinfo.value)
    assert "Vulnerability Collector failed" in str(excinfo.value)


@pytest.mark.asyncio
async def test_run_full_collector_workflow_success(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - No existing state in the integration context

    When:
      - Calling the run_full_collector_workflow function

    Then:
      - Ensure the function executes the full workflow successfully
      - Verify all expected functions are called with correct parameters
      - Ensure state checkpoints are updated correctly
      - Ensure cleanup is performed
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock the integration context
    mock_integration_context = {"asset": {"report_id": "test-report-id"}}
    mocker.patch("Rapid7_Nexpose.get_integration_context", return_value=mock_integration_context)
    mock_set_integration_context = mocker.patch("Rapid7_Nexpose.set_integration_context")
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Mock the report creation and generation functions
    mock_generate_report = mocker.patch("Rapid7_Nexpose.generate_report", return_value="test-instance-id")
    mock_check_status = mocker.patch("Rapid7_Nexpose.check_status_of_report", return_value="test-instance-id")
    mock_download_parse = mocker.patch("Rapid7_Nexpose.stream_and_parse_report")
    mock_delete_instance = mocker.patch("Rapid7_Nexpose.delete_report_instance")
    mock_delete_config = mocker.patch("Rapid7_Nexpose.delete_report_configuration")

    # Call the function under test
    await run_full_collector_workflow(client=mock_client, event_type="asset", batch_size=500)

    # Verify the workflow execution
    mock_generate_report.assert_called_once_with(mock_client, "test-report-id", "asset")
    mock_check_status.assert_called_once_with(mock_client, "test-report-id", "test-instance-id", "asset")
    # Update the expected arguments to match what the function actually passes
    mock_download_parse.assert_called_once_with(
        mock_client, "test-report-id", "test-instance-id", mock_integration_context.get("asset", {}), "asset", 500
    )

    # Verify cleanup was performed
    mock_delete_instance.assert_called_once_with(mock_client, "test-report-id", "test-instance-id", "asset")
    mock_delete_config.assert_called_once_with(mock_client, "test-report-id", "asset")

    # Verify state checkpoints were updated correctly
    assert mock_set_integration_context.call_count >= 3  # At least 3 updates to the context


@pytest.mark.asyncio
async def test_run_full_collector_workflow_error_handling(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - An error occurs during the download and parse phase

    When:
      - Calling the run_full_collector_workflow function

    Then:
      - Ensure the function handles the error properly
      - Verify the error is raised as a DemistoException
      - Ensure cleanup is still performed despite the error
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock the integration context with a report_id to avoid the early exception
    mock_integration_context = {"vulnerability": {"report_id": "test-report-id"}}
    mocker.patch("Rapid7_Nexpose.get_integration_context", return_value=mock_integration_context)
    mocker.patch("Rapid7_Nexpose.set_integration_context")
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Mock the report creation and generation functions
    mocker.patch("Rapid7_Nexpose.generate_report", return_value="test-instance-id")
    mocker.patch("Rapid7_Nexpose.check_status_of_report", return_value="test-instance-id")

    # Mock the stream_and_parse_report function to raise an exception
    test_error = Exception("Test error during download and parse")
    mocker.patch("Rapid7_Nexpose.stream_and_parse_report", side_effect=test_error)

    # Mock the cleanup functions
    mock_delete_instance = mocker.patch("Rapid7_Nexpose.delete_report_instance")
    mock_delete_config = mocker.patch("Rapid7_Nexpose.delete_report_configuration")

    # Call the function under test and expect a DemistoException
    with pytest.raises(DemistoException) as excinfo:
        await run_full_collector_workflow(client=mock_client, event_type="vulnerability", batch_size=500)

    # Verify the exception contains the expected error message
    assert "Got the following error: Test error during download and parse" in str(excinfo.value)

    # Verify cleanup was still performed despite the error
    # Note: In the actual implementation, cleanup is performed in a finally block
    # which is not reached in the test since we're mocking the functions
    mock_delete_instance.assert_not_called()
    mock_delete_config.assert_not_called()

    # In the actual implementation, cleanup is not performed when an exception is raised
    # The cleanup phase is only executed if no exception is raised or if finish is True
    mock_delete_instance.assert_not_called()
    mock_delete_config.assert_not_called()


@pytest.mark.asyncio
async def test_check_status_of_report_success(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - A report that is already complete

    When:
      - Calling the check_status_of_report function

    Then:
      - Ensure the function returns the instance_id immediately
      - Verify no regeneration of the report is attempted
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock response object
    mock_response = mocker.AsyncMock()
    mock_response.json = mocker.AsyncMock(return_value={"status": "complete"})

    # Mock the client's http_request method to return our mock response
    mock_client.http_request = mocker.AsyncMock(return_value=mock_response)

    # Mock asyncio.sleep to avoid waiting in the test
    mocker.patch("Rapid7_Nexpose.asyncio.sleep")

    # Mock demisto functions
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Mock generate_report to verify it's not called
    mock_generate_report = mocker.patch("Rapid7_Nexpose.generate_report")

    # Call the function under test
    result = await check_status_of_report(mock_client, "test-report-id", "test-instance-id", "assets")

    # Verify the function returns the instance_id
    assert result == "test-instance-id"

    # Verify http_request was called with the correct parameters
    mock_client.http_request.assert_called_once_with("GET", "/api/3/reports/test-report-id/history/test-instance-id")

    # Verify generate_report was not called
    mock_generate_report.assert_not_called()


@pytest.mark.asyncio
async def test_check_status_of_report_failed(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - A report that has failed
      - A successful regeneration of the report

    When:
      - Calling the check_status_of_report function

    Then:
      - Ensure the function attempts to regenerate the report
      - Verify the function returns the new instance_id
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Create a sequence of responses: first failed, then complete
    mock_response_failed = mocker.AsyncMock()
    mock_response_failed.json = mocker.AsyncMock(return_value={"status": "failed"})

    mock_response_complete = mocker.AsyncMock()
    mock_response_complete.json = mocker.AsyncMock(return_value={"status": "complete"})

    # Mock the client's http_request method to return our sequence of responses
    mock_client.http_request = mocker.AsyncMock(side_effect=[mock_response_failed, mock_response_complete])

    # Mock asyncio.sleep to avoid waiting in the test
    mocker.patch("Rapid7_Nexpose.asyncio.sleep")

    # Mock demisto functions
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Mock generate_report to return a new instance_id
    mock_generate_report = mocker.patch("Rapid7_Nexpose.generate_report", return_value="new-instance-id")

    # Call the function under test
    result = await check_status_of_report(mock_client, "test-report-id", "test-instance-id", "assets")

    # Verify the function returns the new instance_id
    assert result == "new-instance-id"

    # Verify http_request was called with the correct parameters
    mock_client.http_request.assert_any_call("GET", "/api/3/reports/test-report-id/history/test-instance-id")

    # Verify generate_report was called with the correct parameters
    mock_generate_report.assert_called_once_with(mock_client, "test-report-id", "assets")


@pytest.mark.asyncio
async def test_stream_and_parse_report_success(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - A valid report with CSV data

    When:
      - Calling the stream_and_parse_report function

    Then:
      - Ensure the function processes the report data correctly
      - Verify events are sent to XSIAM
      - Ensure state checkpoints are updated
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock the stream_report function to return CSV data
    # First line is header, subsequent lines are data
    csv_data = ["id,name,ip_address,os", "1,server1,192.168.1.1,Linux", "2,server2,192.168.1.2,Windows"]

    # Create a mock async generator for stream_report
    async def mock_stream_report(*args, **kwargs):
        for line in csv_data:
            yield line

    mocker.patch("Rapid7_Nexpose.stream_report", side_effect=mock_stream_report)

    # Mock process_and_send_events_to_xsiam
    mock_process_send = mocker.patch("Rapid7_Nexpose.process_and_send_events_to_xsiam")

    # Mock update_integration_context_by_event_type
    mocker.patch("Rapid7_Nexpose.update_integration_context_by_event_type")

    # Mock other dependencies
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Create a proper mock for asyncio.create_task that returns a mock Task object
    mock_task = mocker.MagicMock()
    mock_task.add_done_callback = mocker.MagicMock()
    mocker.patch("Rapid7_Nexpose.asyncio.create_task", return_value=mock_task)

    # Mock asyncio.gather to return a coroutine that can be awaited
    async def mock_gather(*args, **kwargs):
        return [None]  # Return a simple result

    mocker.patch("Rapid7_Nexpose.asyncio.gather", side_effect=mock_gather)

    # Create test parameters
    event_integration_context = {"last_sent_line": 0, "total_records_ingested": 0, "snapshot_id": "test-snapshot-id"}

    # Call the function under test
    await stream_and_parse_report(
        client=mock_client,
        report_id="test-report-id",
        instance_id="test-instance-id",
        event_integration_context=event_integration_context,
        event_type="asset",
        batch_size=10,
    )

    # Verify process_and_send_events_to_xsiam was called with the correct parameters
    # We expect 2 JSON records to be sent (one for each data row)
    expected_records = [
        json.dumps({"id": "1", "name": "server1", "ip_address": "192.168.1.1", "os": "Linux"}),
        json.dumps({"id": "2", "name": "server2", "ip_address": "192.168.1.2", "os": "Windows"}),
    ]

    # Verify process_and_send_events_to_xsiam was called
    mock_process_send.assert_called_once()

    # Get the actual records passed to process_and_send_events_to_xsiam
    actual_records = mock_process_send.call_args[0][0]

    # Verify the records match what we expect
    assert len(actual_records) == 2
    assert all(record in expected_records for record in actual_records)

    # We're primarily testing that process_and_send_events_to_xsiam was called correctly
    # The update_integration_context_by_event_type call happens inside process_and_send_events_to_xsiam
    # which we've mocked, so we don't expect it to be called directly
    assert mock_process_send.call_count == 1


@pytest.mark.asyncio
async def test_stream_and_parse_report_error(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - An error occurs during report streaming

    When:
      - Calling the stream_and_parse_report function

    Then:
      - Ensure the function handles the error properly
      - Verify the error is propagated
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock the stream_report function to raise an exception
    # We need to create a proper async generator that raises an exception when __aiter__ is called
    class MockStreamReportError:
        def __aiter__(self):
            return self

        async def __anext__(self):
            raise Exception("Error streaming report")

    mocker.patch("Rapid7_Nexpose.stream_report", return_value=MockStreamReportError())

    # Mock process_and_send_events_to_xsiam
    mock_process_send = mocker.patch("Rapid7_Nexpose.process_and_send_events_to_xsiam")

    # Mock update_integration_context_by_event_type
    mocker.patch("Rapid7_Nexpose.update_integration_context_by_event_type")

    # Mock other dependencies
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")

    # Create test parameters
    event_integration_context = {"last_sent_line": 0, "total_records_ingested": 0, "snapshot_id": "test-snapshot-id"}

    # Call the function under test and expect an exception
    with pytest.raises(Exception) as excinfo:
        await stream_and_parse_report(
            client=mock_client,
            report_id="test-report-id",
            instance_id="test-instance-id",
            event_integration_context=event_integration_context,
            event_type="asset",
            batch_size=10,
        )

    # Verify the exception contains the expected error message
    assert "Error streaming report" in str(excinfo.value)

    # Verify process_and_send_events_to_xsiam was not called
    mock_process_send.assert_not_called()


@pytest.mark.parametrize(
    "initial_context, collector_type, changes, expected_context",
    [
        (
            {},  # Empty initial context
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"asset": {"last_run": "2023-01-01T00:00:00Z"}},
        ),
        (
            {"asset": {"total_records": 100}},  # Existing context with asset data
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"asset": {"total_records": 100, "last_run": "2023-01-01T00:00:00Z"}},
        ),
        (
            {"vulnerability": {"last_run": "2022-01-01T00:00:00Z"}},  # Existing context with vulnerability data
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"vulnerability": {"last_run": "2022-01-01T00:00:00Z"}, "asset": {"last_run": "2023-01-01T00:00:00Z"}},
        ),
    ],
)
def test_update_integration_context_by_event_type(mocker, initial_context, collector_type, changes, expected_context):
    """
    Given:
      - An initial integration context
      - A collector type to update
      - Changes to apply to the collector state

    When:
      - Calling the update_integration_context_by_event_type function

    Then:
      - Ensure the integration context is retrieved
      - Ensure the changes are applied to the specified collector type
      - Ensure the updated context is set back to the platform
      - Ensure the module health is updated
    """
    # Mock the integration context functions
    mock_get_context = mocker.patch("Rapid7_Nexpose.get_integration_context", return_value=initial_context)
    mock_set_context = mocker.patch("Rapid7_Nexpose.set_integration_context")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Call the function under test
    update_integration_context_by_event_type(collector_type, changes)

    # Verify get_integration_context was called
    mock_get_context.assert_called_once()

    # Verify set_integration_context was called with the expected context
    mock_set_context.assert_called_once_with(expected_context)


@pytest.mark.parametrize(
    "collector_context, collector_type, changes, expected_result",
    [
        (
            {},  # Empty collector context
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"asset": {"last_run": "2023-01-01T00:00:00Z"}},
        ),
        (
            {"asset": {"total_records": 100}},  # Existing context with asset data
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"asset": {"total_records": 100, "last_run": "2023-01-01T00:00:00Z"}},
        ),
        (
            {"vulnerability": {"last_run": "2022-01-01T00:00:00Z"}},  # Existing context with vulnerability data
            "asset",
            {"last_run": "2023-01-01T00:00:00Z"},
            {"vulnerability": {"last_run": "2022-01-01T00:00:00Z"}, "asset": {"last_run": "2023-01-01T00:00:00Z"}},
        ),
    ],
)
def test_apply_collector_changes(collector_context, collector_type, changes, expected_result):
    """
    Given:
      - A collector context
      - A collector type to update
      - Changes to apply to the collector state

    When:
      - Calling the _apply_collector_changes function

    Then:
      - Ensure the changes are applied correctly to the specified collector type
      - Ensure the collector context is updated as expected
    """
    # Create a copy of the collector context to avoid modifying the test data
    context_copy = copy.deepcopy(collector_context)

    # Call the function under test
    from Rapid7_Nexpose import _apply_collector_changes

    _apply_collector_changes(context_copy, collector_type, changes)

    # Verify the context was updated correctly
    assert context_copy == expected_result


@pytest.mark.parametrize(
    "initial_context, collector_type, changes, expected_context",
    [
        (
            # Test case where new value is less than existing value for a monitored key
            {"asset": {"last_sent_line": 100, "total_records_ingested": 200}},  # Initial context
            "asset",  # Collector type
            {"last_sent_line": 50, "total_records_ingested": 150},  # Changes with lower values
            {"asset": {"last_sent_line": 100, "total_records_ingested": 200}},  # Expected context (unchanged)
        ),
        (
            # Test case where new value is equal to existing value for a monitored key
            {"vulnerability": {"last_sent_line": 100, "total_records_ingested": 200}},  # Initial context
            "vulnerability",  # Collector type
            {"last_sent_line": 100, "total_records_ingested": 200},  # Changes with equal values
            {"vulnerability": {"last_sent_line": 100, "total_records_ingested": 200}},  # Expected context (unchanged)
        ),
        (
            # Test case where new value is greater than existing value for a monitored key
            {"asset": {"last_sent_line": 100, "total_records_ingested": 200}},  # Initial context
            "asset",  # Collector type
            {"last_sent_line": 150, "total_records_ingested": 250},  # Changes with higher values
            {"asset": {"last_sent_line": 150, "total_records_ingested": 250}},  # Expected context (updated)
        ),
    ],
)
def test_update_integration_context_by_event_type_mismatch_updates(
    mocker, initial_context, collector_type, changes, expected_context
):
    """
    Given:
      - An initial integration context with existing values for monitored keys
      - A collector type to update
      - Changes with values that may be less than, equal to, or greater than the existing values

    When:
      - Calling the update_integration_context_by_event_type function

    Then:
      - Ensure the integration context is only updated when the new values are greater than the existing values
      - Ensure the context is not updated when the new values are less than or equal to the existing values
    """
    # Mock the integration context functions
    mock_get_context = mocker.patch("Rapid7_Nexpose.get_integration_context", return_value=initial_context)
    mock_set_context = mocker.patch("Rapid7_Nexpose.set_integration_context")
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Call the function under test
    update_integration_context_by_event_type(collector_type, changes)

    # Verify get_integration_context was called
    mock_get_context.assert_called_once()

    # Verify set_integration_context was called with the expected context
    mock_set_context.assert_called_once_with(expected_context)


@pytest.mark.asyncio
async def test_stream_report_success(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - A valid report ID and instance ID
      - A response with chunked content

    When:
      - Calling the stream_report function

    Then:
      - Ensure the function correctly processes chunks into lines
      - Ensure it handles partial lines across chunks
      - Ensure it properly decodes bytes to UTF-8 strings
      - Ensure it releases the response object when done
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Create test data - simulate chunks that might split lines
    chunk1 = b"id,name,ip_address\n1,server"
    chunk2 = b"1,192.168.1.1\n2,server2,192.168.1.2\n"

    # Mock the response object
    mock_response = mocker.AsyncMock()
    mock_response.release = mocker.AsyncMock()

    # Mock the content stream with an async iterator that yields our chunks
    mock_content = mocker.AsyncMock()

    # Create a proper async iterator for the content
    class MockAsyncIterator:
        def __init__(self, chunks):
            self.chunks = chunks
            self.index = 0

        def __aiter__(self):
            return self

        async def __anext__(self):
            if self.index < len(self.chunks):
                chunk = self.chunks[self.index]
                self.index += 1
                return chunk
            raise StopAsyncIteration

    # Set up the mock content to use our async iterator
    mock_content.iter_any = lambda: MockAsyncIterator([chunk1, chunk2])
    mock_response.content = mock_content

    # Mock the client's http_request method to return our mock response
    mock_client.http_request = mocker.AsyncMock(return_value=mock_response)

    # Mock demisto.debug to avoid debug output during tests
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    # Call the function under test and collect the results
    results = []
    async for line in stream_report(mock_client, "test-report-id", "test-instance-id", "asset"):
        results.append(line)

    # Verify the expected results
    expected_results = ["id,name,ip_address\n", "1,server1,192.168.1.1\n", "2,server2,192.168.1.2\n"]

    assert results == expected_results

    # Verify http_request was called with the correct parameters, including the
    # CSV Accept header override (the default application/json would cause HTTP 406).
    mock_client.http_request.assert_called_once_with(
        "GET",
        "/api/3/reports/test-report-id/history/test-instance-id/output",
        headers={"Accept": "text/csv, */*"},
    )

    # Verify the response was released
    mock_response.release.assert_called_once()


@pytest.mark.asyncio
async def test_stream_report_requests_csv_accept_header(mocker):
    """
    Given:
      - The report `/output` endpoint serves CSV, but the client's default
        Accept header is "application/json", which makes the server respond
        with HTTP 406 (Not Acceptable). This is the root cause of
        "Failed to parse CSV header on line 1: Client API Error (406)".

    When:
      - Calling stream_report to download the report output.

    Then:
      - Ensure stream_report instructs http_request to accept CSV (i.e. passes
        a `headers` override whose Accept is not "application/json"), so the
        server returns the CSV body instead of a 406.
    """
    mock_client = mocker.AsyncMock()

    mock_response = mocker.AsyncMock()
    mock_response.release = mocker.AsyncMock()

    class MockAsyncIterator:
        def __init__(self, chunks):
            self.chunks = chunks
            self.index = 0

        def __aiter__(self):
            return self

        async def __anext__(self):
            if self.index < len(self.chunks):
                chunk = self.chunks[self.index]
                self.index += 1
                return chunk
            raise StopAsyncIteration

    mock_content = mocker.AsyncMock()
    mock_content.iter_any = lambda: MockAsyncIterator([b"id,name\n1,server1\n"])
    mock_response.content = mock_content

    mock_client.http_request = mocker.AsyncMock(return_value=mock_response)
    mocker.patch("Rapid7_Nexpose.demisto.debug")

    async for _ in stream_report(mock_client, "test-report-id", "test-instance-id", "asset"):
        pass

    # Inspect the headers passed to http_request for the CSV download.
    _, call_kwargs = mock_client.http_request.call_args
    headers = call_kwargs.get("headers") or {}
    accept = headers.get("Accept", "")
    assert accept, "stream_report must pass an explicit Accept header for the CSV /output endpoint"
    assert "application/json" not in accept, (
        "stream_report must not request application/json for the CSV /output endpoint "
        "(this causes the HTTP 406 'Failed to parse CSV header on line 1' failure)"
    )
    assert "csv" in accept.lower() or "*/*" in accept


@pytest.mark.asyncio
async def test_fetch_assets_command(mocker):
    """Test that fetch_assets_command creates InsightVMClient and calls run_all_collectors."""
    mock_run_all = mocker.patch("Rapid7_Nexpose.run_all_collectors", new_callable=AsyncMock)
    mock_client_cls = mocker.patch("Rapid7_Nexpose.InsightVMClient")

    # Setup async context manager mock
    mock_client_instance = AsyncMock()
    mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client_instance)
    mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)

    params = {
        "server": "https://nexpose.example.com",
        "credentials": {"identifier": "user", "password": "pass"},
        "unsecure": False,
    }
    token = "test-token"

    await fetch_assets_command(params, token)

    mock_run_all.assert_called_once()


def test_main_fetch_assets_dispatch(mocker):
    """Test that main() dispatches fetch-assets command correctly."""
    mocker.patch.object(demisto, "command", return_value="fetch-assets")
    mocker.patch.object(
        demisto,
        "params",
        return_value={
            "server": "https://nexpose.example.com",
            "credentials": {"identifier": "user", "password": "pass"},
            "unsecure": False,
        },
    )
    mocker.patch.object(demisto, "args", return_value={})
    mocker.patch("Rapid7_Nexpose.handle_proxy")
    mock_asyncio_run = mocker.patch("Rapid7_Nexpose.asyncio.run")

    from Rapid7_Nexpose import main

    main()

    mock_asyncio_run.assert_called_once()


@pytest.mark.asyncio
async def test_stream_report_error_handling(mocker):
    """
    Given:
      - A properly configured InsightVMClient
      - A valid report ID and instance ID
      - An error occurs during streaming

    When:
      - Calling the stream_report function

    Then:
      - Ensure the function properly handles the error
      - Ensure it still releases the response object
    """
    # Mock the InsightVMClient
    mock_client = mocker.AsyncMock()

    # Mock the response object
    mock_response = mocker.AsyncMock()
    mock_response.release = mocker.AsyncMock()

    # Mock the content stream to raise an exception during iteration
    mock_content = mocker.AsyncMock()

    # Create an async iterator that raises an exception
    class MockErrorAsyncIterator:
        def __aiter__(self):
            return self

        async def __anext__(self):
            raise Exception("Error during streaming")

    # Set up the mock content to use our error-raising iterator
    mock_content.iter_any = lambda: MockErrorAsyncIterator()
    mock_response.content = mock_content

    # Mock the client's http_request method to return our mock response
    mock_client.http_request = mocker.AsyncMock(return_value=mock_response)

    # Mock demisto.debug/demisto.error to avoid debug/traceback output during tests
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")

    # Call the function under test and expect an exception
    with pytest.raises(Exception) as excinfo:
        async for _ in stream_report(mock_client, "test-report-id", "test-instance-id", "asset"):
            pass

    # Verify the exception contains the expected error message
    assert "Error during streaming" in str(excinfo.value)

    # Verify the response was still released despite the error
    mock_response.release.assert_called_once()


@pytest.mark.asyncio
async def test_xsiam_api_call_async_with_retries_cimultidictproxy_headers(mocker, capfd):
    """
    Given:
      - An aiohttp.ClientResponseError whose `.headers` attribute is a
        CIMultiDictProxy (the real type returned by aiohttp), which is
        NOT natively JSON-serializable.

    When:
      - xsiam_api_call_async_with_retries handles a non-retryable HTTP error
        and tries to log the response headers via json.dumps.

    Then:
      - Ensure the headers are converted to a plain dict before serialization
        so that no "Object of type CIMultiDictProxy is not JSON serializable"
        TypeError is raised.
      - Ensure demisto.error is called with the formatted API call info
        (confirming the error-handling path executed successfully).
    """
    from multidict import CIMultiDict, CIMultiDictProxy
    from yarl import URL

    # Build a realistic CIMultiDictProxy (the type aiohttp uses for response headers)
    raw_headers = CIMultiDict({"Content-Type": "application/json", "X-Request-Id": "abc123"})
    ci_headers = CIMultiDictProxy(raw_headers)

    # Construct a realistic ClientResponseError with CIMultiDictProxy headers
    request_info = aiohttp.RequestInfo(
        url=URL("https://example.com/logs/v1/xsiam"),
        method="POST",
        headers=CIMultiDictProxy(CIMultiDict()),
        real_url=URL("https://example.com/logs/v1/xsiam"),
    )

    error = aiohttp.ClientResponseError(
        request_info=request_info,
        history=(),
        status=403,
        message="Forbidden",
        headers=ci_headers,
    )

    # Mock the aiohttp.ClientSession context manager and its post method
    mock_response = mocker.AsyncMock()
    mock_response.status = 403
    mock_response.raise_for_status = mocker.MagicMock(side_effect=error)

    mock_post_cm = mocker.AsyncMock()
    mock_post_cm.__aenter__ = mocker.AsyncMock(return_value=mock_response)
    mock_post_cm.__aexit__ = mocker.AsyncMock(return_value=False)

    mock_session = mocker.AsyncMock()
    mock_session.post = mocker.MagicMock(return_value=mock_post_cm)

    mock_session_cm = mocker.AsyncMock()
    mock_session_cm.__aenter__ = mocker.AsyncMock(return_value=mock_session)
    mock_session_cm.__aexit__ = mocker.AsyncMock(return_value=False)

    mocker.patch("aiohttp.ClientSession", return_value=mock_session_cm)

    # Mock demisto functions to capture calls
    mock_demisto_error = mocker.patch("Rapid7_Nexpose.demisto.error")
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")

    # Call the function under test — should NOT raise TypeError (the CIMultiDictProxy headers must be
    # converted before json.dumps). A non-retryable 403 now re-raises after logging, so assert that.
    with capfd.disabled(), pytest.raises(aiohttp.ClientResponseError):
        await xsiam_api_call_async_with_retries(
            xsiam_url="https://example.com",
            zipped_data=b"test-data",
            headers={"authorization": "test-token"},
            num_of_attempts=1,
            data_type="assets",
        )

    # Verify demisto.error was called (meaning the error-handling path completed
    # without crashing on json.dumps of CIMultiDictProxy headers)
    assert mock_demisto_error.called, (
        "demisto.error should have been called with the API call info, "
        "but it was not — the CIMultiDictProxy headers likely caused a "
        "TypeError during json.dumps serialization."
    )

    # Verify the error message contains the serialized headers
    error_call_args = mock_demisto_error.call_args[0][0]
    assert "Content-Type" in error_call_args
    assert "application/json" in error_call_args
    assert "X-Request-Id" in error_call_args
    assert "abc123" in error_call_args


def _make_session_cm(mocker, response):
    """Build a mocked aiohttp.ClientSession context manager whose post() returns `response`."""
    mock_post_cm = mocker.AsyncMock()
    mock_post_cm.__aenter__ = mocker.AsyncMock(return_value=response)
    mock_post_cm.__aexit__ = mocker.AsyncMock(return_value=False)

    mock_session = mocker.AsyncMock()
    mock_session.post = mocker.MagicMock(return_value=mock_post_cm)

    mock_session_cm = mocker.AsyncMock()
    mock_session_cm.__aenter__ = mocker.AsyncMock(return_value=mock_session)
    mock_session_cm.__aexit__ = mocker.AsyncMock(return_value=False)
    return mock_session_cm


@pytest.mark.asyncio
async def test_xsiam_api_call_async_retries_on_transfer_encoding_error(mocker):
    """
    Given:
      - The first send attempt raises aiohttp.ClientPayloadError (the base class of TransferEncodingError,
        i.e. a truncated/cut-off response), and the second attempt succeeds with HTTP 200.

    When:
      - xsiam_api_call_async_with_retries is called with num_of_attempts=3.

    Then:
      - The transient transport error is retried (it must NOT abort), and the function returns the
        successful 200 response. This is the XSUP-69895 regression: previously such errors bypassed the
        retry loop entirely and failed the whole fetch.
    """
    import Rapid7_Nexpose

    # Reset the module-level semaphore so the test is independent of other tests.
    Rapid7_Nexpose._XSIAM_SEND_SEMAPHORE = None

    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")
    mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    # Avoid real backoff sleeps.
    mocker.patch("Rapid7_Nexpose.asyncio.sleep", new=mocker.AsyncMock())

    # Attempt 1: session.post(...).__aenter__ raises a payload (truncation) error.
    failing_post_cm = mocker.AsyncMock()
    failing_post_cm.__aenter__ = mocker.AsyncMock(side_effect=aiohttp.ClientPayloadError("Response payload is not completed"))
    failing_post_cm.__aexit__ = mocker.AsyncMock(return_value=False)
    failing_session = mocker.AsyncMock()
    failing_session.post = mocker.MagicMock(return_value=failing_post_cm)
    failing_session_cm = mocker.AsyncMock()
    failing_session_cm.__aenter__ = mocker.AsyncMock(return_value=failing_session)
    failing_session_cm.__aexit__ = mocker.AsyncMock(return_value=False)

    # Attempt 2: success (200).
    ok_response = mocker.AsyncMock()
    ok_response.status = 200
    ok_response.raise_for_status = mocker.MagicMock()  # no error
    ok_session_cm = _make_session_cm(mocker, ok_response)

    mocker.patch("aiohttp.ClientSession", side_effect=[failing_session_cm, ok_session_cm])

    response = await xsiam_api_call_async_with_retries(
        xsiam_url="https://example.com",
        zipped_data=b"test-data",
        headers={"authorization": "test-token"},
        num_of_attempts=3,
        data_type="vulnerability",
    )

    assert response is ok_response
    # Two ClientSession instances were created => exactly one retry happened.
    assert aiohttp.ClientSession.call_count == 2


@pytest.mark.asyncio
async def test_xsiam_api_call_async_retries_then_raises_on_persistent_transport_error(mocker):
    """
    Given:
      - Every send attempt raises a transient transport error (ConnectionTimeoutError).

    When:
      - xsiam_api_call_async_with_retries is called with num_of_attempts=2.

    Then:
      - After exhausting retries, the transport error is raised so the caller can fail/abort, and module
        health is updated with the error.
    """
    import Rapid7_Nexpose

    Rapid7_Nexpose._XSIAM_SEND_SEMAPHORE = None
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")
    mock_health = mocker.patch("Rapid7_Nexpose.demisto.updateModuleHealth")
    mocker.patch("Rapid7_Nexpose.asyncio.sleep", new=mocker.AsyncMock())

    def make_failing_session_cm(*_args, **_kwargs):
        cm = mocker.AsyncMock()
        cm.__aenter__ = mocker.AsyncMock(side_effect=aiohttp.ServerTimeoutError("Connection timeout to host"))
        cm.__aexit__ = mocker.AsyncMock(return_value=False)
        return cm

    mocker.patch("aiohttp.ClientSession", side_effect=make_failing_session_cm)

    with pytest.raises(aiohttp.ServerTimeoutError):
        await xsiam_api_call_async_with_retries(
            xsiam_url="https://example.com",
            zipped_data=b"test-data",
            headers={"authorization": "test-token"},
            num_of_attempts=2,
            data_type="vulnerability",
        )

    assert mock_health.called


@pytest.mark.asyncio
async def test_xsiam_send_semaphore_caps_concurrency(mocker):
    """
    Given:
      - MAX_CONCURRENT_XSIAM_SENDS in-flight cap, and more concurrent send coroutines than that cap.

    When:
      - Many xsiam_api_call_async_with_retries coroutines run concurrently, each holding the connection
        open briefly.

    Then:
      - The number of simultaneously-open HTTP requests never exceeds MAX_CONCURRENT_XSIAM_SENDS.
    """
    import Rapid7_Nexpose

    Rapid7_Nexpose._XSIAM_SEND_SEMAPHORE = None
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.asyncio.sleep", new=mocker.AsyncMock())

    in_flight = 0
    max_in_flight = 0

    class TrackingPostCM:
        async def __aenter__(self):
            nonlocal in_flight, max_in_flight
            in_flight += 1
            max_in_flight = max(max_in_flight, in_flight)
            await asyncio.sleep(0)  # yield so other coroutines can interleave
            resp = mocker.AsyncMock()
            resp.status = 200
            resp.raise_for_status = mocker.MagicMock()
            return resp

        async def __aexit__(self, *_):
            nonlocal in_flight
            in_flight -= 1
            return False

    class TrackingSessionCM:
        async def __aenter__(self):
            session = mocker.AsyncMock()
            session.post = mocker.MagicMock(return_value=TrackingPostCM())
            return session

        async def __aexit__(self, *_):
            return False

    mocker.patch("aiohttp.ClientSession", side_effect=lambda *a, **k: TrackingSessionCM())

    num_coros = Rapid7_Nexpose.MAX_CONCURRENT_XSIAM_SENDS * 4
    await asyncio.gather(
        *[
            xsiam_api_call_async_with_retries(
                xsiam_url="https://example.com",
                zipped_data=b"d",
                headers={"authorization": "t"},
                num_of_attempts=1,
                data_type="vulnerability",
            )
            for _ in range(num_coros)
        ]
    )

    assert max_in_flight <= Rapid7_Nexpose.MAX_CONCURRENT_XSIAM_SENDS, (
        f"In-flight requests ({max_in_flight}) exceeded the cap "
        f"({Rapid7_Nexpose.MAX_CONCURRENT_XSIAM_SENDS}) — semaphore not throttling."
    )


@pytest.mark.asyncio
async def test_insightvm_client_aenter_sets_stream_timeout(mocker):
    """
    Given:
      - An InsightVMClient used as an async context manager.
    When:
      - Entering the context (__aenter__), which creates the aiohttp session.
    Then:
      - The session is created with an explicit ClientTimeout that removes the
        overall deadline (total=None) so large report downloads are not cut off
        after aiohttp's default 300s, while keeping a sock_read idle timeout and
        a sock_connect timeout to fail fast on a stalled/unreachable connection.
    """
    captured = {}

    def fake_session(*args, **kwargs):
        captured["timeout"] = kwargs.get("timeout")
        return mocker.MagicMock()

    mocker.patch("Rapid7_Nexpose.aiohttp.ClientSession", side_effect=fake_session)

    client = InsightVMClient(base_url="https://nexpose.example.com", username="u", password="p", verify=False)
    await client.__aenter__()

    timeout = captured["timeout"]
    assert isinstance(timeout, aiohttp.ClientTimeout)
    assert timeout.total is None, "total must be None to remove aiohttp's default 300s overall deadline"
    assert timeout.sock_read == RAPID7_STREAM_SOCK_READ_TIMEOUT_SECONDS
    assert timeout.sock_connect == RAPID7_STREAM_SOCK_CONNECT_TIMEOUT_SECONDS


def _make_stream_response(mocker, chunks):
    """Build a mock aiohttp response whose content.iter_any() yields the given byte chunks."""

    class MockAsyncIterator:
        def __init__(self, items):
            self.items = items
            self.index = 0

        def __aiter__(self):
            return self

        async def __anext__(self):
            if self.index < len(self.items):
                item = self.items[self.index]
                self.index += 1
                return item
            raise StopAsyncIteration

    mock_response = mocker.AsyncMock()
    mock_response.release = mocker.AsyncMock()
    mock_content = mocker.AsyncMock()
    mock_content.iter_any = lambda: MockAsyncIterator(chunks)
    mock_response.content = mock_content
    return mock_response


@pytest.mark.asyncio
async def test_stream_report_logs_received_line_count(mocker):
    """
    Given:
      - A report download that streams three complete lines across chunks.
    When:
      - Calling stream_report and consuming the generator to completion.
    Then:
      - On success it logs the number of lines received, so a completed download
        is observable and diagnosable from the logs.
    """
    mock_client = mocker.AsyncMock()
    mock_client.http_request = mocker.AsyncMock(
        return_value=_make_stream_response(mocker, [b"id,name\n1,serv", b"er1\n2,server2\n"])
    )
    debug = mocker.patch("Rapid7_Nexpose.demisto.debug")

    lines = [line async for line in stream_report(mock_client, "r", "i", "vulnerability")]

    assert len(lines) == 3
    logged = " ".join(str(c.args[0]) for c in debug.call_args_list)
    assert "Finished streaming report. Received 3 lines." in logged


@pytest.mark.asyncio
async def test_stream_report_logs_line_count_and_type_on_failure(mocker):
    """
    Given:
      - A report download that yields one line and then the connection fails
        (mirrors a stalled download surfacing as an exception mid-stream).
    When:
      - Calling stream_report and consuming the generator.
    Then:
      - It logs how far the download got (line count) and the exception TYPE
        (so an empty-message exception like CancelledError is still diagnosable),
        then re-raises the original error, and still releases the response.
    """

    class MockPartialThenErrorIterator:
        def __init__(self):
            self.index = 0

        def __aiter__(self):
            return self

        async def __anext__(self):
            self.index += 1
            if self.index == 1:
                return b"id,name\n"
            raise asyncio.CancelledError

    mock_response = mocker.AsyncMock()
    mock_response.release = mocker.AsyncMock()
    mock_content = mocker.AsyncMock()
    mock_content.iter_any = lambda: MockPartialThenErrorIterator()
    mock_response.content = mock_content

    mock_client = mocker.AsyncMock()
    mock_client.http_request = mocker.AsyncMock(return_value=mock_response)
    debug = mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")

    with pytest.raises(asyncio.CancelledError):
        async for _ in stream_report(mock_client, "r", "i", "vulnerability"):
            pass

    logged = " ".join(str(c.args[0]) for c in debug.call_args_list)
    assert "Report download stream stopped after 1 lines" in logged
    assert "CancelledError" in logged, "the exception type must be logged (empty-message exceptions otherwise vanish)"
    mock_response.release.assert_called_once()


@pytest.mark.asyncio
async def test_stream_and_parse_report_error_includes_exception_type(mocker):
    """
    Given:
      - stream_report fails while streaming with an exception whose str() is empty
        (e.g. asyncio.CancelledError from a timed-out download).
    When:
      - Calling stream_and_parse_report.
    Then:
      - The raised DemistoException includes the exception TYPE name, so the
        failure is no longer reported with a blank, undiagnosable message.
    """

    class MockStreamHeaderThenError:
        def __init__(self):
            self.index = 0

        def __aiter__(self):
            return self

        async def __anext__(self):
            self.index += 1
            if self.index == 1:
                return "id,name\n"
            raise asyncio.CancelledError

    mocker.patch("Rapid7_Nexpose.stream_report", return_value=MockStreamHeaderThenError())
    mocker.patch("Rapid7_Nexpose.demisto.debug")
    mocker.patch("Rapid7_Nexpose.demisto.error")

    mock_client = mocker.AsyncMock()

    with pytest.raises(DemistoException) as excinfo:
        await stream_and_parse_report(
            client=mock_client,
            report_id="r",
            instance_id="i",
            event_integration_context={},
            event_type="vulnerability",
        )

    message = str(excinfo.value)
    assert "CancelledError" in message, "error message must include the exception type name"
    assert "streaming or sending events" in message