Rapid7 Nexpose
Vulnerability management solution to help reduce threat exposure.
Vulnerability Management · Rapid7 InsightVM
Details
| ID | Rapid7 Nexpose |
|---|---|
| Provider | Rapid7 |
| Category | Vulnerability Management |
| From Version | 5.0.0 |
| Docker Image | demisto/auth-utils:1.0.0.10133006 |
| Supported Modules | Agentix 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
- Navigate to Settings > Integrations > Servers & Services.
- Search for Rapid7 InsightVM.
-
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 - 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,
WebCVE-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 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 Tokenunsecure— Trust any certificate (not secure)proxy— Use system proxy settingsconnection_error_retries— Number of connection error retriesisFetchAssets— Fetch AssetsassetsFetchInterval— Assets Fetch Interval
Commands (64)
-
nexpose-add-site-excluded-assetAdd excluded assets to a site.
-
nexpose-add-site-included-assetAdd included assets to a site.
-
nexpose-add-tag-assetAdd an existing asset to an existing tag.
-
nexpose-add-tag-asset-groupAdd existing asset groups to an existing tag.
-
nexpose-create-assetCreate a new asset.
-
nexpose-create-asset-groupCreate an asset group. Note: All filters are relevant only for Dynamic asset groups.
-
nexpose-create-assets-reportGenerates a new report on given assets according to a template and arguments.
-
nexpose-create-scan-reportGenerates a new report for a specified scan.
-
nexpose-create-scan-scheduleBeta Command Create a new site scan schedule.
-
nexpose-create-shared-credentialCreate 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-siteCreates a new site with the specified configuration.
-
nexpose-create-site-scan-credentialBeta 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-reportGenerates a new report on given sites according to a template and arguments.
-
nexpose-create-tagCreate a tag.
-
nexpose-create-vulnerability-exceptionCreate a new vulnerability exception.
-
nexpose-delete-assetDelete an asset.
-
nexpose-delete-scan-scheduleDelete a scheduled scan.
-
nexpose-delete-shared-credentialBeta Command Delete a shared credential.
-
nexpose-delete-siteDeletes a site.
-
nexpose-delete-site-scan-credentialBeta Command Delete a site scan credential.
-
nexpose-delete-tagDelete a tag.
-
nexpose-delete-vulnerability-exceptionDelete a vulnerability exception.
-
nexpose-disable-shared-credentialBeta Command Disable an assigned shared credential.
-
nexpose-download-reportReturns the generated report.
-
nexpose-enable-shared-credentialBeta Command Enable an assigned shared credential.
-
nexpose-get-assetReturns the specified asset.
-
nexpose-get-asset-tagsReturns the specified tags for an asset.
-
nexpose-get-asset-vulnerabilityReturns details and possible remediations for an asset's vulnerability.
-
nexpose-get-assetsReturns all assets for which you have access.
-
nexpose-get-report-statusReturns the status of a report generation process.
-
nexpose-get-report-templatesReturns all available report templates.
-
nexpose-get-scanGet a specific scan.
-
nexpose-get-scansReturn a list of scans. Returns only active scans by default (active=true).
-
nexpose-get-sitesRetrieves accessible sites.
-
nexpose-list-asset-groupReturn a list of asset groups.
-
nexpose-list-assigned-shared-credentialRetrieve information about shared credentials for a specific site.
-
nexpose-list-scan-scheduleBetaCommand Retrieve information about scan schedules for a specific site or a specific scan schedule.
-
nexpose-list-shared-credentialRetrieve information about all or a specific shared credential.
-
nexpose-list-site-excluded-assetReturn a list of excluded assets for a site.
-
nexpose-list-site-excluded-asset-groupReturn a list of excluded asset groups for a site.
-
nexpose-list-site-included-assetReturn a list of included assets for a site.
-
nexpose-list-site-included-asset-groupReturn a list of included asset groups for a site.
-
nexpose-list-site-scan-credentialBeta Command Retrieve information about all or a specific sca credential.
-
nexpose-list-tagReturn a list of tags.
-
nexpose-list-tag-assetReturn the tag assets list.
-
nexpose-list-tag-asset-groupReturn a list of asset groups for a tag.
-
nexpose-list-vulnerabilityRetrieve information about all or a specific vulnerability.
-
nexpose-list-vulnerability-exceptionsRetrieve information about scan schedules for a specific site or a specific scan schedule.
-
nexpose-pause-scanPause a running scan.
-
nexpose-remove-site-excluded-assetRemove excluded assets from a site.
-
nexpose-remove-site-included-assetRemove included assets from a site.
-
nexpose-remove-tag-assetRemove 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-groupRemove an asset group from a tag.
-
nexpose-resume-scanResume a paused scan.
-
nexpose-search-assetsSearch and return all assets matching specific filters. Returns only assets the user has access to.
-
nexpose-start-assets-scanDeprecatedDeprecated. Use `nexpose-start-site-scan` using the `hosts` argument instead.
-
nexpose-start-site-scanStarts a scan for the specified site.
-
nexpose-stop-scanStop a running scan.
-
nexpose-update-scan-scheduleBeta Command Update an existing site scan schedule.
-
nexpose-update-shared-credentialUpdate an existing shared credential.
-
nexpose-update-site-scan-credentialBeta 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-criteriaUpdate tag search criteria.
-
nexpose-update-vulnerability-exception-expirationUpdate an existing vulnerability exception.
-
nexpose-update-vulnerability-exception-statusUpdate 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