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 csv import io import json import traceback from copy import deepcopy from enum import Enum, EnumMeta from time import strptime, struct_time from typing import overload from collections.abc import AsyncGenerator import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 import asyncio import aiohttp VENDOR_NAME = "Rapid7 Nexpose" # Vendor name to use for indicators. API_DEFAULT_PAGE_SIZE = 10 # Default page size that's set on the API. Used for calculations. DEFAULT_PAGE_SIZE = 50 # Default page size to use MATCH_DEFAULT_VALUE = "any" # Default "match" value to use when using search filters. Can be either "all" or "any". REMOVE_RESPONSE_LINKS = True # Whether to remove `links` keys from responses. REPORT_DOWNLOAD_WAIT_TIME = 60 # Time in seconds to wait before downloading a report after starting its generation CONNECTION_ERRORS_RETRIES = 5 # num of times to retry in case of connection-errors CONNECTION_ERRORS_INTERVAL = 1 # num of seconds between each time to send an http-request in case of a connection error. VALID_TAG_TYPES = ["custom", "location", "owner"] VALID_ASSET_GROUP_TYPES = ["dynamic", "static"] VALID_TAG_COLORS = ["blue", "green", "orange", "red", "purple", "default"] urllib3.disable_warnings() # Disable insecure warnings DEFAULT_BATCH_SIZE = 3000 # Max number of concurrent send-to-XSIAM HTTP requests in flight at once (XSUP-69895). # Without this cap the integration fans out one asyncio task + one aiohttp.ClientSession per batch, # opening dozens of simultaneous connections to the ingestion endpoint on large reports, which causes # the server to truncate responses mid-stream (aiohttp TransferEncodingError) / time out new connections. MAX_CONCURRENT_XSIAM_SENDS = 5 # Max number of batch-send tasks allowed to be in flight before the stream-reader applies backpressure. # Keeps memory bounded and surfaces send failures promptly instead of streaming an entire huge report ahead # of the senders. Kept >= MAX_CONCURRENT_XSIAM_SENDS so the semaphore (not this) is the active throttle. MAX_IN_FLIGHT_BATCH_TASKS = 10 # Total timeout (seconds) for a single send-to-XSIAM HTTP request, so a stuck upload fails fast and is # retried instead of hanging until the engine's control-channel timeout trips. XSIAM_SEND_TIMEOUT_SECONDS = 60 # Idle-read timeout (seconds) for streaming the Rapid7 report download. No overall cap, so large reports # can finish in a single fetch; fails only if the connection stalls for this long. RAPID7_STREAM_SOCK_READ_TIMEOUT_SECONDS = 120 RAPID7_STREAM_SOCK_CONNECT_TIMEOUT_SECONDS = 30 BASE_QUERY_FOR_ASSETS = """WITH asset_tags AS ( SELECT dta.asset_id, json_agg( json_build_object( 'Name', dt.tag_name, 'Type', dt.tag_type ) ) AS aggregated_tags_json FROM dim_tag_asset dta JOIN dim_tag dt USING (tag_id) GROUP BY dta.asset_id ), asset_softwares AS ( SELECT das.asset_id, json_agg( json_build_object( 'Software_ID', das.software_id, 'Fingerprint_source_ID', das.fingerprint_source_id, 'Vendor', ds.vendor, 'Family', ds.family, 'Name', ds.name, 'Version', ds.version, 'Software_class', ds.software_class, 'Cpe', ds.cpe ) ) AS aggregated_software_json FROM dim_software ds JOIN dim_asset_software das ON ds.software_id = das.software_id GROUP BY das.asset_id ), asset_macs AS ( SELECT dama.asset_id, json_agg(dama.mac_address) AS aggregated_mac_addresses FROM dim_asset_mac_address dama GROUP BY dama.asset_id ), asset_ips AS ( SELECT daia.asset_id, json_agg(daia.ip_address) AS aggregated_ip_addresses FROM dim_asset_ip_address daia GROUP BY daia.asset_id ), asset_files AS ( SELECT daf.asset_id, json_agg( json_build_object( 'file_id', daf.file_id, 'type', daf.type, 'name', daf.name, 'size', daf.size ) ) AS aggregated_files_json FROM dim_asset_file daf GROUP BY daf.asset_id ), asset_service AS ( SELECT dase.asset_id, json_agg( json_build_object( 'service_id', dase.service_id, 'protocol', dase.protocol_id, 'port', dase.port, 'service_fingerprint_id', dase.service_fingerprint_id, 'certainty', dase.certainty ) ) AS asset_services FROM dim_asset_service dase GROUP BY dase.asset_id ), assets_host AS ( SELECT dahn.asset_id, json_agg( json_build_object( 'source_type_id', dahn.source_type_id, 'description', dhnst.description ) ) AS asset_hosts FROM dim_asset_host_name dahn JOIN dim_host_name_source_type dhnst ON dahn.source_type_id = dhnst.type_id GROUP BY dahn.asset_id ), -- Finds the single, absolute latest scan for each asset (regardless of status) asset_latest_scan AS ( SELECT DISTINCT ON (fas.asset_id) fas.asset_id, ds.finished AS last_scan_date, ds.status_id AS last_scan_status_id, ds.scan_id AS scan_id, ds.started AS started, ds.type_id AS type_id, ds.scan_name AS scan_name, fas.vulnerabilities AS vulnerabilities, fas.aggregated_credential_status_id AS aggregated_credential_status_id FROM fact_asset_scan fas JOIN dim_scan ds ON fas.scan_id = ds.scan_id ORDER BY fas.asset_id, ds.finished DESC -- Order by asset ID, then descending finish time to get the absolute latest ), latest_os_certainty AS ( SELECT DISTINCT ON (fasos.asset_id) fasos.asset_id, fasos.certainty AS latest_os_certainty_value FROM fact_asset_scan_operating_system fasos JOIN dim_scan ds ON fasos.scan_id = ds.scan_id ORDER BY fasos.asset_id, ds.finished DESC, fasos.certainty DESC -- Use certainty as a tie-breaker if dates are equal ) SELECT da.last_assessed_for_vulnerabilities AS "dim_asset.last_assessed_for_vulnerabilities", fad.last_discovered AS "fact_asset_discovery.last_discovered", da.asset_id AS "dim_asset.asset_id", da.host_name AS "dim_asset.host_name", da.host_type_id AS "dim_asset.host_type_id", dht.description AS "dim_host_type.description", CASE WHEN da.ip_address LIKE '%:%' THEN NULL ELSE da.ip_address END AS "ipv4", CASE WHEN da.ip_address LIKE '%:%' THEN da.ip_address ELSE NULL END AS "ipv6", da.mac_address AS "dim_asset.mac_address", da.sites AS "dim_asset.sites", da.operating_system_id AS "dim_asset.operating_system_id", dos.architecture AS "dim_operating_system.architecture", dos.description AS "dim_operating_system.description", dos.family AS "dim_operating_system.family", dos.name AS "dim_operating_system.name", dos.system AS "dim_operating_system.system", dos.asset_type AS "dim_operating_system.asset_type", dos.vendor AS "dim_operating_system.vendor", dos.version AS "dim_operating_system.version", dos.cpe AS "dim_operating_system.cpe", loc.latest_os_certainty_value AS "fact_asset_scan_operating_system.certainty", atags.aggregated_tags_json AS "dim_tag_asset.tags", am.aggregated_mac_addresses AS "dim_asset_mac_address.mac_address", ai.aggregated_ip_addresses AS "dim_asset_ip_address.ip_address", af.aggregated_files_json AS "asset_files", asoft.aggregated_software_json AS "asset_softwares", als.last_scan_date AS "dim_scan.finished", als.last_scan_status_id AS "dim_scan.status_id", als.scan_id AS "dim_scan.scan_id", als.started AS "dim_scan.started", als.type_id AS "dim_scan.type_id", als.scan_name AS "dim_scan.scan_name", als.vulnerabilities AS "fact_asset_scan.vulnerabilities", als.aggregated_credential_status_id AS "fact_asset_scan.aggregated_credential_status_id", dacs.aggregated_credential_status_description AS "dim_aggregated_credential_status.aggregated_credential_status_description", ah.asset_hosts AS "asset hosts", ase.asset_services AS "asset_services" FROM dim_asset da LEFT JOIN fact_asset_discovery fad ON da.asset_id = fad.asset_id LEFT JOIN dim_operating_system dos ON da.operating_system_id = dos.operating_system_id LEFT JOIN asset_tags atags ON da.asset_id = atags.asset_id LEFT JOIN asset_macs am ON da.asset_id = am.asset_id LEFT JOIN asset_ips ai ON da.asset_id = ai.asset_id LEFT JOIN asset_files af ON da.asset_id = af.asset_id LEFT JOIN dim_host_type dht ON da.host_type_id = dht.host_type_id LEFT JOIN asset_softwares asoft ON da.asset_id = asoft.asset_id LEFT JOIN assets_host ah ON da.asset_id = ah.asset_id INNER JOIN asset_latest_scan als ON da.asset_id = als.asset_id LEFT JOIN dim_aggregated_credential_status dacs ON als.aggregated_credential_status_id = dacs.aggregated_credential_status_id LEFT JOIN latest_os_certainty loc ON da.asset_id = loc.asset_id -- Join the new CTE LEFT JOIN asset_service ase ON da.asset_id = ase.asset_id WHERE als.last_scan_date >= CURRENT_DATE - INTERVAL '100 days' AND als.last_scan_status_id = 'C' ORDER BY da.asset_id ASC""" BASE_QUERY_FOR_VULNERABILITIES = """ WITH cve_references AS ( SELECT dvr.vulnerability_id, array_to_string(array_agg(dvr.reference), ', ') AS aggregated_cves FROM dim_vulnerability_reference AS dvr WHERE dvr.source = 'CVE' GROUP BY dvr.vulnerability_id ), aggregated_categories AS ( SELECT dvc.vulnerability_id, json_agg( json_build_object( 'id', dvc.category_id, 'name', dvc.category_name ) ) AS vuln_categories FROM dim_vulnerability_category AS dvc GROUP BY dvc.vulnerability_id ), aggregated_details AS ( SELECT fasvi.asset_id, fasvi.vulnerability_id, fasvi.scan_id, fasvi.date AS scan_date, -- Aggregate the related finding details into a JSON array of objects json_agg( json_build_object( 'status_id', fasvi.status_id, 'proof', htmlToText(fasvi.proof), 'service_id', fasvi.service_id, 'port', fasvi.port, 'protocol_id', fasvi.protocol_id ) ) AS finding_details_json FROM fact_asset_scan_vulnerability_instance fasvi INNER JOIN fact_asset_vulnerability_finding favf ON fasvi.asset_id = favf.asset_id AND fasvi.vulnerability_id = favf.vulnerability_id AND fasvi.scan_id = favf.scan_id GROUP BY fasvi.asset_id, fasvi.vulnerability_id, fasvi.scan_id, fasvi.date ) SELECT ad.asset_id, ad.vulnerability_id AS vuln_id, ad.scan_id, ad.scan_date AS date, ad.finding_details_json, dv.nexpose_id AS vulnerability_name, dv.severity AS vulnerability_severity, dv.title, REPLACE(REPLACE(dv.description, CHR(10), ' '), CHR(13), ' ') AS description, dv.date_published, dv.date_added, dv.severity_score, dv.riskscore, dv.cvss_authentication_id, dv.cvss_exploit_score, dv.cvss_impact_score, dv.cvss_v2_score, dv.cvss_v2_exploit_score, dv.cvss_v2_impact_score, dv.cvss_v3_score, dv.cvss_v3_exploit_score, dv.denial_of_service, dv.exploits, dv.malware_kits, dv.date_modified, ac.vuln_categories, cef.aggregated_cves FROM aggregated_details ad LEFT JOIN dim_vulnerability AS dv ON ad.vulnerability_id = dv.vulnerability_id LEFT JOIN aggregated_categories AS ac ON ad.vulnerability_id = ac.vulnerability_id LEFT JOIN cve_references cef ON ad.vulnerability_id = cef.vulnerability_id WHERE ad.scan_date >= CURRENT_DATE - INTERVAL '100 days' ORDER BY ad.asset_id, ad.vulnerability_id, ad.scan_date DESC """ BASE_QUERY = {"asset": BASE_QUERY_FOR_ASSETS, "vulnerability": BASE_QUERY_FOR_VULNERABILITIES} VENDOR = {"asset": "Rapid7", "vulnerability": "Rapid7"} PRODUCT = {"asset": "nexpose_assets", "vulnerability": "nexpose_vulnerabilities"} XSIAM_EVENT_CHUNK_SIZE = 2**20 # 1 Mib XSIAM_EVENT_CHUNK_SIZE_LIMIT = 9 * (10**6) # 9 MB, note that the allowed max size for 1 entry is 5MB. def log(event_type: str, log_line: str): full_log_line = f"[{event_type}] {log_line}" demisto.debug(full_log_line) class ScanStatus(Enum): """An Enum of possible scan status values.""" PAUSE = "pause" RESUME = "resume" STOP = "stop" class FlexibleEnum(EnumMeta): """A custom EnumMeta to allow flexible conversion from strings to Enum.""" def __getitem__(cls, item: Any): try: return super().__getitem__(item) except KeyError: return super().__getitem__(item.upper().replace(" ", "_").replace("-", "_")) class CredentialService(Enum, metaclass=FlexibleEnum): """An Enum of possible service values for credentials.""" AS400 = "as400" CIFS = "cifs" CIFSHASH = "cifshash" CVS = "cvs" DB2 = "db2" FTP = "ftp" HTTP = "http" MS_SQL = "ms-sql" MYSQL = "mysql" NOTES = "notes" ORACLE = "oracle" POP = "pop" POSTGRESQL = "postgresql" REMOTE_EXEC = "remote-exec" SNMP = "snmp" SNMPV3 = "snmpv3" SSH = "ssh" SSH_KEY = "ssh-key" SYBASE = "sybase" TELNET = "telnet" class RepeatFrequencyType(Enum, metaclass=FlexibleEnum): """An Enum of possible repeat frequency for scheduled scans.""" HOUR = "hour" DAY = "day" WEEK = "week" DATE_OF_MONTH = "date-of-month" DAY_OF_MONTH = "day-of-month" class SharedCredentialSiteAssignment(Enum, metaclass=FlexibleEnum): """An Enum of possible site assignment values for shared credentials.""" ALL_SITES = "all-sites" SPECIFIC_SITES = "specific-sites" class SNMPv3AuthenticationType(Enum, metaclass=FlexibleEnum): """An Enum of possible authentication type values for shared credentials.""" NO_AUTHENTICATION = "no-authentication" MD5 = "md5" SHA = "sha" class SNMPv3PrivacyType(Enum, metaclass=FlexibleEnum): """An Enum of possible privacy type values for SNMPv3P credentials.""" NO_PRIVACY = "no-privacy" DES = "des" AES_128 = "aes-128" AES_192 = "aes-192" AES_192_WITH_3_DES_KEY_EXTENSION = "aes-192-with-3-des-key-extension" AES_256 = "aes-256" AES_265_WITH_3_DES_KEY_EXTENSION = "aes-265-with-3-des-key-extension" class SSHElevationType(Enum, metaclass=FlexibleEnum): """An Enum of possible permission elevation values for SSH credentials.""" NONE = "none" SUDO = "sudo" SUDOSU = "sudosu" SU = "su" PBRUN = "pbrun" PRIVILEGED_EXEC = "privileged-exec" class VulnerabilityExceptionScopeType(Enum, metaclass=FlexibleEnum): """An Enum of possible vulnerability exception scope type values.""" GLOBAL = "Global" SITE = "Site" ASSET = "Asset" ASSET_GROUP = "Asset Group" class Client(BaseClient): """Client class for interactions with Rapid7 Nexpose API.""" def __init__( self, url: str, username: str, password: str, token: str | None = None, verify: bool = True, connection_error_retries: int = CONNECTION_ERRORS_RETRIES, ): """ Initialize the client. Args: url (str): Nexpose server base URL. username (str): Username to use for authentication. password (str): Password to use for authentication. token (str | None, optional): 2FA token to use for authentication. verify (bool | None, optional): Whether to verify SSL certificates. Defaults to True. """ self.base_url = url self._auth_username = username self._auth_password = password self._auth_token = token self._headers = { "Accept": "application/json", "Content-Type": "application/json", } self.connection_error_retries = CONNECTION_ERRORS_RETRIES # Add 2FA token to headers if provided if token: self._headers.update({"Token": token}) super().__init__( base_url=url.rstrip("/") + "/api/3", auth=(username, password), headers=self._headers, ok_codes=(200, 201), verify=verify, ) def _http_request(self, **kwargs): # type: ignore[override] """Wrapper for BaseClient._http_request() that optionally removes `links` keys from responses.""" for _time in range(1, self.connection_error_retries + 1): try: response = super()._http_request(**kwargs) if REMOVE_RESPONSE_LINKS: return remove_dict_key(response, "links") return response except (DemistoException, requests.ReadTimeout) as error: demisto.error(f"Error {error} running _http_request in time {_time}") if ( isinstance(error, DemistoException) and not isinstance(error.exception, requests.ConnectionError) ) or _time == self.connection_error_retries: raise else: time.sleep(1) # pylint: disable=sleep-exists return None def _generate_session_id(self) -> str: """ Generate a new session ID for internal API requests. Note: This is used for internal non-documented API requests that are used when using the web interface, and have no alternative in the native API. Returns: str: A session ID. """ internal_api_headers = self._headers.copy() internal_api_headers["Content-Type"] = "application/x-www-form-urlencoded" return self._http_request( method="POST", full_url=self.base_url.rstrip("/") + "/data/user/login", headers=internal_api_headers, data={ "nexposeccusername": self._auth_username, "nexposeccpassword": self._auth_password, }, ok_codes=(200,), ).get("sessionID") def _paged_http_request( self, page_size: int | None = None, page: int | None = None, sort: str | None = None, limit: int | None = None, **kwargs ) -> list: """ Run _http_request with pagination handling. Args: page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of items to return. None means to not use a limit. Defaults to None. **kwargs: Parameters to pass when calling `_http_request`. Returns: list: A list containing all paginated items. """ if DEFAULT_PAGE_SIZE and not page_size: page_size = DEFAULT_PAGE_SIZE kwargs["params"] = kwargs.get("params", {}) # If `params` is None, set it to an empty dict if page: kwargs["params"]["page"] = str(page) kwargs["params"].update( find_valid_params( page=page, size=page_size, ) ) # If sort is not None, split it into a list and add to kwargs if sort: kwargs["params"]["sort"] = sort.split(sep=";") response: dict = self._http_request(**kwargs) result = response.get("resources", []) if not result: return [] if not page: total_pages = response.get("page", {}).get("totalPages", 1) demisto.debug(f"Total pages = {total_pages}") page_count = 0 # Note: page indexing on Nexpose's API starts at 0 while (page_count + 1) < total_pages and (limit is None or len(result) < limit): page_count += 1 kwargs["params"]["page"] = str(page_count) response = self._http_request(**kwargs) resources = response["resources"] demisto.debug(f"Received {len(resources)} resources with page {page_count=}, {page_size=}") result.extend(resources) if limit and limit < len(result): return result[:limit] return result def create_asset( self, site_id: str, date: str, ip_address: str | None = None, hostname: str | None = None, hostname_source: str | None = None, ) -> dict: """ | Create a new asset on a site. | | https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createAsset Note: The API endpoint that's used has additional parameters, and can be also used to update existing assets. These options are currently not supported. Args: site_id (str): ID of the site to create the asset on. date (str): The date the data was collected on the asset. ip_address (str | None, optional): IP address of the asset to create. hostname (str | None, optional): Hostname of the asset to create. hostname_source (str | None, optional): Source of the hostname. Returns: dict: API response with information about the newly generated asset. """ if ip_address is None and hostname is None: raise ValueError('At least one of "ip" and "host_name" arguments must be passed.') post_data: dict = {"date": date} if ip_address is not None: post_data["ip"] = ip_address if hostname is not None: post_data["hostName"] = {"name": hostname} if hostname_source is not None: post_data["hostName"]["source"] = hostname_source.lower() return self._http_request( method="POST", url_suffix=f"/sites/{site_id}/assets", json_data=post_data, resp_type="json", ) def create_report(self, report_id: str) -> dict: """ | Generates a configured report. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/generateReport Args: report_id (str): ID of the configured report to generate. Returns: dict: API response with information about the newly created report instance. """ return self._http_request( url_suffix=f"/reports/{report_id}/generate", method="POST", resp_type="json", ) def create_report_config(self, scope: dict[str, Any], template_id: str, report_name: str, report_format: str) -> dict: """ | Create a new report configuration. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createReport Args: scope (dict[str, Any]): Scope of the report, see Nexpose's documentation for more details. template_id (str): ID of report template to use. report_name (str): Name for the report that will be generated. report_format (str): Format of the report that will be generated. Returns: dict: API response with information about the newly created report configuration. """ post_data = { "scope": scope, "template": template_id, "name": report_name, "format": report_format.lower(), } return self._http_request( url_suffix="/reports", method="POST", json_data=post_data, resp_type="json", ) def create_shared_credential( self, name: str, site_assignment: SharedCredentialSiteAssignment, service: CredentialService, database_name: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: bool | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, sites: list[int] | None = None, snmp_community_name: str | None = None, snmpv3_authentication_type: SNMPv3AuthenticationType | None = None, snmpv3_privacy_password: str | None = None, snmpv3_privacy_type: SNMPv3PrivacyType | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: SSHElevationType | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: bool | None = None, username: str | None = None, ) -> dict: """ | Create a new shared credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSharedCredential Args: name (str): Name of the credential. site_assignment (SharedCredentialSiteAssignment): Site assignment configuration for the credential. service (CredentialService): Credential service type. database_name (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. sites (list[int] | None, optional): List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. snmp_community_name (str | None, optional): SNMP community for authentication. snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential. snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use. snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (bool | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. Returns: dict: API response with information about the newly created shared credential. """ account_data = create_credential_creation_body( service=service, database_name=database_name, domain=domain, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, snmp_community_name=snmp_community_name, snmpv3_authentication_type=snmpv3_authentication_type, snmpv3_privacy_password=snmpv3_privacy_password, snmpv3_privacy_type=snmpv3_privacy_type, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication, username=username, ) post_data = find_valid_params( description=description, hostRestriction=host_restriction, name=name, siteAssignment=site_assignment.value, ) if port_restriction is not None and host_restriction is not None: post_data["portRestriction"] = port_restriction if sites is not None and site_assignment == SharedCredentialSiteAssignment.SPECIFIC_SITES: post_data["sites"] = sites post_data["account"] = account_data return self._http_request( method="POST", url_suffix="/shared_credentials", json_data=post_data, resp_type="json", ) def create_site( self, name: str, description: str | None = None, assets: list[str] | None = None, site_importance: str | None = None, template_id: str | None = None, ) -> dict: """ | Create a new site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSite Args: name (str): Name of the site. Must be unique. description (str | None, optional): Description of the site. Defaults to None. assets (list[str] | None, optional): List of asset IDs to be included in site scans. Defaults to None. site_importance (str | None, optional): Importance of the site. Defaults to None (results in using API's default - "normal"). template_id (str | None, optional): The identifier of a scan template. Defaults to None (results in using default scan template). Returns: dict: API response with information about the newly created site. """ post_data = find_valid_params( name=name, description=description, importance=site_importance.lower() if site_importance else None, scanTemplateId=template_id, ) if assets: post_data["scan"] = {"assets": {"includedTargets": {"addresses": assets}}} return self._http_request( url_suffix="/sites", method="POST", json_data=post_data, resp_type="json", ) def create_site_scan_credential( self, site_id: str, name: str, service: CredentialService, database_name: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: bool | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, snmp_community_name: str | None = None, snmpv3_authentication_type: SNMPv3AuthenticationType | None = None, snmpv3_privacy_password: str | None = None, snmpv3_privacy_type: SNMPv3PrivacyType | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: SSHElevationType | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: bool | None = None, username: str | None = None, ) -> dict: """ | Create a new site scan credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteCredential Args: name (str): Name of the credential. Assign the shared scan credential either to be available to all sites, or a specific list of sites. site_id (str): ID of the site to create the credential for. service (CredentialService): Credential service type. database_name (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. snmp_community_name (str | None, optional): SNMP community for authentication. snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential. snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use. snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (bool | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. Returns: dict: API response with information about the newly created shared credential. """ account_data = create_credential_creation_body( service=service, database_name=database_name, domain=domain, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, snmp_community_name=snmp_community_name, snmpv3_authentication_type=snmpv3_authentication_type, snmpv3_privacy_password=snmpv3_privacy_password, snmpv3_privacy_type=snmpv3_privacy_type, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication, username=username, ) post_data = find_valid_params( description=description, hostRestriction=host_restriction, name=name, ) if port_restriction is not None and host_restriction is not None: post_data["portRestriction"] = port_restriction post_data["account"] = account_data return self._http_request( method="POST", url_suffix=f"/sites/{site_id}/site_credentials", json_data=post_data, resp_type="json", ) def create_site_scan_schedule( self, site_id: str, start_date: str, enabled: bool, excluded_asset_groups: list[int] | None = None, excluded_targets: list[str] | None = None, included_asset_groups: list[int] | None = None, included_targets: list[str] | None = None, duration: str | None = None, repeat_behaviour: str | None = None, frequency: RepeatFrequencyType | None = None, interval: int | None = None, date_of_month: int | None = None, scan_name: str | None = None, scan_template_id: str | None = None, ) -> dict: """ | Create a new site scan schedule. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createSiteScanSchedule Args: site_id (str): ID of the site to create a new scheduled scan for. start_date (str): The scheduled start date and time formatted in ISO 8601 format. enabled (bool): A flag indicating whether the scan schedule is enabled. excluded_asset_groups (list[int] | None, optional): Asset groups to exclude from the scan. excluded_targets (list[str] | None, optional): Addresses to exclude from the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. included_asset_groups (list[int] | None, optional): Asset groups to include in the scan. included_targets (list[str] | None, optional): Addresses to include in the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. duration (str | None, optional): An ISO 8601 formatted duration string that Specifies the maximum duration the scheduled scan is allowed to run. repeat_behaviour (str | None, optional): The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. frequency (RepeatFrequencyType | None, optional): Frequency for the schedule to repeat. Required if using other repeat settings. interval (int | None, optional): The interval time the schedule should repeat. Required if using other repeat settings. date_of_month(int | None, optional): Specifies the schedule repeat day of the interval month. Required and used only if frequency is set to "DATE_OF_MONTH". scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. scan_template_id (str | None, optional): ID of the scan template to use. Returns: dict: API response with information about the newly created scan schedule. """ assets: dict = {} repeat: dict = {} if excluded_asset_groups: assets["excludedAssetGroups"] = {"assetGroupIDs": excluded_asset_groups} if excluded_targets: assets["excludedTargets"] = {"addresses": excluded_targets} if included_asset_groups: assets["includedAssetGroups"] = {"assetGroupIDs": included_asset_groups} if included_targets: assets["includedTargets"] = {"addresses": included_targets} if frequency is not None: if interval is None: raise ValueError("'interval' parameter must be set when frequency is used.") if frequency == RepeatFrequencyType.DATE_OF_MONTH and date_of_month is None: raise ValueError("'date-of-month' parameter must be set if frequency is set to 'Date of month'.") repeat["every"] = frequency.value repeat.update( find_valid_params( interval=interval, dateOfMonth=date_of_month, ) ) post_data = find_valid_params( duration=duration, enabled=enabled, onScanRepeat=repeat_behaviour.lower() if repeat_behaviour is not None else None, scanName=scan_name, scanTemplateId=scan_template_id, start=start_date, ) post_data.update( find_valid_params( strict_mode=True, assets=assets, repeat=repeat, ) ) return self._http_request( url_suffix=f"/sites/{site_id}/scan_schedules", method="POST", json_data=post_data, resp_type="json", ) def create_vulnerability_exception( self, vulnerability_id: str, scope_type: VulnerabilityExceptionScopeType, state: str, reason: str, scope_id: int | None = None, expires: str | None = None, comment: str | None = None, ) -> dict: """ | Create a new vulnerability exception. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createVulnerabilityException Args: vulnerability_id (str): ID of the vulnerability to create an exception for. scope_type (VulnerabilityExceptionScopeType): The type of the exception scope. state (str): The state of the vulnerability exception. reason (str): The reason the vulnerability exception was submitted. Can be one of: "False Positive", "Compensating Control", "Acceptable Use", "Acceptable Risk", and "Other". scope_id (int): ID of the chosen `scope_type` (site ID, asset ID, etc.). Required if `scope_type` is anything other than `Global` expires (str | None, optional): The date and time the vulnerability exception is set to expire. comment (str | None, optional): A comment from the submitter as to why the exception was submitted. Returns: dict: API response with information about the newly created vulnerability exception. """ scope_obj = { "id": scope_id, "type": scope_type.value, "vulnerability": vulnerability_id, } submit_obj = find_valid_params( reason=reason, comment=comment, ) post_data = find_valid_params( expires=expires, scope=scope_obj, state=state, submit=submit_obj, ) return self._http_request( url_suffix="/vulnerability_exceptions", method="POST", json_data=post_data, resp_type="json", ) def delete_asset(self, asset_id: str) -> dict: """ | Delete an asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteAsset Args: asset_id (str): ID of the asset to delete. Returns: dict: API response. """ return self._http_request( method="DELETE", url_suffix=f"/assets/{asset_id}", resp_type="json", ) def delete_scan_schedule(self, site_id: str, scheduled_scan_id: str) -> dict: """ | Delete a scheduled scan. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSiteScanSchedule Args: site_id (str): ID of the site to delete the scheduled scan from. scheduled_scan_id (str): ID of the scheduled scan to delete. Returns: dict: API response. """ return self._http_request( url_suffix=f"/sites/{site_id}/scan_schedules/{scheduled_scan_id}", method="DELETE", resp_type="json", ) def delete_site(self, site_id: str) -> dict: """ | Delete a site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSite Args: site_id (str): ID of the site to delete. Returns: dict: API response with information about the deleted site. """ return self._http_request( url_suffix=f"/sites/{site_id}", method="DELETE", resp_type="json", ) def delete_shared_credential(self, shared_credential_id: str) -> dict: """ | Delete a shared credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSharedCredential Args: shared_credential_id (str): ID of the shared credential to delete. Returns: dict: API response with information about the deleted shared credential. """ return self._http_request( url_suffix=f"/shared_credentials/{shared_credential_id}", method="DELETE", resp_type="json", ) def delete_site_scan_credential(self, site_id: str, site_credential_id: str) -> dict: """ | Delete a site scan credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteSiteCredential Args: site_id (str): ID of the site to delete the scan credential from. site_credential_id (str): ID of the scan credential to delete. Returns: dict: API response with information about the deleted site scan credential. """ return self._http_request( url_suffix=f"/sites/{site_id}/site_credentials/{site_credential_id}", method="DELETE", resp_type="json", ) def delete_vulnerability_exception(self, vulnerability_exception_id: str) -> dict: """ | Delete a vulnerability exception. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/removeVulnerabilityException Args: vulnerability_exception_id (str): ID of the vulnerability exception to delete. Returns: dict: API response with information about the deleted vulnerability exception. """ return self._http_request( url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}", method="DELETE", resp_type="json", ) def download_report(self, report_id: str, instance_id: str) -> bytes: """ | Download a report. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/downloadReport Args: report_id (str): ID of the report to download. instance_id (str): ID of the report instance. Returns: bytes: Report file in bytes. """ request_headers = self._headers.copy() request_headers["Accept"] = "*/*" return self._http_request( url_suffix=f"/reports/{report_id}/history/{instance_id}/output", method="GET", headers=request_headers, resp_type="content", ) def get_asset_vulnerability(self, asset_id: str, vulnerability_id: str) -> dict: """ | Retrieve information about vulnerability findings on an asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerability Args: asset_id (str): ID of the asset to retrieve information about. vulnerability_id (str): ID of the vulnerability to look for. Returns: dict: API response with information about vulnerability findings on the asset. """ return self._http_request( url_suffix=f"/assets/{asset_id}/vulnerabilities/{vulnerability_id}", method="GET", resp_type="json", ) def get_asset_vulnerabilities( self, asset_id: str, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieves a list of all vulnerability findings on an asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerabilities Args: asset_id (str): ID of the site to retrieve linked assets from. page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of assets to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list with all vulnerability findings on an asset. """ return self._paged_http_request( url_suffix=f"/assets/{asset_id}/vulnerabilities", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_asset(self, asset_id: str) -> dict: """ | Retrieve information about a specific asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAsset Args: asset_id (str): ID of the asset to retrieve information about. Returns: dict: API response with information about a specific asset. """ return self._http_request( url_suffix=f"/assets/{asset_id}", method="GET", resp_type="json", ) def get_asset_tags(self, asset_id: str) -> dict: """ | Retrieve tags about a specific asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetTags Args: asset_id (str): ID of the asset to retrieve information about. Returns: dict: API response with list of tags about a specific asset. """ return self._http_request( url_suffix=f"/assets/{asset_id}/tags", method="GET", resp_type="json", ) def get_asset_vulnerability_solution(self, asset_id: str, vulnerability_id: str) -> dict: """ | Retrieve information about solutions that can be used to remediate a vulnerability on an asset. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetVulnerabilitySolutions Args: asset_id (str): ID of the asset to retrieve solutions for. vulnerability_id (str): ID of the vulnerability to retrieve solutions for. Returns: dict: API response with information about solutions that can be used to remediate a vulnerability on an asset. """ return self._http_request( url_suffix=f"/assets/{asset_id}/vulnerabilities/{vulnerability_id}/solution", method="GET", resp_type="json", ) def get_assets( self, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of all assets. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssets Args: page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of assets to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list of all assets (up to a limit, if set). """ return self._paged_http_request( url_suffix="/assets", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_assigned_shared_credentials(self, site_id: str) -> dict: """ | Retrieve information about all credentials that are shared with a specific site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteSharedCredentials Args: site_id (str): ID of the site to retrieve credentials that are shared with. Returns: dict: API response with information shared credentials that are shared with a specific site. """ return self._http_request( url_suffix=f"/sites/{site_id}/shared_credentials", method="GET", resp_type="json", ).get("resources") def get_report_history(self, report_id: str, instance_id: str) -> dict: """ | Retrieve information about a generated report. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getReportInstance Args: report_id (str): ID of the report to retrieve information about. instance_id (str): ID of the report instance to retrieve information about. Returns: dict: API response with information about the generated report. """ return self._http_request( url_suffix=f"/reports/{report_id}/history/{instance_id}", method="GET", resp_type="json", ) def get_report_templates(self) -> dict: """ | Retrieve a list of all available report templates. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getReportTemplates Returns: dict: API response with information about all available report templates. """ return self._http_request( url_suffix="/report_templates", method="GET", resp_type="json", ) def get_scan(self, scan_id: str) -> dict: """ | Retrieve information about a specific scan. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getScan Args: scan_id (str): ID of the scan to retrieve. Returns: dict: API response with information about a specific scan. """ return self._http_request( url_suffix=f"/scans/{scan_id}", method="GET", resp_type="json", ) def get_scans( self, active: bool | None = False, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of all scans. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getScans Args: active (bool | None, optional): Whether to return active scans or not. Defaults to False. page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list of all scans (up to a limit, if set). """ params = {"active": active} if active is not None else {} return self._paged_http_request( url_suffix="/scans", method="GET", params=params, page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_scan_schedule(self, site_id: str, schedule_id: str) -> dict: """ | Retrieve information about a specific scan schedule. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScanSchedule Args: site_id (str): ID of the site to retrieve scan schedule from. schedule_id (str): ID of the scan schedule to retrieve. Returns: dict: A dictionary containing information about the scan schedule. """ return self._http_request( url_suffix=f"/sites/{site_id}/scan_schedules/{schedule_id}", method="GET", resp_type="json", ) def get_scan_schedules(self, site_id: str) -> list[dict]: """ | Retrieve information about scan schedules for a specific site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScanSchedules Args: site_id (str): ID of the site to retrieve scan schedules from. Returns: list[dict]: A list of scan schedules for the site. """ return self._http_request( url_suffix=f"/sites/{site_id}/scan_schedules", method="GET", resp_type="json", ).get("resources") def get_shared_credential(self, credential_id: str) -> dict: """ | Retrieve information about a specific shared credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSharedCredential Args: credential_id (str): ID of the shared credential to retrieve information about. Returns: dict: API response with information about a specific shared credential. """ return self._http_request( url_suffix=f"/shared_credentials/{credential_id}", method="GET", resp_type="json", ) def get_shared_credentials(self) -> list[dict]: """ | Retrieve information about all shared credentials. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSharedCredentials Returns: list[dict]: A list with all shared credentials. """ return self._http_request( url_suffix="/shared_credentials", method="GET", resp_type="json", ).get("resources") def get_site_assets( self, site_id: str, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of all assets that are linked with a specific site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteAssets Args: site_id (str): ID of the site to retrieve linked assets from. page_size (int | None, optional): Number of assets to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of sites to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list with all assets that are linked with a specific site (up to a limit, if set). """ return self._paged_http_request( url_suffix=f"/sites/{site_id}/assets", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_site_scan_credential(self, site_id: str, credential_id: str) -> dict: """ | Retrieve information about a specific site scan credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteCredential Args: site_id (str): ID of the site to retrieve scan credentials from. credential_id (str): ID of the scan credential to retrieve. Returns: dict: API response with information about a specific site scan credential. """ return self._http_request( url_suffix=f"/sites/{site_id}/site_credentials/{credential_id}", method="GET", resp_type="json", ) def get_site_scan_credentials(self, site_id: str) -> list[dict]: """ | Retrieve information about a specific site scan credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteCredentials Args: site_id (str): ID of the site to retrieve scan credentials from. Returns: list[dict]: A list with information about all site scan credentials. """ return self._http_request( url_suffix=f"/sites/{site_id}/site_credentials", method="GET", resp_type="json", ).get("resources") def get_site_scans( self, site_id: str, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of scans from a specific site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSiteScans Args: site_id (str): ID of the site to retrieve scans from. page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. Returns: list[dict]: API response with information about all scans from the specific site. """ return self._paged_http_request( url_suffix=f"/sites/{site_id}/scans", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_sites( self, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of sites. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getSites Args: page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of sites to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list of sites (up to a limit, if set). """ return self._paged_http_request( url_suffix="/sites", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_vulnerabilities( self, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve information about all existing vulnerabilities. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilities Args: page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of sites to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list of sites (up to a limit, if set). """ return self._paged_http_request( url_suffix="/vulnerabilities", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_vulnerability(self, vulnerability_id: str) -> dict: """ | Retrieve information about a specific vulnerability. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerability Args: vulnerability_id (str): ID of the vulnerability to retrieve information about. Returns: dict: API response with information about a specific vulnerability. """ return self._http_request( url_suffix=f"/vulnerabilities/{vulnerability_id}", method="GET", resp_type="json", ) def get_vulnerability_exception(self, vulnerability_exception_id: str) -> dict: """ | Retrieve information about an exception made on a vulnerability. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilityException Args: vulnerability_exception_id (str): ID of the vulnerability exception to retrieve information about. Returns: dict: API response with information about a specific exception made on a vulnerability. """ return self._http_request( url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}", method="GET", resp_type="json", ) def get_vulnerability_exceptions( self, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve exceptions defined on vulnerabilities. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getVulnerabilityExceptions Args: page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of sites to return. None means to not use a limit. """ return self._paged_http_request( url_suffix="/vulnerability_exceptions", method="GET", page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def search_assets( self, filters: list[dict] | None = None, match: str = MATCH_DEFAULT_VALUE, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: """ | Retrieve a list of all assets with access permissions that match the provided search filters. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/findAssets Args: filters (list[dict] | None, optional): List of filters to use for searching assets. Defaults to None. match (str): Determine if the filters should match all or any of the filters. Can be either "all" or "any". Defaults to MATCH_DEFAULT_VALUE. page_size (int | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (int | None, optional): Specific pagination page to retrieve. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (int | None, optional): Limit the number of sites to return. None means to not use a limit. Defaults to None. Returns: list[dict]: A list of assets (up to a limit, if set) matching the filters. """ post_data = find_valid_params( filters=filters, match=match, ) return self._paged_http_request( url_suffix="/assets/search", method="POST", json_data=post_data, page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def set_assigned_shared_credential_status(self, site_id: str, shared_credential_id: str, enabled: bool) -> dict: """ | Update the status of a shared credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/enableSharedCredentialOnSite Args: site_id (str): ID of the site to update the shared credential status on. shared_credential_id (str): ID of the shared credential to update the status of. enabled (bool): A flag indicating whether the shared credential should be enabled or not. Returns: dict: API response with information about the updated shared credential. """ return self._http_request( url_suffix=f"/sites/{site_id}/shared_credentials/{shared_credential_id}/enabled", method="PUT", data=json.dumps(enabled), # type: ignore resp_type="json", ) def update_scan_schedule( self, site_id: str, scan_schedule_id: int, repeat_behaviour: str, start_date: str, enabled: bool, excluded_asset_groups: list[int] | None = None, excluded_targets: list[str] | None = None, included_asset_groups: list[int] | None = None, included_targets: list[str] | None = None, duration: str | None = None, frequency: RepeatFrequencyType | None = None, interval: int | None = None, date_of_month: int | None = None, scan_name: str | None = None, scan_template_id: str | None = None, ) -> dict: """ | Update a site scan schedule. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSiteScanSchedule Args: site_id (str): ID of the site to create a new scheduled scan for. scan_schedule_id (int): ID of the scan schedule to update. repeat_behaviour (str | None, optional): The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. start_date (str): The scheduled start date and time formatted in ISO 8601 format. enabled (bool): A flag indicating whether the scan schedule is enabled. excluded_asset_groups (list[int] | None, optional): Asset groups to exclude from the scan. excluded_targets (list[str] | None, optional): Addresses to exclude from the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. included_asset_groups (list[int] | None, optional): Asset groups to include in the scan. included_targets (list[str] | None, optional): Addresses to include in the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. duration (str | None, optional): An ISO 8601 formatted duration string that Specifies the maximum duration the scheduled scan is allowed to run. frequency (RepeatFrequencyType | None, optional): Frequency for the schedule to repeat. Required if using other repeat settings. interval (int | None, optional): The interval time the schedule should repeat. Required if using other repeat settings. date_of_month(int | None, optional): Specifies the schedule repeat day of the interval month. Required and used only if frequency is set to `DATE_OF_MONTH`. scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. scan_template_id (str | None, optional): ID of the scan template to use. Returns: str: ID of the newly created scan schedule. """ assets: dict = {} repeat: dict = {} if excluded_asset_groups: assets["excludedAssetGroups"] = {"assetGroupIDs": excluded_asset_groups} if excluded_targets: assets["excludedTargets"] = {"addresses": excluded_targets} if included_asset_groups: assets["includedAssetGroups"] = {"assetGroupIDs": included_asset_groups} if included_targets: assets["includedTargets"] = {"addresses": included_targets} if frequency is not None: if interval is None: raise ValueError("'interval' parameter must be set when frequency is used.") if frequency == RepeatFrequencyType.DATE_OF_MONTH and date_of_month is None: raise ValueError("'date-of-month' parameter must be set if frequency is set to 'Date of month'.") repeat["every"] = frequency.value repeat.update( find_valid_params( interval=interval, dateOfMonth=date_of_month, ) ) post_data = find_valid_params( duration=duration, enabled=enabled, onScanRepeat=repeat_behaviour.lower(), scanName=scan_name, scanTemplateId=scan_template_id, start=start_date, ) post_data.update( find_valid_params( strict_mode=True, assets=assets, repeat=repeat, ) ) return self._http_request( url_suffix=f"/sites/{site_id}/scan_schedules/{scan_schedule_id}", method="PUT", json_data=post_data, resp_type="json", ) def start_site_scan(self, site_id: str, scan_name: str, hosts: list[str]) -> dict: """ | Start a scan for a specific site. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/startScan Args: site_id (str): ID of the site to start a scan on. scan_name (str): Name to set for the new scan. hosts (list[str]): Hosts to scan. Returns: dict: API response with information about the started scan. """ post_data: dict = { "name": scan_name, } if hosts: post_data["hosts"] = hosts return self._http_request( url_suffix=f"/sites/{site_id}/scans", method="POST", json_data=post_data, resp_type="json", ) def update_scan_status(self, scan_id: str, status: ScanStatus) -> dict: """ | Update status for a specific scan. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/setScanStatus Args: scan_id (str): ID of the scan to update. status (ScanStatus): Status to set the scan to. """ return self._http_request( url_suffix=f"/scans/{scan_id}/{status.value}", method="POST", resp_type="json", ) def update_shared_credential( self, shared_credential_id: str, name: str, site_assignment: SharedCredentialSiteAssignment, service: CredentialService, database_name: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: bool | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, sites: list[int] | None = None, snmp_community_name: str | None = None, snmpv3_authentication_type: SNMPv3AuthenticationType | None = None, snmpv3_privacy_password: str | None = None, snmpv3_privacy_type: SNMPv3PrivacyType | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: SSHElevationType | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: bool | None = None, username: str | None = None, ) -> dict: """ | Update an existing new shared credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSharedCredential Args: shared_credential_id (str): ID of the shared credential to update. name (str): Name of the credential. site_assignment (SharedCredentialSiteAssignment): Site assignment configuration for the credential. service (CredentialService): Credential service type. database_name (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. sites (list[int] | None, optional): List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. snmp_community_name (str | None, optional): SNMP community for authentication. snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential. snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use. snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (bool | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. Returns: dict: API response with information about the newly created shared credential. """ account_data = create_credential_creation_body( service=service, database_name=database_name, domain=domain, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, snmp_community_name=snmp_community_name, snmpv3_authentication_type=snmpv3_authentication_type, snmpv3_privacy_password=snmpv3_privacy_password, snmpv3_privacy_type=snmpv3_privacy_type, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication, username=username, ) post_data = find_valid_params( description=description, hostRestriction=host_restriction, name=name, siteAssignment=site_assignment.value, ) if port_restriction is not None and host_restriction is not None: post_data["portRestriction"] = port_restriction if sites is not None and site_assignment == SharedCredentialSiteAssignment.SPECIFIC_SITES: post_data["sites"] = sites post_data["account"] = account_data return self._http_request( method="PUT", url_suffix=f"/shared_credentials/{shared_credential_id}", json_data=post_data, resp_type="json", ) def update_site_scan_credential( self, site_id: str, credential_id: str, name: str, service: CredentialService, database_name: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: bool | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, snmp_community_name: str | None = None, snmpv3_authentication_type: SNMPv3AuthenticationType | None = None, snmpv3_privacy_password: str | None = None, snmpv3_privacy_type: SNMPv3PrivacyType | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: SSHElevationType | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: bool | None = None, username: str | None = None, ) -> dict: """ | Update an existing site scan credential. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateSiteCredential Args: name (str): Name of the credential. Assign the shared scan credential either to be available to all sites, or a specific list of sites. site_id (str): ID of the site to create the credential for. credential_id (str): ID of the site scan credential to update. service (CredentialService): Credential service type. database_name (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. snmp_community_name (str | None, optional): SNMP community for authentication. snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential. snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use. snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (bool | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. Returns: dict: API response with information about the newly created shared credential. """ account_data = create_credential_creation_body( service=service, database_name=database_name, domain=domain, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, snmp_community_name=snmp_community_name, snmpv3_authentication_type=snmpv3_authentication_type, snmpv3_privacy_password=snmpv3_privacy_password, snmpv3_privacy_type=snmpv3_privacy_type, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication, username=username, ) post_data = find_valid_params( description=description, hostRestriction=host_restriction, name=name, ) post_data["id"] = credential_id if port_restriction is not None and host_restriction is not None: post_data["portRestriction"] = port_restriction post_data["account"] = account_data return self._http_request( method="PUT", url_suffix=f"/sites/{site_id}/site_credentials/{credential_id}", json_data=post_data, resp_type="json", ) def update_vulnerability_exception_status(self, vulnerability_exception_id: str, status: str) -> dict: """ | Update the status of a vulnerability exception. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateVulnerabilityExceptionStatus Args: vulnerability_exception_id (str): ID of the vulnerability exception to update. status (str): Status to set the vulnerability exception to. Returns: dict: API response with information about the updated vulnerability exception. """ return self._http_request( url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/{status.lower()}", method="POST", resp_type="json", ) def update_vulnerability_exception_expiration(self, vulnerability_exception_id: str, expiration_date: str) -> dict: """ | Update the expiration date for a vulnerability exception. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateVulnerabilityExceptionExpiration Args: vulnerability_exception_id (str): ID of the vulnerability exception to update. expiration_date (str): The new expiration date for the vulnerability exception. Returns: dict: API response with information about the updated vulnerability exception. """ return self._http_request( url_suffix=f"/vulnerability_exceptions/{vulnerability_exception_id}/expires", method="PUT", data=json.dumps(expiration_date), resp_type="json", ) def find_asset_site(self, asset_id: str) -> Optional["Site"]: """ Find the site of a given asset ID. Note: This method is from older versions of this pack. It uses an internal API to fetch a list of scans data for an asset, and fetches site data for these scans to determine asset's site. This will not work if: The asset has no previous scans, or if the asset has been moved to another site since the last scan. Args: asset_id (str): ID of the asset to find additional data for. Returns: Site: Site object containing data (ID, name) of the asset's site. """ request_headers = self._headers.copy() request_headers.update({"nexposeCCSessionID": self._generate_session_id()}) try: response_data: dict = self._http_request( full_url=self.base_url.rstrip("/") + f"/data/assets/{asset_id}/scans", method="POST", headers=request_headers, resp_type="json", ) except Exception: return None finally: self._session.cookies.clear() # Remove cookies received and saved to session be this request. if not response_data.get("records"): return None record_data = response_data["records"][0] if None in (record_data.get("siteID"), record_data.get("siteName")): return None return Site( site_id=str(response_data["records"][0]["siteID"]), site_name=str(response_data["records"][0]["siteName"]), ) def find_site_id(self, name: str) -> str | None: """ Find a site ID by its name. Returns: str | None: Site ID corresponding to the passed name. None if no match was found. """ for site in self.get_sites(): if site["name"] == name: return str(site["id"]) return None def create_tag( self, name: str, type: str, color: str, filters: list[dict] | None = None, match: str | None = MATCH_DEFAULT_VALUE ) -> dict: """ | Create a new tag. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/createTag Args: name (str): Name of the tag. type (str): Type of the tag. color (str): Color of the tag. filters (list[dict], optional): Filters to apply to the tag. match (str, optional): Match criteria for the filters. Default is MATCH_DEFAULT_VALUE. Returns: dict: API response. """ json_data: dict[str, str | dict] = {"type": type, "name": name, "color": color} if filters: json_data["searchCriteria"] = find_valid_params(filters=filters, match=match) return self._http_request( method="POST", url_suffix="/tags", json_data=json_data, resp_type="json", ) def delete_tag(self, id: int) -> dict: """ | Delete a tag by ID. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/deleteTag Args: id (int): ID of the tag to delete. Returns: dict: API response. """ return self._http_request(method="DELETE", url_suffix=f"/tags/{id}", resp_type="json") def get_tag_by_id(self, id: int) -> dict: """ | Get details of a tag by ID. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getTag Args: id (int): ID of the tag to retrieve. Returns: dict: API response. """ return self._http_request(url_suffix=f"/tags/{id}", method="GET", resp_type="json") def get_tags_list( self, name: str | None = None, type: str | None = None, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, limit: int | None = None, ): """ | Get a list of tags. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getTags Args: name (str, optional): Filter tags by name. type (str, optional): Filter tags by type. page_size (int, optional): Number of results per page. Default is DEFAULT_PAGE_SIZE. page (int, optional): Page number to retrieve. limit (int, optional): Maximum number of results to retrieve. Returns: dict: API response. """ params = assign_params(name=name, type=type) return self._paged_http_request( url_suffix="/tags", method="GET", params=params, page_size=page_size, page=page, limit=limit, resp_type="json", ) def update_tag_search_criteria(self, id: int, filters: list[dict] = [], match: str | None = MATCH_DEFAULT_VALUE) -> dict: """ | Update search criteria for a tag by ID. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html#operation/updateTagSearchCriteria Args: id (int): ID of the tag to update. filters (list[dict], optional): New filters to apply to the tag. match (str, optional): Match criteria for the filters. Default is MATCH_DEFAULT_VALUE. Returns: dict: API response. """ return self._http_request( method="PUT", url_suffix=f"/tags/{id}/search_criteria", json_data=find_valid_params(filters=filters, match=match), resp_type="json", ) def send_http_request(self, method: str, url: str, body: list[Any] | None = None) -> dict: """ | Send a generic HTTP request. | | For more information see: https://help.rapid7.com/insightvm/en-us/api/index.html Args: method (str): HTTP method (e.g., 'GET', 'POST'). url (str): URL suffix for the request. body (list[Any], optional): JSON data to send in the body of the request. Returns: dict: API response. """ request_data: dict[str, Any] = {"method": method, "url_suffix": url, "resp_type": "json"} if body is not None: request_data["json_data"] = body return self._http_request(**request_data) def create_asset_group( self, name: str, type: str, description: str, filters: list[dict] | None = None, match: str | None = MATCH_DEFAULT_VALUE ) -> dict: json_data = assign_params( name=name, type=type, description=description, searchCriteria=find_valid_params(filters=filters, match=match) ) return self._http_request(method="POST", url_suffix="/asset_groups", json_data=json_data, resp_type="json") def get_asset_groups( self, name: str | None = None, type: str | None = None, page_size: int | None = DEFAULT_PAGE_SIZE, page: int | None = None, sort: str | None = None, limit: int | None = None, ) -> list[dict]: params = assign_params(name=name, type=type) return self._paged_http_request( url_suffix="/asset_groups", method="GET", params=params, page_size=page_size, page=page, sort=sort, limit=limit, resp_type="json", ) def get_asset_group_by_id(self, id: int) -> list[dict]: return self._http_request( url_suffix=f"/asset_groups/{id}", method="GET", resp_type="json", ) class InsightVMClient: """ Asynchronous client for interacting with the Rapid7 InsightVM API. Handles session and authentication management. """ def __init__( self, base_url: str, username: str, password: str, token: str = "", verify: bool = True, ): self._base_url = base_url.rstrip("/") self._auth_username = username self._auth_password = password self._auth_token = token self._verify = verify self._headers = { "Accept": "application/json", "Content-Type": "application/json", } self.connection_error_retries = CONNECTION_ERRORS_RETRIES # Add 2FA token to headers if provided if token: self._headers.update({"Token": token}) auth_string = base64.b64encode(f"{username}:{password}".encode()).decode("utf-8") self._headers.update({"Authorization": f"Basic {auth_string}"}) async def __aenter__(self): """Asynchronous context manager entry: creates the aiohttp session.""" # Create a single session that persists for the client's lifespan. # total=None removes aiohttp's default 300s overall deadline so large report downloads # can stream in a single fetch; sock_read still fails fast if the connection stalls. timeout = aiohttp.ClientTimeout( total=None, sock_read=RAPID7_STREAM_SOCK_READ_TIMEOUT_SECONDS, sock_connect=RAPID7_STREAM_SOCK_CONNECT_TIMEOUT_SECONDS, ) self._session = aiohttp.ClientSession(timeout=timeout) return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Asynchronous context manager exit: closes the aiohttp session.""" await self._session.close() async def http_request( self, method: str, endpoint: str, payload: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, ) -> aiohttp.ClientResponse: """ Executes an asynchronous HTTP request and returns the raw response object. Includes retry logic for server errors. Args: method (str): The HTTP method to use (e.g. "GET", "POST"). endpoint (str): The API endpoint (appended to the base URL). payload (Optional[Dict[str, Any]]): Optional JSON body for the request. headers (Optional[Dict[str, str]]): Optional per-request header overrides merged on top of the client's default headers. Used, for example, to request "Accept: text/csv" when downloading a CSV report output, since the default "Accept: application/json" causes the report `/output` endpoint to respond with HTTP 406 (Not Acceptable). """ url = self._base_url + endpoint # Merge any per-request header overrides on top of the client defaults. request_headers = self._headers if not headers else {**self._headers, **headers} # Select the method function (get, post, etc.) request_func = getattr(self._session, method.lower()) MAX_RETRIES = 3 # Retry on: 500/502/503/504 (Server Errors), 429 (Rate Limit) RETRYABLE_STATUSES = {500, 502, 503, 504, 429} for attempt in range(MAX_RETRIES + 1): if attempt > 0: delay = 5**attempt demisto.debug(f"Retrying {method} {endpoint}... Attempt {attempt}/{MAX_RETRIES}. Waiting {delay}s...") await asyncio.sleep(delay) try: response = await request_func(url, headers=request_headers, json=payload, ssl=False) # 1. Handle Retryable Errors if response.status in RETRYABLE_STATUSES: try: message = response.message except AttributeError: message = "No message from Rapid7" demisto.debug(f"API returned retryable status {response.status}: {message}") response.close() # Close connection before retrying continue # 2. Handle Fatal Client Errors (4xx) if 400 <= response.status < 500: try: error_body = await response.text() except Exception: error_body = "Could not read error body." response.close() raise DemistoException(f"Client API Error ({response.status}): {error_body}") # 3. Success (2xx) # Return the RAW response object so the caller can read headers/json as needed. return response except aiohttp.ClientConnectorError as e: demisto.debug(f"Connection error: {e}") if attempt == MAX_RETRIES: raise raise DemistoException(f"API request failed after {MAX_RETRIES} attempts.") class Site: """A class representing a site, which can be identified by ID or name.""" def __init__(self, site_id: str | None = None, site_name: str | None = None, client: Client | None = None) -> None: """ Create a new Site object. Required parameters are either `site_id`, or both `site_name` and `client`. Args: site_id (str | None, optional): ID of the site. site_name (str | None, optional): Name of the site to create an object for. client (Client | None): Client object to use for API requests. Required to fetch ID if only name is provided. Raises: ValueError: If neither of `site_id` and `site_name` was provided, or `site_name` was provided without a `site_id`, and `client` was not provided. InvalidSiteNameException: If no ID was provided and a site with a matching name could not be found. """ self.id: str self.name: str | None = None if site_id: self.id = site_id elif site_name: if client: site_id = client.find_site_id(site_name) if not site_id: raise DemistoException(f'No site with name "{site_name}" was found.') self.id = site_id else: raise ValueError("Can't fetch site ID as no Client was provided.") else: raise ValueError("Either a site ID or a site name must be passed as an argument.") self.name = site_name def convert_asset_search_filters(search_filters: str | list[str]) -> list[dict]: """ | Convert string-based asset search filters to dict-based asset search filters that can be used in Nexpose's API. | | Format specification can be found under "Search Criteria" on: https://help.rapid7.com/insightvm/en-us/api/index.html#section/Overview/Responses Args: search_filters (str | list[str]): List of string-based search filters. Returns: list(dict): List of the same search filters in a dict-based format. """ range_operators = ["in-range", "is-between", "not-in-range"] numeric_operators = ["is-earlier-than", "is-greater-than"] numeric_operators.extend(range_operators) values_field = ["site-id"] if isinstance(search_filters, str): search_filters = [search_filters] normalized_filters = [] for search_filter in search_filters: # Example: risk-score is-between 5,10 # _field = risk-score # _operator = is-greater-than # _value = 5,10 _field, _operator, _value = search_filter.split(" ") values = argToList(_value) if _operator in numeric_operators: values = [float(value) for value in values] filter_dict = { "field": _field, "operator": _operator, } if len(values) > 1: if _operator in range_operators: filter_dict["lower"] = values[0] filter_dict["upper"] = values[1] else: filter_dict["values"] = values elif _field in values_field: filter_dict["values"] = values else: filter_dict["value"] = values[0] normalized_filters.append(filter_dict) return normalized_filters def convert_datetime_str(time_str: str) -> struct_time: """ Convert an ISO 8601 datetime string to a `struct_time` object. Args: time_str (str): A string representing an ISO 8601 datetime. Returns: struct_time: The datetime represented in a `struct_time` object. """ try: return strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ") except ValueError: return strptime(time_str, "%Y-%m-%dT%H:%M:%SZ") def create_credential_creation_body( service: CredentialService, database_name: str | None = None, domain: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: bool | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, snmp_community_name: str | None = None, snmpv3_authentication_type: SNMPv3AuthenticationType | None = None, snmpv3_privacy_password: str | None = None, snmpv3_privacy_type: SNMPv3PrivacyType | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: SSHElevationType | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: bool | None = None, username: str | None = None, ) -> dict: """ Create `account` body for credential-creation API requests. Args: service (CredentialService): Credential service type. database_name (str | None, optional): Database name. domain (str | None, optional): Domain address. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (bool | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. snmp_community_name (str | None, optional): SNMP community for authentication. snmpv3_authentication_type (SNMPv3AuthenticationType): SNMPv3 authentication type for the credential. snmpv3_privacy_password (str | None, optional): SNMPv3 privacy password to use. snmpv3_privacy_type (SNMPv3PrivacyType, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (SSHElevationType | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (bool | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. Returns: dict: `account` body to use for credential-creation API requests. """ missing_params: list[str] = [] special_validation_errors: list[str] = [] account_data: dict = {"service": service.value} s = CredentialService # Simplify object name for shorter lines # Services where "username" field is required if service in ( s.AS400, s.CIFS, s.CIFSHASH, s.CVS, s.DB2, s.FTP, s.HTTP, s.MS_SQL, s.MYSQL, s.ORACLE, s.POP, s.POSTGRESQL, s.REMOTE_EXEC, s.SNMPV3, s.SSH, s.SSH_KEY, s.SYBASE, s.TELNET, ): if username is None: missing_params.append("Username") else: account_data["username"] = username # Services where "password" field is required if service in ( s.AS400, s.CIFS, s.CIFSHASH, s.CVS, s.DB2, s.FTP, s.HTTP, s.MS_SQL, s.MYSQL, s.ORACLE, s.POP, s.POSTGRESQL, s.REMOTE_EXEC, s.SSH, s.SYBASE, s.TELNET, ): if password is None: missing_params.append("Password") else: account_data["password"] = password # Services with optional "useWindowsAuthentication" field. if service in (s.MS_SQL, s.SYBASE) and use_windows_authentication is not None: account_data["useWindowsAuthentication"] = use_windows_authentication # Services with optional "domain" field. if service in (s.AS400, s.CIFS, s.CIFSHASH, s.CVS, s.MS_SQL, s.SYBASE) and domain is not None: if service in (s.MS_SQL, s.SYBASE): if use_windows_authentication: account_data["domain"] = domain else: account_data["domain"] = domain # Services with optional "database" field. if service in (s.DB2, s.MS_SQL, s.MYSQL, s.POSTGRESQL, s.SYBASE) and database_name is not None: account_data["database"] = database_name if service == s.CIFSHASH: if ntlm_hash is None: missing_params.append("NTLM hash") else: account_data["ntlmHash"] = ntlm_hash if service == s.HTTP and http_realm is not None: account_data["realm"] = http_realm if service == s.NOTES and notes_id_password is not None: account_data["notesIDPassword"] = notes_id_password if service == s.ORACLE: if oracle_sid is not None: account_data["sid"] = oracle_sid if oracle_enumerate_sids is not None: account_data["enumerateSids"] = oracle_enumerate_sids if oracle_enumerate_sids and oracle_listener_password is None: missing_params.append("Oracle Listener Password") else: account_data["oracleListenerPassword"] = oracle_listener_password if service == s.SNMP: if snmp_community_name is None: missing_params.append("Community Name") else: account_data["community"] = snmp_community_name if service == s.SNMPV3: if snmpv3_authentication_type is None: missing_params.append("Authentication Type") else: account_data["authenticationType"] = snmpv3_authentication_type.value if snmpv3_authentication_type != SNMPv3AuthenticationType.NO_AUTHENTICATION: if password is None: special_validation_errors.append( f"Password is required for {service.value} services " "when authentication type is set to anything other " 'than "no-authentication".' ) else: account_data["password"] = password if snmpv3_privacy_type is not None: account_data["privacyType"] = snmpv3_privacy_type.value if snmpv3_privacy_type != SNMPv3PrivacyType.NO_PRIVACY and snmpv3_privacy_password is None: special_validation_errors.append( f"Privacy password is required for {service.value} services when the " f"authentication type is set to any value other than " f'"no-authentication", and privacy type is set to any value other ' f'than "no-privacy".' ) else: account_data["privacyPassword"] = snmpv3_privacy_password if service in (s.SSH, s.SSH_KEY): if ssh_permission_elevation: account_data["permissionElevation"] = ssh_permission_elevation.value if ssh_permission_elevation not in (SSHElevationType.NONE, SSHElevationType.PBRUN): missing_elevation_params: list[str] = [] if ssh_permission_elevation_username is None: missing_elevation_params.append("Elevation Username") else: account_data["permissionElevationUsername"] = ssh_permission_elevation_username if ssh_permission_elevation_password is None: missing_elevation_params.append("Elevation Password") else: account_data["permissionElevationPassword"] = ssh_permission_elevation_password if len(missing_elevation_params) > 0: special_validation_errors.append( f"{', '.join(missing_elevation_params)} are required for " f"\"{service.value}\" services when \"ssh_permission_elevation\" " f"is not set to \"none\" or \"pbrun\"." ) if service == s.SSH_KEY: if ssh_key_pem is None: missing_params.append("SSH Key PEM") else: account_data["pemKey"] = ssh_key_pem if ssh_private_key_password is not None: account_data["privateKeyPassword"] = ssh_private_key_password error_message: str = "" if len(missing_params) > 0: error_message += f"Missing required parameters for \"{service.value}\": {', '.join(missing_params)}.\n" if len(special_validation_errors) > 0: for special_validation_error in special_validation_errors: error_message += f"{special_validation_error}\n" if error_message: raise ValueError(error_message.rstrip("\n")) return account_data def create_report( client: Client, scope: dict[str, Any], template_id: str | None = None, report_name: str | None = None, report_format: str | None = None, download_immediately: bool | None = None, ) -> dict | CommandResults: """ Create a report and optionally download it. Args: client (Client): Client to use for API requests. scope (dict[str, Any]): Scope of the report, see Nexpose's documentation for more details. template_id (str | None, optional): ID of report template to use. Defaults to None (will result in using the first available template) report_name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default. report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF. download_immediately: (bool | None, optional) = Whether to download the report automatically after creation. Defaults to True. """ if template_id is None: templates_data = client.get_report_templates() if not templates_data.get("resources"): return CommandResults( readable_output="No available templates were found.", raw_response=templates_data, ) template_id = templates_data["resources"][0]["id"] if report_name is None: report_name = "report " + str(datetime.now()) if not report_format: report_format = "pdf" if download_immediately is None: download_immediately = True report_data = client.create_report_config( scope=scope, template_id=template_id, report_name=report_name, report_format=report_format.lower(), ) instance_data = client.create_report(report_data["id"]) context = { "Name": report_name, "ID": report_data["id"], "InstanceID": instance_data["id"], "Format": report_format.lower(), } hr = tableToMarkdown("Report Information", context) if download_immediately: try: # Wait for the report to be completed time.sleep(REPORT_DOWNLOAD_WAIT_TIME) # pylint: disable=E9003 return download_report_command( client=client, report_id=report_data["id"], instance_id=instance_data["id"], name=report_name, report_format=report_format, ) except Exception as e: # A 404 response could mean that the report generation process has not finished yet. # In that case report's information will be returned to the user for them to download it manually. if "404" not in str(e): raise return CommandResults( readable_output=hr, outputs_prefix="Nexpose.Report", outputs=context, outputs_key_field=["ID", "InstanceID"], raw_response=instance_data, ) def find_asset_last_scan_data(asset_data: dict) -> tuple[str, str]: """ Find the date and ID for the last scan of an asset. Note: `-` is used as a placeholder for missing values instead of `None` because of backwards compatibility. Args: asset_data (dict): A dictionary representing an asset as received from the API. Returns: tuple[str, str]: A tuple containing the date (first value) and ID (seconds value) of the last scan of the asset. """ scan_date = "-" scan_id = "-" if asset_data.get("history"): sorted_scans = sorted(asset_data["history"], key=lambda x: convert_datetime_str(x.get("date")), reverse=True) if "date" in sorted_scans[0]: scan_date = sorted_scans[0]["date"] if "scanId" in sorted_scans[0]: scan_id = sorted_scans[0]["scanId"] return scan_date, scan_id def find_valid_params(strict_mode: bool = False, **kwargs) -> dict: """ A function for filtering dictionaries (passed as kwargs) to remove keys that have a None value. Args: strict_mode (bool, optional): If set to true, keys with a False value (e.g. [], {}, '', False) will be removed as well. kwargs: A collection of keyword args to filter. Returns: dict: A dictionary containing only keywords with a value that isn't None. """ new_kwargs = {} for key, value in kwargs.items(): if (strict_mode and value) or (not strict_mode and value is not None): new_kwargs[key] = value return new_kwargs def get_scan_entry(scan: dict) -> CommandResults: """ Generate entry data from scan data (as received from the API). Args: scan (dict): Scan data as it was received from the API. Returns: CommandResults: Scan data in a normalized format that will be displayed in the UI. """ scan_output = normalize_scan_data(scan) vulnerability_headers = [ "Critical", "Severe", "Moderate", "Total", ] vulnerability_output = generate_new_dict( data=scan["vulnerabilities"], name_mapping={ "critical": "Critical", "severe": "Severe", "moderate": "Moderate", "total": "Total", }, include_none=True, ) scan_hr = tableToMarkdown( name=f"Nexpose Scan ID {scan['id']!s}", t=scan_output, headers=[ "Id", "ScanType", "ScanName", "StartedBy", "Assets", "TotalTime", "Completed", "Status", "Message", ], removeNull=True, ) scan_hr += tableToMarkdown("Vulnerabilities", vulnerability_output, vulnerability_headers, removeNull=True) scan_output["Vulnerabilities"] = vulnerability_output return CommandResults( outputs_prefix="Nexpose.Scan", outputs_key_field="Id", outputs=scan_output, readable_output=scan_hr, raw_response=scan, ) def generate_duration_time( years: int | None = None, months: int | None = None, weeks: int | None = None, days: int | None = None, hours: int | None = None, minutes: int | None = None, seconds: float | None = None, ) -> str | None: """ | Generate an ISO 8601 duration string. | More info about format's specification can be found on: https://en.wikipedia.org/wiki/ISO_8601#Durations | | If an overflow of a time unit occurs, the next unit will be incremented. | For months, 4 weeks are will be added to a month, even though months have variable length. Args: years (int | None, optional): Duration years. months (int | None, optional): Duration months. weeks (int | None, optional): Duration weeks. days (int | None, optional): Duration days. hours (int | None, optional): Duration hours. minutes (int | None, optional): Duration minutes. seconds (float | None, optional): Duration seconds. Returns: str: The duration represented in an ISO 8601 duration string. """ if not any((years, months, weeks, days, hours, minutes, seconds)): return None duration_str = "P" if years: duration_str += f"{years}Y" if months: duration_str += f"{months}M" if weeks: duration_str += f"{weeks}W" if days: duration_str += f"{days}D" if hours or minutes or seconds: duration_str += "T" if hours: duration_str += f"{hours}H" if minutes: duration_str += f"{minutes}M" if seconds: duration_str += f"{seconds}S" return duration_str def normalize_scan_data(scan_data: dict) -> dict: """ Normalizes scan data received from the API to a HumanReadable format that will be displayed in the UI. Args: scan_data (dict): Scan data as it was received from the API. Returns: dict: Scan data in a normalized format that will be displayed in the UI. """ result = generate_new_dict( data=scan_data, name_mapping={ "id": "Id", "scanType": "ScanType", "scanName": "ScanName", "startedBy": "StartedBy", "assets": "Assets", "endTime": "Completed", "status": "Status", "message": "Message", }, include_none=True, ) if scan_data.get("duration"): result["TotalTime"] = readable_duration_time(scan_data["duration"]) else: result["TotalTime"] = "No duration data was found." return result def readable_duration_time(duration: str) -> str: """ | Convert an ISO 8601 duration string to a human-readable string format. | More info about format's specification can be found on: https://en.wikipedia.org/wiki/ISO_8601#Durations Args: duration (str): An ISO 8601 duration string. Returns: str: The duration represented in a human-readable string format. """ # Assure duration is in a valid format if not re.fullmatch(r"P(?:[\d.]+[YMWD]){0,4}T(?:[\d.]+[HMS]){0,3}", duration): raise ValueError(f'"{duration}" is not a valid ISO 8601 duration string.') p_duration, t_duration = duration.replace("T", ",T").split(",") p_duration = re.findall(r"([\d.]+[A-Z])", p_duration) t_duration = re.findall(r"([\d.]+[A-Z])", t_duration) duration_mapping_p = { "Y": "years", "M": "months", "W": "weeks", "D": "days", } duration_mapping_t = { "H": "hours", "M": "minutes", "S": "seconds", } duration_values: dict = { "years": 0, "months": 0, "weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0, } for item in p_duration: designator = item[-1] number_float = float(item[:-1]) if number_float.is_integer(): number_int = int(number_float) else: number_int = round(number_float) duration_values[duration_mapping_p[designator]] = number_int for item in t_duration: designator = item[-1] number_float = float(item[:-1]) if number_float.is_integer(): duration_values[duration_mapping_t[designator]] = int(number_float) else: duration_values[duration_mapping_t[designator]] = number_float result = [] for item in duration_values: zero_up_to_now = True if duration_values[item] > 0: zero_up_to_now = False if not zero_up_to_now: result += [f"{duration_values[item]} {item}"] return ", ".join(result) @overload def remove_dict_key(data: dict, key: Any) -> dict: # pragma: no cover pass @overload def remove_dict_key(data: list, key: Any) -> list: # pragma: no cover pass @overload def remove_dict_key(data: tuple, key: Any) -> tuple: # pragma: no cover pass def remove_dict_key(data: dict | list | tuple, key: Any) -> dict | list | tuple: """ Recursively remove a dictionary key from an object Args: data (dict | list | tuple): A dictionary or an iterable to remove keys for dictionaries within it. key (Any): Key to remove from dictionaries. Returns: dict | list | tuple: The data-structure (original or copy) with the specified key name removed from all dictionaries within. """ if isinstance(data, dict): if key in data: del data[key] for k in data: remove_dict_key(data[k], key) if isinstance(data, list | tuple): for item in data: remove_dict_key(item, key) return data @overload def generate_new_dict(data: dict, name_mapping: dict[str, str], include_none: bool = False) -> dict: # pragma: no cover pass @overload def generate_new_dict(data: list, name_mapping: dict[str, str], include_none: bool = False) -> list: # pragma: no cover pass def generate_new_dict(data: dict | list, name_mapping: dict[str, str], include_none: bool = False) -> dict | list | tuple: """ Generate a new dictionary from an existing dictionary, with the keys renamed according to `name_mapping`. Args: data (dict | list): The dictionary to generate a new dictionary from. If a list is passed, the function will run recursively on each item in the list. name_mapping (dict[str, str]): A mapping between old key names to the new key names in a `key-path: new-key` format. include_none (bool, optional): Whether to include keys with `None` values in the new dictionary. """ if isinstance(data, dict): new_dict = {} for key_path, new_key in name_mapping.items(): value = find_dict_item(data, key_path) if include_none or value is not None: new_dict[new_key] = value return new_dict elif isinstance(data, list): return [generate_new_dict(item, name_mapping, include_none) for item in data] elif isinstance(data, tuple): return tuple(generate_new_dict(item, name_mapping, include_none) for item in data) else: return data def find_dict_item(data: dict | list | tuple, key_path: str) -> Any: """ Find a dictionary item by its key path. Note: This code snippet assumes that `data` does not contain None values. If it does, None values will be returned both if the value is None, or if the key couldn't be found. Args: data (dict | list | tuple): A dictionary, a list, or a tuple to search for the key path in. key_path (str): The key path to search for. Keys are separated by a dot (.) character. Returns: Any: The value of the key path if found, None otherwise. """ if isinstance(data, dict): key_path_list = key_path.split(".") if key_path_list[0] in data: if len(key_path_list) == 1: return data[key_path_list[0]] else: return find_dict_item( data=data[key_path_list[0]], key_path=".".join(key_path_list[1:]), ) else: return None elif isinstance(data, list | tuple): result = [ find_dict_item( data=item, key_path=key_path, ) for item in data ] return [item for item in result if item is not None] return None def parse_asset_filters(client, **kwargs): """ Parse and generate a list of asset filters based on provided keyword arguments. Args: client (Client): Client to use for API requests. **kwargs: Arbitrary keyword arguments representing filter criteria. - ip_address_is (str): A specific IP address to filter assets by. - host_name_is (str): A specific host name to filter assets by. - risk_score_higher_than (str): A minimum risk score to filter assets by. - vulnerability_title_contains (str): A keyword to filter assets by vulnerability title. - query (str): A semicolon-separated list of custom query strings. - site_id_in (str): A comma-separated list of site IDs to filter assets by. - site_name_in (str): A comma-separated list of site names to filter assets by. Returns: list[str]: A list of asset filter strings. """ filters_data: list[str] = [] if kwargs.get("ip_address_is"): filters_data.append("ip-address is " + kwargs["ip_address_is"]) if kwargs.get("host_name_is"): filters_data.append("host-name is " + kwargs["host_name_is"]) if kwargs.get("risk_score_higher_than"): filters_data.append("risk-score is-greater-than " + kwargs["risk_score_higher_than"]) if kwargs.get("vulnerability_title_contains"): filters_data.append("vulnerability-title contains " + kwargs["vulnerability_title_contains"]) if kwargs.get("query"): filters_data.extend(kwargs["query"].split(";")) sites: list[Site] = [] for site_id in argToList(kwargs.get("site_id_in")): sites.append(Site(site_id=site_id, client=client)) for site_name in argToList(kwargs.get("site_name_in")): sites.append(Site(site_name=site_name, client=client)) if sites: str_site_ids: str = "" if isinstance(sites, list): str_site_ids = ",".join([site.id for site in sites]) elif isinstance(sites, Site): str_site_ids = sites.id filters_data.append("site-id in " + str_site_ids) return filters_data def validate_input(input_value: str | None, valid_options: list[str], arg_name: str, is_required: bool = True): """ Validates the input value against a list of valid options. Args: input_value (str | None): The input value to validate. valid_options (list[str]): The list of valid options. parameter_name (str): The name of the parameter being validated (used in the error message). is_required (bool): Whether the input value is required. Defaults to True. Raises: DemistoException: If the input value is invalid or not in the list of valid options. Returns: bool: True if the input value is valid or not required and None is provided. """ if input_value is None and not is_required: return True elif not input_value or input_value.lower() not in valid_options: raise DemistoException(f"{input_value} is an invalid {arg_name} the only options are: {', '.join(valid_options)}") return True # --- Command Functions --- # def create_asset_command( client: Client, date: str, site_id: str | None = None, site_name: str | None = None, ip: str | None = None, host_name: str | None = None, host_name_source: str | None = None, ) -> CommandResults: """ Create a new asset. Args: client (Client): The client to use. date (str): The date the data was collected on the asset. site_id (str | None, optional): Name of the site to create the asset in. site_name (str | None, optional): Name of the site to create the asset in. Can be used instead of "site_id". ip (str | None, optional): The IP address of the asset. host_name (str | None, optional): The hostname of the asset. host_name_source (str | None, optional): The source of the hostname. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) response_data = client.create_asset( site_id=site.id, date=date, ip_address=ip, hostname=host_name, hostname_source=host_name_source, ) return CommandResults( readable_output=f"New asset has been created with ID {response_data['id']}.", outputs_prefix="Nexpose.Asset", outputs_key_field="id", outputs={"id": response_data["id"]}, raw_response=response_data, ) def create_assets_report_command( client: Client, assets: str, template: str | None = None, name: str | None = None, report_format: str | None = None, download_immediately: str | None = None, ) -> dict | CommandResults: """ Create a report about specific assets. Args: client (Client): Client to use for API requests. assets (str): List of assets to include in the report. template (str | None, optional): ID of report template to use. Defaults to None (will result in using the first available template) name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default. report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF. download_immediately: (str | None, optional) = Whether to download the report automatically after creation. Defaults to True. """ download_immediately_bool = None asset_ids_list: list[str] = argToList(assets) if download_immediately is not None: download_immediately_bool = argToBoolean(download_immediately) scope = {"assets": [int(asset_id) for asset_id in asset_ids_list]} return create_report( client=client, scope=scope, template_id=template, report_name=name, report_format=report_format, download_immediately=download_immediately_bool, ) def create_scan_report_command( client: Client, scan: str, template: str | None = None, name: str | None = None, report_format: str | None = None, download_immediately: str | None = None, ) -> dict | CommandResults: """ Create a report about specific sites. Args: client (Client): Client to use for API requests. scan (str): ID of the scan to create a report on. template (str | None, optional): ID of report template to use. Defaults to None (will result in using the first available template) name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default. report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF. download_immediately: (str | None, optional) = Whether to download the report automatically after creation. Defaults to True. """ download_immediately_bool = None if download_immediately is not None: download_immediately_bool = argToBoolean(download_immediately) scope = {"scan": arg_to_number(scan, required=True)} return create_report( client=client, scope=scope, template_id=template, report_name=name, report_format=report_format, download_immediately=download_immediately_bool, ) def create_scan_schedule_command( client: Client, on_scan_repeat: str, start: str, site_id: str | None = None, site_name: str | None = None, excluded_asset_groups: str | None = None, excluded_targets: str | None = None, included_asset_groups: str | None = None, included_targets: str | None = None, duration_days: str | None = None, duration_hours: str | None = None, duration_minutes: str | None = None, enabled: str | None = None, frequency: str | None = None, interval_time: str | None = None, scan_name: str | None = None, date_of_month: int | None = None, scan_template_id: str | None = None, ) -> CommandResults: """ Create a new site scan schedule. Args: client (Client): Client to use for API requests. on_scan_repeat (str): The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. start (str): The scheduled start date and time formatted in ISO 8601 format. site_id (str | None, optional): ID of the site to create a scheduled scan for. site_name (str | None, optional): Name of the site to create a scheduled scan for. Can be used instead of "site_id". excluded_asset_groups (str | None, optional): Asset groups to exclude from the scan. excluded_targets (str | None, optional): Addresses to exclude from the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. included_asset_groups (str | None, optional): Asset groups to include in the scan. included_targets (str | None, optional): Addresses to include in the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. duration_days (str | None, optional): Maximum duration of the scan in days. Can be used along with `duration_hours` and `duration_minutes`. duration_hours (str | None, optional): Maximum duration of the scan in hours. Can be used along with `duration_days` and `duration_minutes`. duration_minutes (str | None, optional): Maximum duration of the scan in minutes. Can be used along with `duration_days` and `duration_hours`. enabled (str | None, optional): A flag indicating whether the scan schedule is enabled. Defaults to None, which results in using True. frequency (str | None, optional): Frequency for the schedule to repeat. Required if using other repeat settings. interval_time (str | None, optional): The interval time the schedule should repeat. Required if using other repeat settings. date_of_month(str | None, optional): Specifies the schedule repeat day of the interval month. Required and used only if frequency is set to `DATE_OF_MONTH`. scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. scan_template_id (str | None, optional): ID of the scan template to use. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) excluded_asset_groups_list = None excluded_targets_list = None frequency_enum = None included_asset_groups_list = None included_targets_list = None if excluded_asset_groups is not None: excluded_asset_groups_list = [int(asset_id) for asset_id in argToList(excluded_asset_groups)] if excluded_targets is not None: excluded_targets_list = argToList(excluded_targets) if frequency is not None: frequency_enum = RepeatFrequencyType[frequency] if included_asset_groups is not None: included_asset_groups_list = [int(asset_id) for asset_id in argToList(included_asset_groups)] if included_targets is not None: included_targets_list = argToList(included_targets) duration_days_int = arg_to_number(duration_days, required=False) duration_hours_int = arg_to_number(duration_hours, required=False) duration_minutes_int = arg_to_number(duration_minutes, required=False) interval_int = arg_to_number(interval_time, required=False) date_of_month_int = arg_to_number(date_of_month, required=False) if enabled is not None: enabled_bool = argToBoolean(enabled) else: enabled_bool = True duration = generate_duration_time( days=duration_days_int, hours=duration_hours_int, minutes=duration_minutes_int, ) response_data = client.create_site_scan_schedule( site_id=site.id, enabled=enabled_bool, repeat_behaviour=on_scan_repeat, start_date=start, excluded_asset_groups=excluded_asset_groups_list, excluded_targets=excluded_targets_list, included_asset_groups=included_asset_groups_list, included_targets=included_targets_list, duration=duration, frequency=frequency_enum, interval=interval_int, date_of_month=date_of_month_int, scan_name=scan_name, scan_template_id=scan_template_id, ) return CommandResults( readable_output=f"New scheduled scan has been created with ID {response_data['id']}.", outputs_prefix="Nexpose.ScanSchedule", outputs_key_field="id", outputs={"id": response_data["id"]}, raw_response=response_data, ) def create_shared_credential_command( client: Client, name: str, site_assignment: str, service: str, database: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: str | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, sites: str | None = None, authentication_type: str | None = None, community_name: str | None = None, privacy_password: str | None = None, privacy_type: str | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: str | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: str | None = None, username: str | None = None, ) -> CommandResults: """ Create a new shared credential. Args: client (Client): Client to use for API requests. name (str): Name of the credential. site_assignment (str): Site assignment configuration for the credential. service (str): Credential service type. database (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. sites (str | None, optional): List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. authentication_type (str | None, optional): SNMPv3 authentication type for the credential. community_name (str | None, optional): SNMP community for authentication. privacy_password (str | None, optional): SNMPv3 privacy password to use. privacy_type (str | None, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (str | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (str | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. """ oracle_enumerate_sids_bool = None sites_list = None snmpv3_authentication_type_enum = None snmpv3_privacy_type_enum = None ssh_permission_elevation_enum = None use_windows_authentication_bool = None if oracle_enumerate_sids is not None: oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids) if sites is not None: sites_list = [int(item) for item in argToList(sites)] if authentication_type is not None: snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type] if privacy_type is not None: snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type] if ssh_permission_elevation is not None: ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation] if use_windows_authentication is not None: use_windows_authentication_bool = argToBoolean(use_windows_authentication) response_data = client.create_shared_credential( name=name, site_assignment=SharedCredentialSiteAssignment[site_assignment], service=CredentialService[service], database_name=database, description=description, domain=domain, host_restriction=host_restriction, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids_bool, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, port_restriction=port_restriction, sites=sites_list, snmp_community_name=community_name, snmpv3_authentication_type=snmpv3_authentication_type_enum, snmpv3_privacy_password=privacy_password, snmpv3_privacy_type=snmpv3_privacy_type_enum, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation_enum, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication_bool, username=username, ) return CommandResults( readable_output=f"New shared credential has been created with ID {response_data['id']}.", outputs_prefix="Nexpose.SharedCredential", outputs_key_field="id", outputs={"id": response_data["id"]}, raw_response=response_data, ) def create_site_command( client: Client, name: str, description: str | None = None, assets: str | None = None, importance: str | None = None, template_id: str | None = None, ) -> CommandResults: """ Create a new site. Args: client (Client): Client to use for API requests. name (str): Name of the site. Must be unique. description (str | None, optional): Description of the site. Defaults to None. assets (str | None, optional): List of asset IDs to be included in site scans. Defaults to None. importance (str | None, optional): Importance of the site. Defaults to None (results in using API's default - "normal"). template_id (str | None, optional): The identifier of a scan template. Defaults to None (results in using default scan template). """ assets_list = None if assets is not None: assets_list = argToList(assets) response_data = client.create_site( name=name, description=description, assets=assets_list, site_importance=importance, template_id=template_id ) return CommandResults( readable_output=f"New site has been created with ID {response_data['id']}.", outputs_prefix="Nexpose.Site", outputs_key_field="Id", outputs={"Id": response_data["id"]}, raw_response=response_data, ) def create_sites_report_command( client: Client, sites: str | None = None, site_names: str | None = None, template: str | None = None, name: str | None = None, report_format: str | None = None, download_immediately: str | None = None, ) -> dict | CommandResults: """ Create a report about specific sites. Args: client (Client): Client to use for API requests. sites (str | None, optional): List of site IDs to create the report about. site_names (str | None, optional): List of site names to create the report about. template (str | None, optional): ID of report template to use. Defaults to None (will result in using the first available template) name (str | None, optional): Name for the report that will be generated. Uses "report {date}" by default. report_format (str | None, optional): Format of the report that will be generated. Defaults to PDF. download_immediately: (str | None, optional) = Whether to download the report automatically after creation. Defaults to True. """ sites_list = [Site(site_id=site_id, client=client) for site_id in argToList(sites)] sites_list.extend([Site(site_name=site_name, client=client) for site_name in argToList(site_names)]) if len(sites_list) == 0: raise Exception("At least one site ID or site name must be provided.") download_immediately_bool = None if download_immediately is not None: download_immediately_bool = argToBoolean(download_immediately) scope = {"sites": [int(site.id) for site in sites_list]} return create_report( client=client, scope=scope, template_id=template, report_name=name, report_format=report_format, download_immediately=download_immediately_bool, ) def create_site_scan_credential_command( client: Client, name: str, service: str, site_id: str | None = None, site_name: str | None = None, authentication_type: str | None = None, community_name: str | None = None, database: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: str | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, privacy_password: str | None = None, privacy_type: str | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: str | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: str | None = None, username: str | None = None, ) -> CommandResults: """ Create a new site scan credential. Args: client (Client): Client to use for API requests. name (str): Name of the credential. service (str): Credential service type. site_id (str | None, optional): ID of a site to create the credential for. site_name (str | None, optional): Name of a site to create the credential for. Can be used instead of "site_id". authentication_type (str): SNMPv3 authentication type for the credential. community_name (str | None, optional): SNMP community for authentication. database (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. privacy_password (str | None, optional): SNMPv3 privacy password to use. privacy_type (str | None, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (str | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (str | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) oracle_enumerate_sids_bool = None snmpv3_authentication_type_enum = None snmpv3_privacy_type_enum = None ssh_permission_elevation_enum = None use_windows_authentication_bool = None if oracle_enumerate_sids is not None: oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids) if authentication_type is not None: snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type] if privacy_type is not None: snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type] if ssh_permission_elevation is not None: ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation] if use_windows_authentication is not None: use_windows_authentication_bool = argToBoolean(use_windows_authentication) response_data = client.create_site_scan_credential( site_id=site.id, name=name, service=CredentialService[service], database_name=database, description=description, domain=domain, host_restriction=host_restriction, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids_bool, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, port_restriction=port_restriction, snmp_community_name=community_name, snmpv3_authentication_type=snmpv3_authentication_type_enum, snmpv3_privacy_password=privacy_password, snmpv3_privacy_type=snmpv3_privacy_type_enum, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation_enum, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication_bool, username=username, ) return CommandResults( readable_output=f"New site scan credential has been created with ID {response_data['id']}.", outputs_prefix="Nexpose.SiteScanCredential", outputs_key_field="id", outputs={"id": response_data["id"]}, raw_response=response_data, ) def create_vulnerability_exception_command( client: Client, vulnerability_id: str, scope_type: str, state: str, reason: str, scope_id: str | None = None, expires: str | None = None, comment: str | None = None, ) -> CommandResults: """ Create a vulnerability exception. Args: client (Client): Client to use for API requests. vulnerability_id (str): ID of the vulnerability to create the exception for. scope_type (str): The type of the exception scope. state (str): The state of the vulnerability exception. reason (str): The reason the vulnerability exception was submitted. scope_id (int): ID of the chosen `scope_type` (site ID, asset ID, etc.). Required if `scope_type` is anything other than `Global` expires (str | None, optional): The date and time the vulnerability exception is set to expire. comment (str | None, optional): A comment from the submitter as to why the exception was submitted. """ scope_type_enum = VulnerabilityExceptionScopeType[scope_type] if scope_type_enum != VulnerabilityExceptionScopeType.GLOBAL and scope_id is None: raise ValueError( f'"scope_id" must be set when using scopes different than "{VulnerabilityExceptionScopeType.GLOBAL.value}".' ) response_data = client.create_vulnerability_exception( vulnerability_id=vulnerability_id, scope_type=scope_type_enum, state=state, reason=reason, scope_id=int(scope_id) if scope_id is not None else None, expires=expires, comment=comment, ) return CommandResults( readable_output=f"New vulnerability exception has been created with ID {response_data['id']!s}.", outputs_prefix="Nexpose.VulnerabilityException", outputs_key_field="id", outputs={"id": response_data["id"]}, raw_response=response_data, ) def delete_asset_command(client: Client, asset_id: str) -> CommandResults: """ Delete an asset. Args: client (Client): Client to use for API requests. asset_id (str): ID of the asset to delete. """ response_data = client.delete_asset(asset_id=asset_id) return CommandResults( readable_output=f"Asset {asset_id} has been deleted.", raw_response=response_data, ) def delete_scan_schedule_command( client: Client, schedule_id: str, site_id: str | None = None, site_name: str | None = None ) -> CommandResults: """ Delete a scheduled scan. Args: client (Client): Client to use for API requests. schedule_id (str): ID of the scheduled scan to delete. site_id (str | None, optional): ID of the site to delete the scheduled scan from. site_name (str | None, optional): Name of the site to delete the scheduled scan from. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) response_data = client.delete_scan_schedule( site_id=site.id, scheduled_scan_id=schedule_id, ) return CommandResults( readable_output=f"Scheduled scan with ID {schedule_id} has been deleted.", raw_response=response_data, ) def delete_site_command(client: Client, site_id: str | None = None, site_name: str | None = None) -> CommandResults: """ Delete a site. Args: client (Client): Client to use for API requests. site_id (str | None, optional): ID of a site to delete. site_name (str | None, optional): Name of a site to delete. Can be used instead of "site_id". """ site = Site( site_id=site_id, site_name=site_name, client=client, ) response_data = client.delete_site(site.id) return CommandResults( readable_output=f"Site ID {site.id} has been deleted.", outputs_prefix="Nexpose.Report", outputs_key_field=["ID", "InstanceID"], raw_response=response_data, ) def delete_shared_credential_command(client: Client, shared_credential_id: str) -> CommandResults: """ Delete a shared credential. Args: client (Client): Client to use for API requests. shared_credential_id (str): ID of the shared credential to delete. """ response_data = client.delete_shared_credential(shared_credential_id) return CommandResults( readable_output=f"Shared credential with ID {shared_credential_id} has been deleted.", raw_response=response_data, ) def delete_site_scan_credential_command( client: Client, credential_id: str, site_id: str | None = None, site_name: str | None = None ) -> CommandResults: """ Delete a site scan credential. Args: client (Client): Client to use for API requests. credential_id (str): ID of the site scan credential to delete. site_id (str | None, optional): ID of the site to delete the site scan credential from. site_name (str | None, optional): Name of the site to delete the site scan credential from. Can be used instead of "site_id". """ site = Site( site_id=site_id, site_name=site_name, client=client, ) response_data = client.delete_site_scan_credential( site_id=site.id, site_credential_id=credential_id, ) return CommandResults( readable_output=f"Site scan credential with ID {credential_id} has been deleted.", raw_response=response_data, ) def delete_vulnerability_exception_command(client: Client, vulnerability_exception_id: str) -> CommandResults: """ Delete a vulnerability exception. Args: client (Client): Client to use for API requests. vulnerability_exception_id (str): ID of the vulnerability exception to delete. """ response_data = client.delete_vulnerability_exception(vulnerability_exception_id) return CommandResults( readable_output=f"Vulnerability exception with ID {vulnerability_exception_id} has been deleted.", raw_response=response_data, ) def download_report_command( client: Client, report_id: str, instance_id: str, name: str | None = None, report_format: str | None = None ) -> dict: """ Download a report file. Note: Not sure why there's a report_format parameter, as the format is set when generating the report, and all the parameter seems to do here, is just change the file extension (which is obviously not how file conversion works). This function currently remains as it is, since removing it might break client's using it for some reason. Args: client (Client): Client to use for API requests. report_id (str): ID of the report to download. instance_id (str): ID of the report instance. name (str | None, optional): Name to give the generated report file. Defaults to None (results in using a "report <date>" format as a name). report_format (str | None, optional): File format to use for the generated report. Defaults to None (results in using PDF). Returns: dict: A dict generated by `CommonServerPython.fileResult` representing a War Room entry. """ if name is None: name = f"report {datetime.now()!s}" if not report_format: report_format = "pdf" report_data = client.download_report(report_id=report_id, instance_id=instance_id) return fileResult( filename=f"{name}.{report_format.lower()}", data=report_data, file_type=entryTypes["entryInfoFile"], ) def get_asset_tags_command(client: Client, asset_id: str) -> CommandResults | list[CommandResults]: """ Retrieve tags associated to an asset. Args: client (Client): Client to use for API requests. asset_id (str): ID of the asset to retrieve information about. """ tags = [] try: tag_raw_data = client.get_asset_tags(asset_id) except DemistoException as e: if e.res is not None and e.res.status_code is not None and e.res.status_code == 404: return CommandResults(readable_output="Asset not found.") for tag in tag_raw_data.get("resources", []): tag_output = generate_new_dict( data=tag, name_mapping={ "type": "Type", "riskModifier": "RiskModifier", "name": "Name", "created": "CreatedTime", }, include_none=True, ) tags.append(tag_output) readable_output = tableToMarkdown( name=f"Nexpose Asset Tags for Asset {asset_id}", t=tags, headers=["Type", "Name", "Risk Modifier", "Created Time"], ) result = CommandResults( readable_output=readable_output, outputs_prefix="Nexpose.AssetTag", outputs=tags, outputs_key_field="type", raw_response=tag_raw_data, ) return result def get_asset_command(client: Client, asset_id: str) -> CommandResults | list[CommandResults]: """ Retrieve information about an asset. Args: client (Client): Client to use for API requests. asset_id (str): ID of the asset to retrieve information about. """ hr_asset_headers = [ "AssetId", "Addresses", "Hardware", "Aliases", "HostType", "Site", "OperatingSystem", "CPE", "LastScanDate", "LastScanId", "RiskScore", ] hr_service_headers = [ "Name", "Port", "Product", "Protocol", ] hr_software_headers = [ "Software", "Version", ] hr_users_headers = [ "FullName", "Name", "UserId", ] hr_vulnerability_headers = [ "Id", "Title", "Malware", "Exploit", "CVSS", "Risk", "PublishedOn", "ModifiedOn", "Severity", "Instances", ] try: asset_data = client.get_asset(asset_id) except DemistoException as e: if e.res is not None and e.res.status_code is not None and e.res.status_code == 404: return CommandResults(readable_output="Asset not found.") raise e asset_output = generate_new_dict( data=asset_data, name_mapping={ "id": "AssetId", "ip": "Address", "addresses.ip": "Addresses", "addresses.mac": "Hardware", "hostNames.name": "Aliases", "type": "HostType", "Site": "Site", "os": "OperatingSystem", "vulnerabilities.total": "Vulnerabilities", "cpe.v2.3": "CPE", "riskScore": "RiskScore", }, include_none=True, ) site = client.find_asset_site(asset_data["id"]) if site is not None: asset_output["Site"] = site.name asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset_data) asset_output["Software"] = None asset_output["Service"] = None asset_output["User"] = None if asset_data.get("software"): asset_output["Software"] = generate_new_dict( data=asset_data["software"], name_mapping={ "description": "Software", "version": "Version", }, include_none=True, ) if asset_data.get("services"): asset_output["Service"] = generate_new_dict( data=asset_data["services"], name_mapping={ "name": "Name", "port": "Port", "product": "Product", "protocol": "Protocol", }, include_none=True, ) if asset_data.get("users"): asset_output["User"] = generate_new_dict( data=asset_data["users"], name_mapping={ "name": "Name", "fullName": "FullName", "id": "UserId", }, include_none=True, ) vulnerabilities = client.get_asset_vulnerabilities(asset_id=str(asset_data["id"])) asset_output["Vulnerability"] = [] cve_indicators: list[CommandResults] = [] for vulnerability in vulnerabilities: extra_info = client.get_vulnerability(vulnerability["id"]) vulnerability_output = { "Id": vulnerability["id"], "Title": extra_info["title"], "Malware": extra_info["malwareKits"], "Exploit": extra_info["exploits"], "CVSS": extra_info["cvss"]["v2"]["score"], "Risk": extra_info["riskScore"], "PublishedOn": extra_info["published"], "ModifiedOn": extra_info["modified"], "Severity": extra_info["severity"], "Instances": vulnerability["instances"], } asset_output["Vulnerability"].append(vulnerability_output) if "cves" in extra_info: for cve in extra_info["cves"]: if "v3" in extra_info["cvss"]: cvss_info = extra_info["cvss"]["v3"] cvss_version = "3" else: cvss_info = extra_info["cvss"]["v2"] cvss_version = "2" cve_indicators.append( CommandResults( readable_output=tableToMarkdown(cve, vulnerability_output, hr_vulnerability_headers, removeNull=True), indicator=Common.CVE( id=cve, cvss=None, # type: ignore cvss_score=cvss_info.get("score"), cvss_vector=cvss_info.get("vector"), cvss_version=cvss_version, description=extra_info["description"]["text"], modified=extra_info["modified"], published=extra_info["published"], ), ) ) readable_output = tableToMarkdown( name=f"Nexpose Asset {asset_data['id']!s}", t=asset_output, headers=hr_asset_headers, removeNull=True ) if asset_output.get("Vulnerability"): readable_output += tableToMarkdown( name="Vulnerabilities", t=asset_output["Vulnerability"], headers=hr_vulnerability_headers, removeNull=True ) if asset_output.get("Software"): readable_output += tableToMarkdown( name="Software", t=asset_output["Software"], headers=hr_software_headers, removeNull=True ) if asset_output.get("Service"): readable_output += tableToMarkdown( name="Services", t=asset_output["Service"], headers=hr_service_headers, removeNull=True ) if asset_output.get("User"): readable_output += tableToMarkdown(name="Users", t=asset_output["User"], headers=hr_users_headers, removeNull=True) result = CommandResults( readable_output=readable_output, outputs_prefix="Nexpose.Asset", outputs=asset_output, outputs_key_field="AssetId", indicator=Common.Endpoint( id=asset_output.get("AssetId"), hostname=asset_output.get("Aliases"), ip_address=asset_output.get("Addresses"), os=asset_output.get("OperatingSystem"), vendor=VENDOR_NAME, ), raw_response=asset_data, ) if cve_indicators: return [*cve_indicators, result] return result def get_assets_command( client: Client, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None ) -> CommandResults | list[CommandResults]: """ Retrieve a list of all assets. Args: client (Client): Client to use for API requests. page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) hr_headers = [ "AssetId", "Address", "Name", "Site", "Exploits", "Malware", "OperatingSystem", "Vulnerabilities", "RiskScore", "Assessed", "LastScanDate", "LastScanId", ] assets_data = client.get_assets(page_size=page_size_int, page=page_int, sort=sort, limit=limit_int) if not assets_data: return CommandResults(readable_output="No assets found", raw_response=assets_data) results = [] for asset_data in assets_data: asset_output = generate_new_dict( data=asset_data, name_mapping={ "id": "AssetId", "ip": "Address", "addresses.ip": "Addresses", "hostName": "Name", "Site": "Site", "vulnerabilities.exploits": "Exploits", "vulnerabilities.malwareKits": "Malware", "os": "OperatingSystem", "vulnerabilities.total": "Vulnerabilities", "riskScore": "RiskScore", "assessedForVulnerabilities": "Assessed", }, include_none=True, ) site = client.find_asset_site(asset_data["id"]) if site is not None: asset_output["Site"] = site.name asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset_data) results.append( CommandResults( outputs_prefix="Nexpose.Asset", outputs_key_field="Id", outputs=asset_output, readable_output=tableToMarkdown(f"Nexpose Asset {asset_data['id']!s}", asset_output, hr_headers, removeNull=True), raw_response=asset_data, indicator=Common.Endpoint( id=asset_data["id"], hostname=asset_data.get("hostName"), ip_address=asset_data.get("ip"), mac_address=asset_data.get("mac"), os=asset_data.get("os"), vendor=VENDOR_NAME, ), ) ) return results def get_asset_vulnerability_command( client: Client, asset_id: str, vulnerability_id: str ) -> CommandResults | list[CommandResults]: """ Retrieve information about vulnerability findings on an asset. Args: client (Client): Client to use for API requests. asset_id (str): ID of the asset to retrieve information about. vulnerability_id (str): ID of the vulnerability to look for """ hr_vulnerability_headers = [ "Id", "Title", "Severity", "RiskScore", "CVSS", "CVSSV3", "Published", "Added", "Modified", "CVSSScore", "CVSSV3Score", "Categories", "CVES", ] hr_results_headers = [ "Port", "Protocol", "Since", "Proof", "Status", ] hr_solutions_headers = [ "Type", "Summary", "Steps", "Estimate", "AdditionalInformation", ] try: vulnerability_data = client.get_asset_vulnerability( asset_id=asset_id, vulnerability_id=vulnerability_id, ) # A 404 error is returned when the asset or vulnerability could not be found, # or if the asset is not vulnerable to this vulnerability. # This code section is to separate the different errors and return a different message for each case. except DemistoException as e: if e.res is not None and e.res.status_code is not None and e.res.status_code == 404: try: client.get_asset(asset_id) except DemistoException as e2: if e2.res is not None and e2.res.status_code is not None and e2.res.status_code == 404: raise ValueError("Asset not found.") try: client.get_vulnerability(vulnerability_id) except DemistoException as e2: if e2.res is not None and e2.res.status_code is not None and e2.res.status_code == 404: # type: ignore raise ValueError("Vulnerability not found.") return CommandResults(readable_output=f'Asset {asset_id} is not vulnerable to "{vulnerability_id}".') raise e # Add extra info about vulnerability vulnerability_extra_data = client.get_vulnerability(vulnerability_id=vulnerability_id) vulnerability_data.update(deepcopy(vulnerability_extra_data)) vulnerability_outputs = generate_new_dict( data=vulnerability_extra_data, name_mapping={ "id": "Id", "title": "Title", "severity": "Severity", "riskScore": "RiskScore", "cvss.v2.vector": "CVSS", "cvss.v3.vector": "CVSSV3", "published": "Published", "added": "Added", "modified": "Modified", "cvss.v2.score": "CVSSScore", "cvss.v3.score": "CVSSV3Score", "categories": "Categories", "cves": "CVES", }, include_none=True, ) results_output: list = [] if vulnerability_data.get("results"): results_output = generate_new_dict( data=vulnerability_data["results"], name_mapping={ "port": "Port", "protocol": "Protocol", "since": "Since", "proof": "Proof", "status": "Status", }, include_none=True, ) # Remove HTML tags for result in results_output: result["Proof"] = re.sub("<.*?>", "", result["Proof"]) # Add solutions data solutions_output: list = [] solutions = client.get_asset_vulnerability_solution(asset_id, vulnerability_id) vulnerability_data["solutions"] = solutions if solutions and solutions.get("resources"): solutions_output = generate_new_dict( data=solutions["resources"], name_mapping={ "type": "Type", "summary.text": "Summary", "steps.text": "Steps", "estimate": "Estimate", "additionalInformation.text": "AdditionalInformation", }, include_none=True, ) for idx, _val in enumerate(solutions_output): solutions_output[idx]["Estimate"] = readable_duration_time(solutions_output[idx]["Estimate"]) vulnerability_outputs["Check"] = results_output vulnerability_outputs["Solution"] = solutions_output vulnerabilities_md = tableToMarkdown( f"Vulnerability {vulnerability_id!s}", vulnerability_outputs, hr_vulnerability_headers, removeNull=True ) results_md = tableToMarkdown("Checks", results_output, hr_results_headers, removeNull=True) if len(results_output) > 0 else "" solutions_md = ( tableToMarkdown("Solutions", solutions_output, hr_solutions_headers, removeNull=True) if solutions_output is not None else "" ) indicators: list = [] if vulnerability_data.get("cves"): for cve in vulnerability_data["cves"]: if "v3" in vulnerability_data["cvss"]: cvss_info = vulnerability_data["cvss"]["v3"] cvss_version = "3" else: cvss_info = vulnerability_data["cvss"]["v2"] cvss_version = "2" indicators.append( Common.CVE( id=cve, cvss=None, # type: ignore cvss_score=cvss_info.get("score"), cvss_vector=cvss_info.get("vector"), cvss_version=cvss_version, description=vulnerability_data["description"]["text"], modified=vulnerability_data["modified"], published=vulnerability_data["published"], ) ) if len(indicators) == 0: indicators = [None] results = [] for indicator in indicators: results.append( CommandResults( outputs_prefix="Nexpose.Asset", outputs_key_field="AssetId", outputs={ "AssetId": asset_id, "Vulnerability": [vulnerability_outputs], }, readable_output=vulnerabilities_md + results_md + solutions_md, indicator=indicator, ) ) return results def get_generated_report_status_command(client: Client, report_id: str, instance_id: str) -> CommandResults: """ Retrieve information about a generated report's status. Args: client (Client): Client to use for API requests. report_id (str): ID of the report to retrieve information about. instance_id (str): ID of the report instance to retrieve information about. """ response = client.get_report_history(report_id, instance_id) context = { "ID": report_id, "InstanceID": instance_id, "Status": response.get("status", "unknown"), } hr = tableToMarkdown("Report Generation Status", context) return CommandResults( readable_output=hr, outputs_prefix="Nexpose.Report", outputs=context, outputs_key_field=["ID", "InstanceID"], raw_response=response, ) def get_report_templates_command(client: Client) -> CommandResults: """ Retrieve information about all available report templates. Args: client (Client): Client to use for API requests. """ hr_headers = ["Id", "Name", "Description", "Type"] report_templates_data = client.get_report_templates() if not report_templates_data.get("resources"): return CommandResults( readable_output="No templates found", raw_response=report_templates_data, ) report_templates_output = generate_new_dict( data=report_templates_data["resources"], name_mapping={ "id": "Id", "name": "Name", "description": "Description", "type": "Type", }, include_none=True, ) return CommandResults( outputs_prefix="Nexpose.Template", outputs_key_field="Id", outputs=report_templates_output, readable_output=tableToMarkdown("Nexpose Templates", report_templates_output, hr_headers, removeNull=True), raw_response=report_templates_data, ) def get_scan_command(client: Client, scan_ids: str) -> list[CommandResults]: """ Retrieve information about a specific or multiple scans. Args: client (Client): Client to use for API requests. scan_ids (str | list): ID of the scan to retrieve. """ scan_ids_list = argToList(scan_ids) results = [] for scan_id in scan_ids_list: try: scan_data = client.get_scan(scan_id) except DemistoException as e: if e.res is not None and e.res.status_code is not None and e.res.status_code == 404: scan_entry = CommandResults(readable_output=f"Scan for ID {scan_id} was not found.") else: raise e else: scan_entry = get_scan_entry(scan_data) results.append(scan_entry) return results def get_scans_command( client: Client, active: str | None = None, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None, ) -> CommandResults: """ Retrieve a list of all scans. Args: client (Client): Client to use for API requests. active (str | None, optional): Whether to return active scans or not. Defaults to False. page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) scans_data = client.get_scans( active=argToBoolean(active) if active is not None else None, page_size=page_size_int, page=page_int, sort=sort, limit=limit_int, ) if not scans_data: return CommandResults( readable_output="No scans found", raw_response=scans_data, ) normalized_scans = [normalize_scan_data(scan) for scan in scans_data] scan_hr = tableToMarkdown( name="Nexpose Scans", t=normalized_scans, headers=[ "Id", "ScanType", "ScanName", "StartedBy", "Assets", "TotalTime", "Completed", "Status", "Message", ], removeNull=True, ) return CommandResults( outputs_prefix="Nexpose.Scan", outputs_key_field="Id", outputs=normalized_scans, readable_output=scan_hr, raw_response=scans_data, ) def get_sites_command( client: Client, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None ) -> CommandResults: """ Retrieve a list of sites. Args: client (Client): Client to use for API requests. page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ hr_headers = ["Id", "Name", "Assets", "Vulnerabilities", "Risk", "Type", "LastScan"] page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) sites_data = client.get_sites( page_size=page_size_int, page=page_int, sort=sort, limit=limit_int, ) if not sites_data: return CommandResults( readable_output="No sites found", raw_response=sites_data, ) sites_output = generate_new_dict( data=sites_data, name_mapping={ "id": "Id", "name": "Name", "assets": "Assets", "vulnerabilities.total": "Vulnerabilities", "riskScore": "Risk", "type": "Type", "lastScanTime": "LastScan", }, include_none=True, ) return CommandResults( outputs_prefix="Nexpose.Site", outputs_key_field="Id", outputs=sites_output, readable_output=tableToMarkdown("Nexpose Sites", sites_output, hr_headers, removeNull=True), raw_response=sites_data, ) def list_scan_schedule_command( client: Client, site_id: str | None = None, site_name: str | None = None, schedule_id: str | None = None, limit: str | None = None, ) -> CommandResults: """ Retrieve information about scan schedules for a specific site or a specific scan schedule. Args: client (Client): Client to use for API requests. site_id (str | None, optional): ID of a site to retrieve scan schedules from. site_name (str | None, optional): Name of a site to retrieve scan schedules from. Can be used instead of "site_id". schedule_id (str): ID of a specific scan schedule to retrieve. Defaults to None (Results in getting all scan schedules for the site). limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) hr_headers = [ "Enable", "StartDate", "Name", "MaxDuration", "Repeat", "NextStart", ] limit_int = arg_to_number(limit, required=False) if not schedule_id: scan_schedules_data = client.get_scan_schedules(site_id=site.id) if limit_int is not None and limit_int < len(scan_schedules_data): scan_schedules_data = scan_schedules_data[:limit_int] else: scan_schedules_data = [ client.get_scan_schedule( site_id=site.id, schedule_id=schedule_id, ) ] if not scan_schedules_data: return CommandResults( readable_output="No scan schedules were found for the site.", raw_response=scan_schedules_data, ) hr_outputs = generate_new_dict( data=scan_schedules_data, name_mapping={ "id": "Id", "enabled": "Enable", "start": "StartDate", "scanName": "Name", "duration": "MaxDuration", "repeat.every": "Repeat", "nextRuntimes": "NextStart", }, ) for scan_schedule in hr_outputs: if scan_schedule.get("MaxDuration"): scan_schedule["MaxDuration"] = readable_duration_time(scan_schedule["MaxDuration"]) if scan_schedule.get("Repeat"): scan_schedule["Repeat"] = "every " + scan_schedule["Repeat"] return CommandResults( outputs_prefix="Nexpose.ScanSchedule", outputs_key_field="id", outputs=scan_schedules_data, readable_output=tableToMarkdown("Nexpose Scan Schedules", hr_outputs, hr_headers, removeNull=True), raw_response=scan_schedules_data, ) def list_shared_credential_command(client: Client, credential_id: str | None = None, limit: str | None = None) -> CommandResults: """ Retrieve information about all or a specific vulnerability. Args: client (Client): Client to use for API requests. credential_id (str | None, optional): ID of a specific shared credential to retrieve. Defaults to None (Results in getting all vulnerabilities). limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit. Defaults to None. """ hr_headers = [ "Id", "Name", "Service", "Domain", "UserName", "AvailableToSites", ] limit_int = arg_to_number(limit, required=False) if not credential_id: shared_credentials_data = client.get_shared_credentials() if limit_int is not None and limit_int < len(shared_credentials_data): shared_credentials_data = shared_credentials_data[:limit_int] else: shared_credentials_data = [client.get_shared_credential(credential_id)] if not shared_credentials_data: return CommandResults( readable_output="No shared credentials were found.", raw_response=shared_credentials_data, ) shared_credentials_hr = generate_new_dict( data=shared_credentials_data, name_mapping={ "id": "Id", "name": "Name", "account.service": "Service", "account.domain": "Domain", "account.username": "UserName", }, ) for shared_credential in shared_credentials_hr: if shared_credential.get("sites"): shared_credential["AvailableToSites"] = len(shared_credential["sites"]) return CommandResults( outputs_prefix="Nexpose.SharedCredential", outputs_key_field="id", outputs=shared_credentials_data, readable_output=tableToMarkdown("Nexpose Shared Credentials", shared_credentials_hr, hr_headers, removeNull=True), raw_response=shared_credentials_data, ) def list_assigned_shared_credential_command( client: Client, site_id: str | None = None, site_name: str | None = None, limit: str | None = None ) -> CommandResults: """ Retrieve information about shared credentials for a specific site. Args: client (Client): Client to use for API requests. site_id (str | None, optional): ID of a site to retrieve shared credentials from. site_name (str | None, optional): Name of a site to retrieve shared credentials from. Can be used instead of "site_id". limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) hr_headers = [ "Id", "Name", "Service", "Enabled", ] limit_int = arg_to_number(limit, required=False) response_data = client.get_assigned_shared_credentials(site_id=site.id) if not response_data: site_id = site.name if site.name else site.id return CommandResults( readable_output=f'No assigned shared credentials were found for site "{site_id}".', raw_response=response_data, ) if limit_int: response_data = response_data[:limit_int] assigned_shared_credentials_hr = generate_new_dict( data=response_data, name_mapping={ "id": "Id", "name": "Name", "service": "Service", "enabled": "Enabled", }, ) return CommandResults( outputs_prefix="Nexpose.AssignedSharedCredential", outputs_key_field="id", outputs=response_data, readable_output=tableToMarkdown( "Nexpose Assigned Shared Credentials", assigned_shared_credentials_hr, hr_headers, removeNull=True ), raw_response=response_data, ) def list_site_scan_credential_command( client: Client, site_id: str | None = None, site_name: str | None = None, credential_id: str | None = None, limit: str | None = None, ) -> CommandResults: """ Retrieve information about all or a specific scan credential. Args: client (Client): Client to use for API requests. site_id (str | None, optional): ID of a site to retrieve scan credentials from. site_name (str | None, optional): Name of a site to retrieve scan credentials from. Can be used instead of "site_id". credential_id (str | None, optional): ID of a specific scan credential to retrieve. limit (str | None, optional): Limit the number of credentials to return. None means to not use a limit. Defaults to None. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) hr_headers = [ "Id", "Enabled", "Name", "Service", "UserName", "RestrictToHostName", "RestrictToPort", ] limit_int = arg_to_number(limit, required=False) if credential_id is not None: site_scan_credentials_data = client.get_site_scan_credential(site_id=site.id, credential_id=credential_id) site_scan_credentials_data = [site_scan_credentials_data] else: site_scan_credentials_data = client.get_site_scan_credentials(site_id=site.id) if not site_scan_credentials_data: site_id = site.name if site.name else site.id return CommandResults( readable_output=f'No site scan credentials were found for site "{site_id}".', raw_response=site_scan_credentials_data, ) if limit_int and len(site_scan_credentials_data) > limit_int: site_scan_credentials_data = site_scan_credentials_data[:limit_int] site_scan_credentials_hr = generate_new_dict( data=site_scan_credentials_data, name_mapping={ "id": "Id", "enabled": "Enabled", "name": "Name", "account.service": "Service", "account.username": "UserName", "hostRestriction": "RestrictToHostName", "portRestriction": "RestrictToPort", }, ) return CommandResults( outputs_prefix="Nexpose.SiteScanCredential", outputs_key_field="id", outputs=site_scan_credentials_data, readable_output=tableToMarkdown("Nexpose Site Scan Credentials", site_scan_credentials_hr, hr_headers, removeNull=True), raw_response=site_scan_credentials_data, ) def list_vulnerability_command( client: Client, vulnerability_id: str | None = None, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None, ) -> CommandResults: """ Retrieve information about all or a specific vulnerability. Args: client (Client): Client to use for API requests. vulnerability_id (str | None, optional): ID of a specific vulnerability to retrieve. Defaults to None (Results in getting all vulnerabilities). page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ hr_headers = [ "Title", "MalwareKits", "Exploits", "CVSS", "CVSSv3", "Risk", "PublishedOn", "ModifiedOn", "Severity", ] page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) if not vulnerability_id: vulnerabilities_data = client.get_vulnerabilities( page_size=page_size_int, page=page_int, sort=sort, limit=limit_int, ) else: vulnerabilities_data = [client.get_vulnerability(vulnerability_id)] if not vulnerabilities_data: return CommandResults( readable_output="No vulnerability exceptions were found.", raw_response=vulnerabilities_data, ) vulnerabilities_hr = generate_new_dict( data=vulnerabilities_data, name_mapping={ "title": "Title", "malwareKits": "MalwareKits", "exploits": "Exploits", "cvss.v2.score": "CVSS", "cvss.v3.score": "CVSSv3", "riskScore": "Risk", "published": "PublishedOn", "modified": "ModifiedOn", "severity": "Severity", }, ) return CommandResults( outputs_prefix="Nexpose.Vulnerability", outputs_key_field="id", outputs=vulnerabilities_data, readable_output=tableToMarkdown("Nexpose Vulnerabilities", vulnerabilities_hr, hr_headers, removeNull=True), raw_response=vulnerabilities_data, ) def list_vulnerability_exceptions_command( client: Client, vulnerability_exception_id: str | None = None, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None, ) -> CommandResults: """ Retrieve information about all or a specific vulnerability exception. Args: client (Client): Client to use for API requests. vulnerability_exception_id (str | None, optional): ID of a specific vulnerability exception to retrieve. Defaults to None (Results in getting all vulnerability exceptions). page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ hr_headers = [ "Id", "Vulnerability", "ExceptionScope", "Reason", "ReportedBy", "ReportedOn", "ReviewStatus", "ReviewedOn", "ExpiresOn", ] page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) if not vulnerability_exception_id: vulnerability_exceptions_data = client.get_vulnerability_exceptions( page_size=page_size_int, page=page_int, sort=sort, limit=limit_int, ) else: vulnerability_exceptions_data = [client.get_vulnerability_exception(vulnerability_exception_id)] if not vulnerability_exceptions_data: return CommandResults( readable_output="No vulnerability exceptions were found.", raw_response=vulnerability_exceptions_data, ) hr_vulnerability_exceptions_data = generate_new_dict( data=vulnerability_exceptions_data, name_mapping={ "id": "Id", "scope.vulnerability": "Vulnerability", "scope.type": "ExceptionScope", "submit.reason": "Reason", "submit.name": "ReportedBy", "state": "ReviewStatus", "review.date": "ReviewedOn", "expires": "ExpiresOn", }, ) return CommandResults( outputs_prefix="Nexpose.VulnerabilityException", outputs_key_field="id", outputs=vulnerability_exceptions_data, readable_output=tableToMarkdown( "Nexpose Vulnerability Exceptions", hr_vulnerability_exceptions_data, hr_headers, removeNull=True ), raw_response=vulnerability_exceptions_data, ) def search_assets_command( client: Client, query: str | None = None, ip_address_is: str | None = None, host_name_is: str | None = None, risk_score_higher_than: str | None = None, vulnerability_title_contains: str | None = None, site_id_in: str | None = None, site_name_in: str | None = None, match: str | None = None, page_size: str | None = None, page: str | None = None, sort: str | None = None, limit: str | None = None, ) -> CommandResults | list[CommandResults]: """ Retrieve a list of all assets with access permissions that match the provided search filters. Args: client (Client): Client to use for API requests. query (str | None, optional): String based filters to use separated by ';'. Defaults to None. ip_address_is (str | None, optional): IP address(es) to filter for. Defaults to None. host_name_is (str | None, optional): Hostname(s) to filter for. Defaults to None. risk_score_higher_than (str | None, optional): Filter for risk scores that are higher than the provided value. Defaults to None. vulnerability_title_contains (str | None, optional): Filter for vulnerability titles that contain the provided value. Defaults to None. Defaults to None. site_id_in (str | None, optional): Filter for assets that are under a specific site(s). Defaults to None. site_name_in (str | None, optional): Filter for assets that are under a specific site(s). Defaults to None. match (str | None, optional): Determine if the filters should match all or any of the filters. Can be either "all" or "any". Defaults to None (Results in using MATCH_DEFAULT_VALUE). page_size (str | None, optional): Number of scans to return per page when using pagination. Defaults to DEFAULT_PAGE_SIZE. page (str | None, optional): Specific pagination page to retrieve. Defaults to None. Defaults to None. sort (str | None, optional): Sort results by fields. Uses a `property[,ASC|DESC]...` format. Defaults to None. limit (str | None, optional): Limit the number of scans to return. None means to not use a limit. Defaults to None. """ # sites: list[Site] = [] hr_headers = [ "AssetId", "Address", "Name", "Site", "Exploits", "Malware", "OperatingSystem", "RiskScore", "Assessed", "LastScanDate", "LastScanId", ] page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) if not match: match = MATCH_DEFAULT_VALUE filters_data = parse_asset_filters( client=client, ip_address_is=ip_address_is, host_name_is=host_name_is, risk_score_higher_than=risk_score_higher_than, vulnerability_title_contains=vulnerability_title_contains, site_id_in=site_id_in, site_name_in=site_name_in, query=query, ) assets = client.search_assets( filters=convert_asset_search_filters(filters_data), match=match, page_size=page_size_int, page=page_int, sort=sort, limit=limit_int, ) if not assets: return CommandResults(readable_output="No assets were found") results = [] for asset in assets: asset_output = generate_new_dict( data=asset, name_mapping={ "id": "AssetId", "ip": "Address", "addresses.ip": "Addresses", "hostName": "Name", "Site": "Site", "vulnerabilities.exploits": "Exploits", "vulnerabilities.malwareKits": "Malware", "os": "OperatingSystem", "vulnerabilities.total": "Vulnerabilities", "riskScore": "RiskScore", "assessedForVulnerabilities": "Assessed", }, include_none=True, ) site = client.find_asset_site(asset["id"]) if site is not None: asset_output["Site"] = site.name asset_output["LastScanDate"], asset_output["LastScanId"] = find_asset_last_scan_data(asset) results.append( CommandResults( outputs_prefix="Nexpose.Asset", outputs_key_field="Id", outputs=asset_output, readable_output=tableToMarkdown(f"Nexpose Asset {asset['id']!s}", asset_output, hr_headers, removeNull=True), raw_response=asset, indicator=Common.Endpoint( id=asset["id"], hostname=asset.get("hostName"), ip_address=asset.get("ip"), mac_address=asset.get("mac"), os=asset.get("os"), vendor=VENDOR_NAME, ), ) ) return results def set_assigned_shared_credential_status_command( client: Client, credential_id: str, enabled: bool, site_id: str | None = None, site_name: str | None = None ) -> CommandResults: """ Enable or disable a shared credential. Args: client (Client): Client to use for API requests. credential_id (str): ID of the shared credential to enable or disable. enabled (bool): Whether to enable or disable the shared credential. site_id (Site): ID of a site to use for API requests. site_name (Site): Name of a site to use for API requests. Can be used instead of "site_id". """ site = Site( site_id=site_id, site_name=site_name, client=client, ) response_data = client.set_assigned_shared_credential_status( site_id=site.id, shared_credential_id=credential_id, enabled=enabled, ) return CommandResults( readable_output=f'Shared credential "{credential_id}" enablement has been set to "{str(enabled).lower()}".', raw_response=response_data, ) def start_assets_scan_command( client: Client, ip_addresses: str | None = None, hostnames: str | None = None, name: str | None = None ) -> CommandResults: """ Start a scan on the provided assets. Note: Both `ip_addresses` and `hostnames` are optional, but at least one of them must be provided. Args: client (Client): Client to use for API requests. ip_addresses (str | None, optional): IP(s) of assets to scan. Defaults to None hostnames (str | None, optional): Hostname(s) of assets to scan. Defaults to None name (str | None): Name to set for the new scan. Defaults to None (Results in using a "scan <date>" format). """ if ip_addresses is None and hostnames is None: raise ValueError('At least one of "ips" and "hostnames" must be provided.') ip_addresses_list = None hostnames_list = None asset_filter = "" if ip_addresses is not None: ip_addresses_list = argToList(ip_addresses) asset_filter = "ip-address is " + ip_addresses_list[0] if hostnames is not None: hostnames_list = argToList(hostnames) asset_filter = "host-name is " + hostnames_list[0] if not name: name = f"scan {datetime.now()}" asset_data = client.search_assets(filters=convert_asset_search_filters(asset_filter), match="all") if not asset_data: return CommandResults( readable_output="Could not find assets.", raw_response=asset_data, ) site = client.find_asset_site(asset_data[0]["id"]) if site is None: return CommandResults( readable_output="Could not find site.", raw_response=site, ) hosts = [] if ip_addresses_list: hosts.extend(ip_addresses_list) if hostnames_list: hosts.extend(hostnames_list) scan_response = client.start_site_scan(site_id=site.id, scan_name=name, hosts=hosts) if "id" not in scan_response: return CommandResults( readable_output="Could not start scan.", raw_response=scan_response, ) return get_scan_entry(client.get_scan(scan_response["id"])) def start_site_scan_command( client: Client, site_id: str | None = None, site_name: str | None = None, hosts: str | None = None, name: str | None = None ) -> CommandResults: """ Start a scan for a specific site. Args: client (Client): Client to use for API requests. site_id (str | None, optional): ID of a site to start a scan on. site_name (str | None, optional): Name of a site to start a scan on. Can be used instead of "site_id". hosts (str | None): Hosts to scan. Defaults to None (Results in scanning all hosts). name (str | None): Name to set for the new scan. Defaults to None (Results in using a "scan <date>" format). """ site = Site( site_id=site_id, site_name=site_name, client=client, ) scan_response = client.start_site_scan( site_id=site.id, scan_name=name if name else f"scan {datetime.now()}", hosts=argToList(hosts) if hosts else None, # type: ignore[arg-type] ) if not scan_response or "id" not in scan_response: return CommandResults( readable_output="Could not start scan", raw_response=scan_response, ) scan_data = client.get_scan(scan_response["id"]) return get_scan_entry(scan_data) def update_scan_command(client: Client, scan_id: str, scan_status: ScanStatus) -> CommandResults: """ Update status for a specific scan. Args: client (Client): Client to use for API requests. scan_id (str): ID of the scan to update. scan_status (ScanStatus): Status to set the scan to. """ response = client.update_scan_status(scan_id, scan_status) return CommandResults( readable_output=f'Successfully updated scan status to "{scan_status.value}"', raw_response=response, ) def update_shared_credential_command( client: Client, shared_credential_id: str, name: str, site_assignment: str, service: str, authentication_type: str | None = None, community_name: str | None = None, database: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: str | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, sites: str | None = None, privacy_password: str | None = None, privacy_type: str | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: str | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: str | None = None, username: str | None = None, ) -> CommandResults: """ Update an existing shared credential. Args: client (Client): Client to use for API requests. shared_credential_id (str): ID of the shared credential to update. name (str): Name of the credential. site_assignment (str): Site assignment configuration for the credential. service (str): Credential service type. authentication_type (str): SNMPv3 authentication type for the credential. community_name (str | None, optional): SNMP community for authentication. database (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. sites (str | None, optional): List of site IDs for the shared credential that are explicitly assigned access to the shared scan credential, allowing it to use the credential during a scan. privacy_password (str | None, optional): SNMPv3 privacy password to use. privacy_type (str | None, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (str | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (str | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. """ oracle_enumerate_sids_list = None sites_list = None snmpv3_authentication_type_enum = None snmpv3_privacy_type_enum = None ssh_permission_elevation_enum = None use_windows_authentication_bool = None if oracle_enumerate_sids is not None: oracle_enumerate_sids_list = argToBoolean(oracle_enumerate_sids) if sites is not None: sites_list = [int(item) for item in argToList(sites)] if authentication_type is not None: snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type] if privacy_type is not None: snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type] if ssh_permission_elevation is not None: ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation] if use_windows_authentication is not None: use_windows_authentication_bool = argToBoolean(use_windows_authentication) response_data = client.update_shared_credential( shared_credential_id=shared_credential_id, name=name, site_assignment=SharedCredentialSiteAssignment[site_assignment], service=CredentialService[service], database_name=database, description=description, domain=domain, host_restriction=host_restriction, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids_list, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, port_restriction=port_restriction, sites=sites_list, snmp_community_name=community_name, snmpv3_authentication_type=snmpv3_authentication_type_enum, snmpv3_privacy_password=privacy_password, snmpv3_privacy_type=snmpv3_privacy_type_enum, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation_enum, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication_bool, username=username, ) return CommandResults( readable_output=f"Shared credential with ID {shared_credential_id} has been updated.", raw_response=response_data ) def update_site_scan_credential_command( client: Client, credential_id: str, name: str, service: str, site_id: str | None = None, site_name: str | None = None, authentication_type: str | None = None, community_name: str | None = None, database: str | None = None, description: str | None = None, domain: str | None = None, host_restriction: str | None = None, http_realm: str | None = None, notes_id_password: str | None = None, ntlm_hash: str | None = None, oracle_enumerate_sids: str | None = None, oracle_listener_password: str | None = None, oracle_sid: str | None = None, password: str | None = None, port_restriction: str | None = None, privacy_password: str | None = None, privacy_type: str | None = None, ssh_key_pem: str | None = None, ssh_permission_elevation: str | None = None, ssh_permission_elevation_password: str | None = None, ssh_permission_elevation_username: str | None = None, ssh_private_key_password: str | None = None, use_windows_authentication: str | None = None, username: str | None = None, ) -> CommandResults: """ Update an existing site scan credential. Args: client (Client): Client to use for API requests. credential_id (str): ID of the site scan credential to update. name (str): Name of the credential. service (str): Credential service type. site_id (str | None, optional): ID of a site to update the site scan credential for. site_name (str | None, optional): Name of a site to update the site scan credential for. Can be used instead of "site_id". authentication_type (str | None, optional): SNMPv3 authentication type for the credential. community_name (str | None, optional): SNMP community for authentication. database (str | None, optional): Database name. description (str | None, optional): Description for the credential. domain (str | None, optional): Domain address. host_restriction (str | None, optional): Hostname or IP address to restrict the credentials to. http_realm (str | None, optional): HTTP realm. notes_id_password (str | None, optional): Password for the notes account that will be used for authenticating. ntlm_hash (str | None, optional): NTLM password hash. oracle_enumerate_sids (str | None, optional): Whether the scan engine should attempt to enumerate SIDs from the environment. oracle_listener_password (str | None, optional): The Oracle Net Listener password. Used to enumerate SIDs from the environment. oracle_sid (str | None, optional): Oracle database name. password (str | None, optional): Password for the credential. port_restriction (str | None, optional): Further restricts the credential to attempt to authenticate on a specific port. Can be used only if `host_restriction` is used. privacy_password (str | None, optional): SNMPv3 privacy password to use. privacy_type (str | None, optional): SNMPv3 Privacy protocol to use. ssh_key_pem (str | None, optional): PEM formatted private key. ssh_permission_elevation (str | None, optional): Elevation type to use for scans. ssh_permission_elevation_password (str | None, optional): Password to use for elevation. ssh_permission_elevation_username (str | None, optional): Username to use for elevation. ssh_private_key_password (str | None, optional): Password for the private key. use_windows_authentication (str | None, optional): Whether to use Windows authentication. username (str | None, optional): Username for the credential. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) oracle_enumerate_sids_bool = None snmpv3_authentication_type_enum = None snmpv3_privacy_type_enum = None ssh_permission_elevation_enum = None use_windows_authentication_bool = None if oracle_enumerate_sids is not None: oracle_enumerate_sids_bool = argToBoolean(oracle_enumerate_sids) if authentication_type is not None: snmpv3_authentication_type_enum = SNMPv3AuthenticationType[authentication_type] if privacy_type is not None: snmpv3_privacy_type_enum = SNMPv3PrivacyType[privacy_type] if ssh_permission_elevation is not None: ssh_permission_elevation_enum = SSHElevationType[ssh_permission_elevation] if use_windows_authentication is not None: use_windows_authentication_bool = argToBoolean(use_windows_authentication) response_data = client.update_site_scan_credential( site_id=site.id, credential_id=credential_id, name=name, service=CredentialService[service], database_name=database, description=description, domain=domain, host_restriction=host_restriction, http_realm=http_realm, notes_id_password=notes_id_password, ntlm_hash=ntlm_hash, oracle_enumerate_sids=oracle_enumerate_sids_bool, oracle_listener_password=oracle_listener_password, oracle_sid=oracle_sid, password=password, port_restriction=port_restriction, snmp_community_name=community_name, snmpv3_authentication_type=snmpv3_authentication_type_enum, snmpv3_privacy_password=privacy_password, snmpv3_privacy_type=snmpv3_privacy_type_enum, ssh_key_pem=ssh_key_pem, ssh_permission_elevation=ssh_permission_elevation_enum, ssh_permission_elevation_password=ssh_permission_elevation_password, ssh_permission_elevation_username=ssh_permission_elevation_username, ssh_private_key_password=ssh_private_key_password, use_windows_authentication=use_windows_authentication_bool, username=username, ) return CommandResults( readable_output=f"Site scan credential with ID {credential_id} has been updated.", raw_response=response_data, ) def update_scan_schedule_command( client: Client, schedule_id: int, on_scan_repeat: str, start: str, site_id: str | None = None, site_name: str | None = None, excluded_asset_groups: str | None = None, excluded_targets: str | None = None, included_asset_groups: str | None = None, included_targets: str | None = None, duration_days: str | None = None, duration_hours: str | None = None, duration_minutes: str | None = None, enabled: str | None = None, frequency: str | None = None, interval: str | None = None, scan_name: str | None = None, date_of_month: str | None = None, scan_template_id: str | None = None, ) -> CommandResults: """ Update a site scan schedule. Args: client (Client): Client to use for API requests. schedule_id (str): ID of the scan schedule to update. on_scan_repeat (str): The desired behavior of a repeating scheduled scan when the previous scan was paused due to reaching its maximum duration. start (str): The scheduled start date and time formatted in ISO 8601 format. site_id (str | None, optional): ID of a site to create a scheduled scan for. site_name (str | None, optional): Name of a site to create a scheduled scan for. Can be used instead of "site_id". excluded_asset_groups (str | None, optional): Asset groups to exclude from the scan. excluded_targets (str | None, optional): Addresses to exclude from the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. included_asset_groups (str | None, optional): Asset groups to include in the scan. included_targets (str | None, optional): Addresses to include in the scan. Each address is a string that can represent either a hostname, ipv4 address, ipv4 address range, ipv6 address, or CIDR notation. duration_days (str | None, optional): Maximum duration of the scan in days. Can be used along with `duration_hours` and `duration_minutes`. duration_hours (str | None, optional): Maximum duration of the scan in hours. Can be used along with `duration_days` and `duration_minutes`. duration_minutes (str | None, optional): Maximum duration of the scan in minutes. Can be used along with `duration_days` and `duration_hours`. enabled (str | None, optional): A flag indicating whether the scan schedule is enabled. Defaults to None, which results in using True. frequency (str | None, optional): Frequency for the schedule to repeat. interval (str | None, optional): The interval time the schedule should repeat. Required if frequency is set to any value other than `DATE_OF_MONTH`. date_of_month(str | None, optional): Specifies the schedule repeat day of the interval month. Required and used only if frequency is set to `DATE_OF_MONTH`. scan_name (str | None, optional): A unique user-defined name for the scan launched by the schedule. If not explicitly set in the schedule, the scan name will be generated prior to the scan launching. scan_template_id (str | None, optional): ID of the scan template to use. """ site = Site( site_id=site_id, site_name=site_name, client=client, ) excluded_asset_groups_list = None excluded_targets_list = None frequency_enum = None included_asset_groups_list = None included_targets_list = None if excluded_asset_groups is not None: excluded_asset_groups_list = [int(asset_id) for asset_id in argToList(excluded_asset_groups)] if excluded_targets is not None: excluded_targets_list = argToList(excluded_targets) if frequency is not None: frequency_enum = RepeatFrequencyType[frequency] if included_asset_groups is not None: included_asset_groups_list = [int(asset_id) for asset_id in argToList(included_asset_groups)] if included_targets is not None: included_targets_list = argToList(included_targets) duration_days_int = arg_to_number(duration_days, required=False) duration_hours_int = arg_to_number(duration_hours, required=False) duration_minutes_int = arg_to_number(duration_minutes, required=False) interval_int = arg_to_number(interval, required=False) date_of_month_int = arg_to_number(date_of_month, required=False) if enabled is not None: enabled_bool = argToBoolean(enabled) else: enabled_bool = True duration = generate_duration_time( days=duration_days_int, hours=duration_hours_int, minutes=duration_minutes_int, ) response_data = client.update_scan_schedule( site_id=site.id, scan_schedule_id=schedule_id, enabled=enabled_bool, repeat_behaviour=on_scan_repeat, start_date=start, excluded_asset_groups=excluded_asset_groups_list, excluded_targets=excluded_targets_list, included_asset_groups=included_asset_groups_list, included_targets=included_targets_list, duration=duration, frequency=frequency_enum, interval=interval_int, date_of_month=date_of_month_int, scan_name=scan_name, scan_template_id=scan_template_id, ) return CommandResults( readable_output=f"Scan schedule {schedule_id} has been updated.", raw_response=response_data, ) def update_vulnerability_exception_expiration_command( client: Client, vulnerability_exception_id: str, expiration: str ) -> CommandResults: """ Update the expiration date of a vulnerability exception. Args: client (Client): Client to use for API requests. vulnerability_exception_id (str): ID of the vulnerability exception to update. expiration (str): Expiration date to set for the vulnerability exception, formatted in ISO 8601 format. """ response = client.update_vulnerability_exception_expiration( vulnerability_exception_id=vulnerability_exception_id, expiration_date=expiration, ) return CommandResults( readable_output=f"Successfully updated expiration date of vulnerability exception {vulnerability_exception_id}.", raw_response=response, ) def update_vulnerability_exception_status_command(client: Client, vulnerability_exception_id: str, status: str) -> CommandResults: """ Update the status of a vulnerability exception. Args: client (Client): Client to use for API requests. vulnerability_exception_id (str): ID of the vulnerability exception to update. status (str): Status to set for the vulnerability exception. """ response = client.update_vulnerability_exception_status( vulnerability_exception_id=vulnerability_exception_id, status=status, ) return CommandResults( readable_output=f"Successfully updated status of vulnerability exception {vulnerability_exception_id}.", raw_response=response, ) def create_tag_command( client: Client, name: str, type: str, color: str, ip_address_is: str | None = None, host_name_is: str | None = None, risk_score_higher_than: str | None = None, vulnerability_title_contains: str | None = None, site_id_in: str | None = None, site_name_in: str | None = None, query: str | None = None, match: str | None = None, ): """ Create a tag. Args: client (Client): Client to use for API requests. name (str): The tag name. type (str): The tag type. color (str): The tag color - relevant only for "custom" type. ip_address_is (str, optional): A specific IP address to search for. host_name_is (str, optional): A specific host name to search for. risk_score_higher_than (str, optional): A minimum risk score to use as a filter. vulnerability_title_contains (str, optional): A string to search for in vulnerability titles. site_id_in (str, optional): Site IDs to filter for. Can be a comma-separated list. site_name_in (str, optional): Site names to filter for. Can be a comma-separated list. query (str, optional): Additional queries to use as a filter, in the format: {field} {operator} {value}. Multiple queries can be specified, separated by a ";" separator. match (str, optional): Operator to determine how to match filters. "All" requires all filters to match, "Any" requires only one filter to match. Returns: CommandResults: Results of the tag creation. """ validate_input(type, VALID_TAG_TYPES, "type", True) validate_input(color, VALID_TAG_COLORS, "color", False) if type.lower() != "custom" and color.lower() != "default": raise DemistoException("color argument is only relevant for “custom” type.") filters_data = parse_asset_filters( client=client, ip_address_is=ip_address_is, host_name_is=host_name_is, risk_score_higher_than=risk_score_higher_than, site_id_in=site_id_in, site_name_in=site_name_in, vulnerability_title_contains=vulnerability_title_contains, query=query, ) filters = convert_asset_search_filters(filters_data) res = client.create_tag(name=name, type=type, color=color, filters=filters, match=match) return CommandResults( outputs_prefix="Nexpose.Tag", outputs_key_field="id", outputs=res, readable_output=f"A new tag '{name}' created successfully with ID: {res['id']}", raw_response=res, ) def delete_tag_command(client: Client, id: str): """ Delete a tag by ID. Args: client (Client): Client to use for API requests. id (str): The tag ID. Returns: CommandResults: Results of the tag deletion. """ id_int = arg_to_number(id, arg_name="id", required=True) client.delete_tag(id_int) # type: ignore[arg-type] return CommandResults(readable_output=f"Tag: {id_int} was deleted successfully") def get_list_tag_command( client: Client, id: str | None = None, name: str | None = None, type: str | None = None, page_size: str | None = None, page: str | None = None, limit: str | None = None, ): """ Get a list of tags or a tag by ID. Args: client (Client): Client to use for API requests. id (str, optional): Get tag by ID. name (str, optional): Filters the returned tags to only those containing the value within their name. type (str, optional): Filters the returned tags to only those of this type. page_size (str, optional): Number of records to retrieve in each API call when pagination is used. page (str, optional): A specific page to retrieve when pagination is used. Page indexing starts at 0. limit (str, optional): A number of records to limit the response to. Returns: CommandResults: Results of the tags retrieval. """ validate_input(type, VALID_TAG_TYPES, "type", False) if id_int := arg_to_number(id, required=False): tags = client.get_tag_by_id(id=id_int) else: page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) tags = client.get_tags_list(name=name, type=type, page_size=page_size_int, page=page_int, limit=limit_int) headers = ["id", "color", "created", "name", "riskmodifier", "source", "type"] return CommandResults( outputs_prefix="Nexpose.Tag", outputs_key_field="id", outputs=tags, readable_output=tableToMarkdown( "Tags list", remove_dict_key(deepcopy(tags), "searchCriteria"), headers=headers, headerTransform=string_to_table_header, ), raw_response=tags, ) def update_tag_search_criteria_command( client: Client, tag_id: str, overwrite: str, ip_address_is: str | None = None, host_name_is: str | None = None, risk_score_higher_than: str | None = None, vulnerability_title_contains: str | None = None, site_id_in: str | None = None, site_name_in: str | None = None, query: str | None = None, match: str | None = None, ): """ Update the search criteria of a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. overwrite (str): Whether to overwrite the original search values or append new conditions to the existing search. ip_address_is (str, optional): A specific IP address to search for. host_name_is (str, optional): A specific host name to search for. risk_score_higher_than (str, optional): A minimum risk score to use as a filter. vulnerability_title_contains (str, optional): A string to search for in vulnerability titles. site_id_in (str, optional): Site IDs to filter for. Can be a comma-separated list. site_name_in (str, optional): Site names to filter for. Can be a comma-separated list. query (str, optional): Additional queries to use as a filter, following the Search Criteria API standard. match (str, optional): Operator to determine how to match filters. "All" requires all filters to match, "Any" requires only one filter to match. Returns: CommandResults: Results of the search criteria update. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) filters_data = parse_asset_filters( client=client, ip_address_is=ip_address_is, host_name_is=host_name_is, risk_score_higher_than=risk_score_higher_than, site_name_in=site_name_in, site_id_in=site_id_in, vulnerability_title_contains=vulnerability_title_contains, query=query, ) filters = convert_asset_search_filters(filters_data) if not argToBoolean(overwrite): tag_data = client.get_tag_by_id(tag_id_int) # type: ignore[arg-type] old_filters = tag_data.get("searchCriteria", {}).get("filters", []) filters.extend(old_filters) client.update_tag_search_criteria(tag_id_int, filters, match) # type: ignore[arg-type] return CommandResults(readable_output=f"Tag {tag_id_int} search criteria were updated successfully") def get_list_tag_asset_group_command(client: Client, tag_id: str): """ Get a list of asset groups for a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. Returns: CommandResults: Results of the asset groups retrieval. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) res = client.send_http_request("GET", f"/tags/{tag_id_int}/asset_groups") asset_groups_ids = res.get("resources", []) return CommandResults( outputs_prefix="Nexpose.TagAssetGroup", outputs=asset_groups_ids, readable_output=tableToMarkdown(f"Tag {tag_id_int} asset groups.", asset_groups_ids, headers=["Asset groups IDs"]), raw_response=res, ) def add_tag_asset_group_command(client: Client, tag_id: str, asset_group_ids: str): """ Add existing asset groups to a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. asset_group_ids (str): The asset group IDs to add. Can be a comma-separated list. Returns: CommandResults: Results of the asset groups addition. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) asset_group_ids_list = argToList(asset_group_ids, transform=int) old_asset_groups = client.send_http_request("GET", f"/tags/{tag_id_int}/asset_groups") all_asset_group = list(set(old_asset_groups.get("resources", []) + asset_group_ids_list)) client.send_http_request("PUT", f"/tags/{tag_id_int}/asset_groups", all_asset_group) return CommandResults(readable_output=f"Asset groups '{asset_group_ids}' were added successfully") def remove_tag_asset_group_command(client: Client, tag_id: str, asset_group_id: str): """ Remove an asset group from a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. asset_group_id (str): The asset group ID to remove. Returns: CommandResults: Results of the asset group removal. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) asset_group_id_int = arg_to_number(asset_group_id, arg_name="asset_group_id", required=True) client.send_http_request("DELETE", f"/tags/{tag_id_int}/asset_groups/{asset_group_id_int}") return CommandResults(readable_output=f"Asset group {asset_group_id_int} was removed from tag {tag_id_int} successfully") def get_list_tag_asset_command(client: Client, tag_id: str): """ Get a list of assets for a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. Returns: CommandResults: Results of the assets retrieval. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) res = client.send_http_request("GET", f"/tags/{tag_id_int}/assets") resources = res.get("resources", []) return CommandResults( outputs_prefix="Nexpose.TagAsset", outputs_key_field="id", outputs=resources, readable_output=tableToMarkdown(f"Tag {tag_id_int} assets", resources, headerTransform=string_to_table_header), raw_response=res, ) def add_tag_asset_command(client: Client, tag_id: str, asset_id: str): """ Add an existing asset to a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. asset_id (str): The asset ID to add. Returns: CommandResults: Results of the asset addition. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) asset_id_int = arg_to_number(asset_id, arg_name="asset_id", required=True) client.send_http_request("PUT", f"/tags/{tag_id_int}/assets/{asset_id_int}") return CommandResults(readable_output=f"Asset {asset_id_int} was added in tag {tag_id_int} successfully") def remove_tag_asset_command(client: Client, tag_id: str, asset_id: str): """ Remove an asset from a tag. Args: client (Client): Client to use for API requests. tag_id (str): The tag ID. asset_id (str): The asset ID to remove. Returns: CommandResults: Results of the asset removal. """ tag_id_int = arg_to_number(tag_id, arg_name="tag_id", required=True) asset_id_int = arg_to_number(asset_id, arg_name="asset_id", required=True) client.send_http_request("DELETE", f"/tags/{tag_id_int}/assets/{asset_id_int}") return CommandResults(readable_output=f"Asset {asset_id_int} was removed from tag {tag_id_int} successfully") def add_site_asset_command( client: Client, target_type: str, site_id: str, assets: str | None = None, asset_group_ids: str | None = None ): """ Add assets or asset groups to a site's included/excluded assets. Args: client (Client): Client to use for API requests. target_type (str): Type of target, either "included" or "excluded". site_id (str): The site ID. assets (str, optional): The assets to add. Can be a comma-separated list. asset_group_ids (str, optional): The asset group IDs to add. Can be a comma-separated list. Returns: CommandResults: Results of the assets or asset groups addition/exclusion. """ site_id_int = arg_to_number(site_id, arg_name="site_id", required=True) if assets_list := argToList(assets): client.send_http_request("POST", f"/sites/{site_id_int}/{target_type}_targets", assets_list) added_assets = f"assets {', '.join(assets_list)}" elif asset_group_ids_list := argToList(asset_group_ids, transform=int): client.send_http_request("PUT", f"/sites/{site_id_int}/{target_type}_asset_groups", asset_group_ids_list) added_assets = f"asset group IDs {asset_group_ids}" else: raise DemistoException("Must provide at least one Asset ID or Asset Group ID") return CommandResults(readable_output=f"Added assets- {added_assets} to site ID - {site_id_int}.") def remove_site_asset_command( client: Client, target_type: str, site_id: str, assets: str | None = None, asset_group_ids: str | None = None ): """ Remove assets or asset groups from a site's included/excluded assets. Args: client (Client): Client to use for API requests. target_type (str): Type of target, either "included" or "excluded". site_id (str): The site ID. assets (str, optional): The assets to remove. Can be a comma-separated list. asset_group_ids (str, optional): The asset group IDs to remove. Can be a comma-separated list. Returns: CommandResults: Results of the assets or asset groups removal. """ site_id_int = arg_to_number(site_id, arg_name="site_id", required=True) if assets_list := argToList(assets): client.send_http_request("DELETE", f"/sites/{site_id_int}/{target_type}_targets", assets_list) removed_assets = f"assets {', '.join(assets_list)}" elif asset_group_ids_list := argToList(asset_group_ids, transform=int): client.send_http_request("DELETE", f"/sites/{site_id_int}/{target_type}_asset_groups", asset_group_ids_list) removed_assets = f"asset group IDs {asset_group_ids}" else: raise DemistoException("Must provide at least one assets or asset_group_ids") return CommandResults(readable_output=f"Removed assets-{removed_assets} from site ID {site_id_int}.") def list_site_assets_command(client: Client, site_id: str, asset_type: str, target_type: str): """ List included or excluded assets or asset groups for a site. Args: client (Client): Client to use for API requests. site_id (str): The site ID. asset_type (str): Type of asset, either "assets" or "asset_groups". target_type (str): Type of target, either "included" or "excluded". Returns: CommandResults: Results of the assets or asset groups retrieval. """ site_id_int = arg_to_number(site_id, arg_name="site_id", required=True) if asset_type == "assets": res = client.send_http_request("GET", f"/sites/{site_id_int}/{target_type}_targets") output_prefix = f"Nexpose.{target_type.capitalize()}Asset" readable_title = f"{target_type.capitalize()} Asset list for site ID {site_id_int}" elif asset_type == "asset_groups": res = client.send_http_request("GET", f"/sites/{site_id_int}/{target_type}_asset_groups") output_prefix = f"Nexpose.{target_type.capitalize()}AssetGroup" readable_title = f"{target_type.capitalize()} Asset group list for site ID {site_id_int}" else: raise ValueError("Invalid asset_type. Expected 'assets' or 'asset_groups'.") outputs = dict(**res, site_id=site_id_int) readable_results = res.get("resources") if asset_type == "asset_groups" else res return CommandResults( outputs_prefix=output_prefix, outputs_key_field="id", outputs=outputs, readable_output=tableToMarkdown( readable_title, readable_results, headerTransform=string_to_table_header, removeNull=True ), raw_response=outputs, ) def create_asset_group_command( client: Client, name: str, description: str, type: str, match: str | None = None, ip_address_is: str | None = None, host_name_is: str | None = None, risk_score_higher_than: str | None = None, vulnerability_title_contains: str | None = None, site_id_in: str | None = None, site_name_in: str | None = None, query: str | None = None, ): """ Creates a new asset group in Nexpose. Args: client (Client): Client to use for API requests. name (str): The name of the asset group. description (str): The description of the asset group. type (str): The type of the asset group, valid values: "dynamic" or "static". match (str, optional): The match criteria for the asset group. ip_address_is (str, optional): Filter by IP address. host_name_is (str, optional): Filter by host name. risk_score_higher_than (str, optional): Filter by risk score higher than the specified value. vulnerability_title_contains (str, optional): Filter by vulnerability title. site_id_in (str, optional): Filter by site ID. site_name_in (str, optional): Filter by site name. query (str, optional): Additional queries to use as a filter, in the format: {field} {operator} {value}. Multiple queries can be specified, separated by a ";" separator. Returns: CommandResults: The results of the command execution. """ filters_data = parse_asset_filters( client=client, ip_address_is=ip_address_is, host_name_is=host_name_is, risk_score_higher_than=risk_score_higher_than, site_id_in=site_id_in, site_name_in=site_name_in, vulnerability_title_contains=vulnerability_title_contains, query=query, ) validate_input(type, VALID_ASSET_GROUP_TYPES, "type", False) if type == "Dynamic" and not filters_data: raise DemistoException("You must add filters to create a Dynamic asset group.") filters = convert_asset_search_filters(filters_data) res = client.create_asset_group(name=name, description=description, type=type, filters=filters, match=match) return CommandResults( outputs_prefix="Nexpose.AssetGroup", outputs_key_field="id", outputs=res, readable_output=f"A new asset group {name} created successfully with ID: {res['id']}", raw_response=res, ) def get_list_asset_group_command( client: Client, group_id: str | None = None, group_name: str | None = None, type: str | None = None, page_size: str | None = None, page: str | None = None, limit: str | None = None, sort: str | None = None, ): """ Get a list of asset groups or a asset group by ID. Args: client (Client): Client to use for API requests. id (str, optional): Get asset group by ID. name (str, optional): Filters the returned asset groups to only those containing the value within their name. type (str, optional): Filters the returned asset groups to only those of this type. page_size (str, optional): Number of records to retrieve in each API call when pagination is used. page (str, optional): A specific page to retrieve when pagination is used. Page indexing starts at 0. limit (str, optional): A number of records to limit the response to. sort (str, optional): The sorting criteria for the results. Returns: CommandResults: Results of the asset groups retrieval. """ validate_input(type, VALID_ASSET_GROUP_TYPES, "type", False) if id_int := arg_to_number(group_id, required=False): asset_groups = client.get_asset_group_by_id(id=id_int) else: page_size_int = arg_to_number(page_size, required=False) page_int = arg_to_number(page, required=False) limit_int = arg_to_number(limit, required=False) asset_groups = client.get_asset_groups( name=group_name, type=type, page_size=page_size_int, page=page_int, limit=limit_int, sort=sort ) return CommandResults( outputs_prefix="Nexpose.AssetGroup", outputs_key_field="id", outputs=asset_groups, readable_output=tableToMarkdown( "Asset groups list", remove_dict_key(deepcopy(asset_groups), "searchCriteria"), headerTransform=string_to_table_header ), raw_response=asset_groups, ) ############################################################################## # ASYNC FUNCTIONS ############################################################################## async def create_report_config_from_template(client: InsightVMClient, event_type: str) -> str: """ Creates a new report configuration asynchronously. This function waits (blocks the calling workflow) until the API responds, ensuring the report is created before moving on. """ endpoint = "/api/3/reports" # Define the payload payload = { "format": "sql-query", "name": f"PA_Asset_Collector_{int(time.time())}", "query": BASE_QUERY.get(event_type), "version": "2.3.0", } log(event_type, "Creating report config.") response = await client.http_request(method="POST", endpoint=endpoint, payload=payload) try: # The Location header contains the URL of the new report report_url = response.headers.get("Location") response.release() if not report_url: raise DemistoException(f"Failed to retrieve report URL from Location header. Status: {response.status}") report_id = report_url.split("/")[-1] return report_id except Exception as e: # Ensure connection is closed even if parsing fails response.release() raise DemistoException(f"Error creating report configuration: {e}") async def generate_report(client: InsightVMClient, report_id: str, event_type: str) -> str: """Triggers the generation of a report.""" endpoint = f"/api/3/reports/{report_id}/generate" # Use the client's http_request method for POST response = await client.http_request("POST", endpoint, payload={}) # Handle response logic data = await response.json() await response.release() instance_id = str(data.get("id")) log(event_type, f"Report generation started. Instance ID: {instance_id}") return instance_id async def check_status_of_report(client: InsightVMClient, report_id: str, instance_id: str, event_type: str) -> str: """Checks the status of a report instance with non-blocking waits.""" while True: endpoint = f"/api/3/reports/{report_id}/history/{instance_id}" # Use the client's http_request method for GET response = await client.http_request("GET", endpoint) status_data = await response.json() await response.release() status = status_data.get("status", "unknown").lower() log(event_type, f"Current report status: '{status}' for {report_id=} and {instance_id=}") if status == "complete": return instance_id if status in ["failed", "aborted"]: log(event_type, f"Report {instance_id} status is '{status}'. Re-triggering report generation") instance_id = await generate_report(client, report_id, event_type) if status in ["generated", "running", "unknown"]: log(event_type, f"Report {instance_id} still processing. Waiting 60 seconds") await asyncio.sleep(REPORT_DOWNLOAD_WAIT_TIME) async def stream_report( client: "InsightVMClient", report_id: str, instance_id: str, event_type: str ) -> AsyncGenerator[str, None]: # noqa: E501 """ Asynchronously streams the content of a Rapid7 InsightVM report. Yields decoded text lines (strings) to be consumed by the parser. """ endpoint = f"/api/3/reports/{report_id}/history/{instance_id}/output" log(event_type, f"Starting report download stream from: {endpoint}") # The report `/output` endpoint serves CSV, not JSON. The client's default # "Accept: application/json" header causes the server to respond with HTTP 406 # (Not Acceptable), surfacing as "Failed to parse CSV header on line 1". # Explicitly request CSV (falling back to any type) for this download. response = await client.http_request("GET", endpoint, headers={"Accept": "text/csv, */*"}) buffer = b"" # Buffer to hold partial lines across chunks total_lines = 0 try: content_stream = response.content # Asynchronously iterate over chunks as they arrive async for chunk in content_stream.iter_any(): buffer += chunk # Split the buffer into lines (including the delimiter in the result) lines = buffer.splitlines(keepends=True) # Update the buffer with the last (potentially incomplete) line # Check if the buffer ended with a newline before popping the last element if buffer.endswith(b"\n"): buffer = b"" else: buffer = lines.pop() # Yield all complete lines found for line_bytes in lines: total_lines += 1 # CRITICAL: Decode the byte string (bytes) into a text string (str). # The .strip() is REMOVED to preserve any trailing whitespace/newlines # for the CSV parser (though newlines are usually handled by splitlines). yield line_bytes.decode("utf-8") # Process any final content left in the buffer if buffer: total_lines += 1 yield buffer.decode("utf-8") log(event_type, f"Finished streaming report. Received {total_lines} lines.") except (Exception, asyncio.CancelledError) as e: # Log how far the download got before failing so a stalled/cut-off stream is diagnosable. # asyncio.CancelledError (raised on timeout cancellation) derives from BaseException and has an # empty str(), so it must be caught explicitly and its type logged, or the failure vanishes. log(event_type, f"Report download stream stopped after {total_lines} lines with {type(e).__name__}: {e}") demisto.error(traceback.format_exc()) raise finally: # Crucial: Always ensure the response object is released/closed await response.release() async def stream_and_parse_report( client: "InsightVMClient", report_id: str, instance_id: str, event_integration_context: dict, event_type: str, batch_size: int = DEFAULT_BATCH_SIZE, ): # --- Checkpoint Setup --- # Retrieve checkpoints: last_sent_line_raw = event_integration_context.get("last_sent_line", 0) total_records_ingested = event_integration_context.get("total_records_ingested", 0) header: Optional[List[str]] = None # The start line for the *next* batch we will build # NOTE: raw line numbers are 1-based and inclusive. start_line_for_batch_raw = last_sent_line_raw + 1 pending_tasks: set[asyncio.Task] = set() log(event_type, f"--- Starting Data Stream (Resuming from line: {start_line_for_batch_raw}) ---") # --- Lookahead Stream Setup --- try: stream_iterator = stream_report(client, report_id, instance_id, event_type) except Exception as e: raise DemistoException(f"Failed to initialize stream: {e}") line_to_process: Optional[str] = None log(event_type, f"--- Starting Data Stream for report instance {instance_id} ---") try: # INITIAL LINE READ: Get the header of the report try: # Fetch the very first line (Line 1) header_line = await anext(stream_iterator) # Parse the header immediately header = next(csv.reader(io.StringIO(header_line))) log(event_type, f"CSV Header parsed: {header}") except StopAsyncIteration: log(event_type, "Report was empty (no header found).") return except Exception as e: raise DemistoException(f"Failed to parse CSV header on line 1: {e}") try: # Try to fetch Line 2 (the first data line) line_to_process = await anext(stream_iterator) except StopAsyncIteration: # Report has a header but no data line_to_process = None # --- EXECUTION PHASE --- if line_to_process: await process_data_stream( stream_iterator=stream_iterator, line_to_process=line_to_process, header=header, last_sent_line_raw=last_sent_line_raw, total_records_ingested=total_records_ingested, batch_size=batch_size, event_type=event_type, pending_tasks=pending_tasks, event_integration_context=event_integration_context, instance_id=instance_id, ) else: log(event_type, "No data rows to process.") except (Exception, asyncio.CancelledError) as e: error_message = f"Error during streaming or sending events ({type(e).__name__}): {e}" log(event_type, error_message) demisto.error(traceback.format_exc()) raise DemistoException(error_message) from e async def process_data_stream( stream_iterator: Any, line_to_process: Optional[str], header: List[str], last_sent_line_raw: int, total_records_ingested: int, batch_size: int, event_type: str, pending_tasks: set, event_integration_context: dict, instance_id: str, ) -> None: """ Consumes the stream iterator, processes data rows, and triggers batch submission. """ # Initialize Loop State # We are starting at Line 1 because the header (Line 1) was already processed outside. current_line_count = 1 batch_counter = 0 current_batch: List[str] = [] current_run_data_records = 0 snapshot_id = event_integration_context.get("snapshot_id", "") if not snapshot_id: snapshot_id = str(round(time.time() * 1000)) log(event_type, f"Did not get snapshot_id from context, will use {snapshot_id=}") update_integration_context_by_event_type(event_type, {"snapshot_id": snapshot_id}) # The start line for the *next* batch we will build # NOTE: raw line numbers are 1-based and inclusive. start_line_for_batch_raw = last_sent_line_raw + 1 # --- MAIN LOOP --- while line_to_process is not None: current_line_count += 1 current_line = line_to_process # --- Try to read the NEXT line (Lookahead) --- next_line = await _get_next_line(stream_iterator) # 1. Skip Checkpoint if _should_skip_line(current_line_count, last_sent_line_raw): line_to_process = next_line continue # 2. Parse Data Row and Append to Batch data_row = _parse_csv_row(current_line, header, current_line_count, event_type) if data_row: current_batch.append(data_row) current_run_data_records += 1 # Count of data records in the current run else: # if the line was malformed, we continue to the next line line_to_process = next_line continue # 3. BATCHING AND LOOKAHEAD CHECK is_last_batch = next_line is None # Send batch if it's full OR if the stream has ended if _is_last_batch(len(current_batch), batch_size, is_last_batch): batch_counter += 1 start_line_for_batch_raw, total_records_ingested = await handle_batch_submission( current_batch=current_batch, start_line_for_batch_raw=start_line_for_batch_raw, total_records_ingested=total_records_ingested, current_run_data_records=current_run_data_records, is_last_batch=is_last_batch, event_type=event_type, pending_tasks=pending_tasks, snapshot_id=snapshot_id, batch_counter=batch_counter, instance_id=instance_id, ) current_run_data_records = 0 # Reset count for the new batch current_batch = [] log(event_type, f"Batch task created. Continuing stream. (Current line: {current_line_count})") # Backpressure: don't let the producer race arbitrarily far ahead of the senders (XSUP-69895). # Once too many batch tasks are in flight, wait for some to complete before reading more of the # stream. This bounds memory and in-flight connections, and surfaces a failed send promptly. while len(pending_tasks) >= MAX_IN_FLIGHT_BATCH_TASKS: done, _ = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED) for finished in done: # Re-raise the first task exception so the workflow aborts instead of continuing to # stream a report whose events are no longer being ingested. if exc := finished.exception(): raise exc # Move processing to the next line: line_to_process = next_line if pending_tasks: log(event_type, f"Waiting for {len(pending_tasks)} final submission task(s) to finish.") results = await asyncio.gather(*pending_tasks, return_exceptions=True) for res in results: if isinstance(res, Exception): raise res def _should_skip_line(current_line_count: int, last_sent_line_raw: int) -> bool: """ Determines if the current line was already processed in a previous run. """ return current_line_count <= last_sent_line_raw def _is_last_batch(len_current_batch: int, batch_size: int, is_last_batch: bool): """ Determines if this is the last batch of events to send. """ return len_current_batch >= batch_size or is_last_batch async def _get_next_line(stream_iterator: Any) -> Optional[str]: """ Safely retrieves the next line from the iterator. Returns None if the stream has ended. """ try: return await anext(stream_iterator) except StopAsyncIteration: return None def _parse_csv_row(current_line: str, header: List[str], current_line_count: int, event_type: str) -> Optional[str]: """ Parses a CSV string into a JSON string. Returns None if the row is malformed or invalid. """ try: # Parse the CSV line data_row = next(csv.reader(io.StringIO(current_line))) # Validate column count matches header if not header or len(data_row) != len(header): # Only log if it's not just an empty trailing line if "".join(data_row).strip(): log( event_type, f"Skipping malformed row {current_line_count}. " f"Got {len(data_row)} cols, expected {len(header)}.", ) return None # Success: Zip headers with data and dump to JSON record_dict = dict(zip(header, data_row)) return json.dumps(record_dict) except Exception as e: log(event_type, f"Error parsing CSV line {current_line_count}: {e}. Skipping record.") return None async def handle_batch_submission( current_batch: List[str], start_line_for_batch_raw: int, total_records_ingested: int, current_run_data_records: int, is_last_batch: bool, event_type: str, pending_tasks: set, snapshot_id: str, batch_counter: int, instance_id: str, ) -> tuple[int, int]: """ Calculates batch metrics, determines the finality indicator, spawns the background send_events task, and returns the updated state for the next batch. Returns: tuple[int, int]: (next_start_line_raw, new_total_records_ingested) """ batch_size_actual = len(current_batch) # NOTE: raw line numbers are 1-based and inclusive. # Example: if start=500 and batch size=100 → end=599 → next start=600. total_lines_after_batch_raw = start_line_for_batch_raw + batch_size_actual - 1 # Calculate the NEW cumulative total records ingested after this batch new_total_records = total_records_ingested + current_run_data_records # --- FINAL COUNT LOGIC --- if is_last_batch: # If this is the last batch, the indicator MUST be the final total record count. lines_to_send_indicator = new_total_records log( event_type, f"DEBUG: End of report number {instance_id} reached. Sending Final Report Total: \ {lines_to_send_indicator}", ) else: # Otherwise, send the placeholder (1) to indicate it's a middle batch. lines_to_send_indicator = 1 # --- END FINAL COUNT LOGIC --- # Create the asynchronous task for concurrent submission try: log(event_type, f"Sending {batch_size_actual} {event_type} to xsiam") task = asyncio.create_task( process_and_send_events_to_xsiam( current_batch, total_lines_after_batch_raw, # Raw line number for resuming stream (e.g., 1001) batch_counter, new_total_records, # Cumulative data records processed (e.g., 1000) event_type, snapshot_id, lines_to_send_indicator, ) ) # Manage the task set pending_tasks.add(task) task.add_done_callback(pending_tasks.discard) except Exception as e: log(event_type, f"CRITICAL ERROR: Failed to schedule batch submission task: {e}") if "task" in locals() and not task.done(): task.cancel() log(event_type, "Safe guard: Orphaned task cancelled.") # Reset batch trackers for the next iteration start_line_for_batch_raw = total_lines_after_batch_raw + 1 return start_line_for_batch_raw, new_total_records async def delete_report_instance(client: InsightVMClient, report_id: str, instance_id: str, event_type: str): """Deletes a specific report instance (a single run).""" endpoint = f"/api/3/reports/{report_id}/history/{instance_id}" demisto.debug(f"Deleting report instance ID: {instance_id} for {event_type}.") try: await client.http_request("DELETE", endpoint) demisto.debug(f"Report instance {instance_id} for {event_type} deleted successfully.") except Exception as e: # Expected if resource is already gone (e.g., Status 404) demisto.debug(f"Warning: Could not guarantee deletion of report {instance_id} instance. {e}") async def delete_report_configuration(client: InsightVMClient, report_id: str, event_type: str): """Deletes the entire report configuration.""" endpoint = f"/api/3/reports/{report_id}" demisto.debug(f"Deleting report configuration ID: {report_id} for {event_type}.") try: await client.http_request("DELETE", endpoint) demisto.debug(f"Report {report_id} for {event_type} configuration deleted successfully.") except Exception as e: # Expected if resource is already gone (e.g., Status 404) demisto.debug(f"Warning: Could not guarantee deletion of report configuration. {e}") def _apply_collector_changes(collector_context: Dict[str, Dict[str, Any]], collector_type: str, changes: Dict[str, Any]) -> None: """ Helper function to apply changes to a specific collector's context dict with concurrency safety logic. Modifies 'collector_context' in place. """ # 1. Ensure the sub-dictionary exists for this collector type if collector_type not in collector_context: collector_context[collector_type] = {} current_sub_dict = collector_context[collector_type] # Keys that must strictly increase during normal operation MONITORED_KEYS = {"last_sent_line", "total_records_ingested"} # 2. Determine 'finish_current_report_stream' state for logic decisions # Priority: Argument in 'changes' > Persisted value in Context > Default False if "finish_current_report_stream" in changes: finish_current_report_stream = changes["finish_current_report_stream"] else: finish_current_report_stream = current_sub_dict.get("finish_current_report_stream", False) # 3. Iterate through ALL keys in the changes dictionary for key, new_value in changes.items(): # LOGIC A: Handle Monitored Counters (prevent race conditions) if key in MONITORED_KEYS and not finish_current_report_stream: old_value = current_sub_dict.get(key, 0) if isinstance(new_value, int | float) and isinstance(old_value, int | float): if new_value > old_value: current_sub_dict[key] = new_value else: # Fallback for unexpected types current_sub_dict[key] = new_value # LOGIC B: Handle Everything Else # This includes IDs, standard keys, AND the 'finish_current_report_stream' key itself. else: current_sub_dict[key] = new_value def update_integration_context_by_event_type(event_type: str, changes: Dict[str, Any]) -> None: """ Retrieves the current integration context, updates the specified event's state with the provided changes, and saves the updated context. Args: collector_type: The top-level key to update ('asset' or 'vulnerability'). changes: A dictionary of {key: value} updates to apply. """ # 1. Get the current context current_context = get_integration_context() # 2. Apply the update using the helper function _apply_collector_changes(current_context, event_type, changes) # 3. Set the updated context back to the platform set_integration_context(current_context) log(event_type, f"State checkpoint saved. New line: {current_context[event_type]}") # noqa: E501 ################################################# # copy from akamai ################################################# async def process_and_send_events_to_xsiam( events: list[str], total_lines_after_batch_raw: int | None, batch_counter: int, new_total_records: int, event_type: str, snapshot_id: str, items_count: int = 1, ): """Send the events to CSP's send_events_to_xsiam. save the offset after the execution is done and update the module health. Args: events (list[str]): The list of json serialized events. should_skip_decode_events (bool): Whether we should skip serializing and decoding events or not. offset (str | None): The offset hash. counter (int): The current execution number. """ log(event_type, f"Running in interval = {batch_counter}. got {len(events)} lines, moving to processing data.") # noqa: E501 processed_events = events size_in_bytes = sum(len(line.encode("utf-8")) for line in events) tasks = send_events_to_xsiam_rapid7( processed_events, VENDOR.get(event_type), PRODUCT.get(event_type), should_update_health_module=False, chunk_size=size_in_bytes, send_events_asynchronously=True, url_key="host", data_format="json", data_size_expected_to_split_evenly=True, counter=batch_counter, items_count=items_count, snapshot_id=snapshot_id, ) log(event_type, f"Running in interval = {batch_counter}. Finished executing send_events_to_xsiam, waiting for tasks to end.") await asyncio.gather(*tasks) log(event_type, f"Running in interval = {batch_counter}. Finished gathering all tasks.") log(event_type, f"Running in interval = {batch_counter}. Updating module health.") update_integration_context_by_event_type( event_type, {"last_sent_line": total_lines_after_batch_raw, "total_records_ingested": new_total_records}, ) demisto.updateModuleHealth({"assetsPulled": len(processed_events)}) log(event_type, f"Running in interval = {batch_counter}. Finished updating module health.") def rapid7_send_data_to_xsiam( data, vendor, product, data_format=None, url_key="url", num_of_attempts=3, chunk_size=XSIAM_EVENT_CHUNK_SIZE, data_type=ASSETS, should_update_health_module=True, add_proxy_to_request=False, snapshot_id="", items_count=None, send_events_asynchronously=False, data_size_expected_to_split_evenly=False, counter=0, ): # pragma: no cover """ Send the supported fetched data types into the XDR data-collector private api. :type data: ``Union[str, list]`` :param data: The data to send to XSIAM server. Should be of the following: 1. List of strings or dicts where each string or dict represents an event or asset. 2. String containing raw events separated by a new line. :type vendor: ``str`` :param vendor: The vendor corresponding to the integration that originated the data. :type product: ``str`` :param product: The product corresponding to the integration that originated the data. :type data_format: ``str`` :param data_format: Should only be filled in case the 'events' parameter contains a string of raw events in the format of 'leef' or 'cef'. In other cases the data_format will be set automatically. :type url_key: ``str`` :param url_key: The param dict key where the integration url is located at. the default is 'url'. :type num_of_attempts: ``int`` :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes) :type chunk_size: ``int`` :param chunk_size: Advanced - The maximal size of each chunk size we send to API. Limit of 9 MB will be inforced. :type data_type: ``str`` :param data_type: Type of data to send to Xsiam, events or assets. :type should_update_health_module: ``bool`` :param should_update_health_module: whether to trigger the health module showing how many events were sent to xsiam This can be useful when using send_data_to_xsiam in batches for the same fetch. :type add_proxy_to_request: ``bool`` :param add_proxy_to_request: whether to add proxy to the send evnets request. :type snapshot_id: ``str`` :param snapshot_id: the snapshot id. :type items_count: ``str`` :param items_count: the asset snapshot items count. :type send_events_asynchronously: ``bool`` :param send_events_asynchronously: whether to use asyncio to send the events to xsiam asynchronously or not. Note that when set to True, the debug should be done from the integration itself. :type data_size_expected_to_split_evenly: ``bool`` :param data_size_expected_to_split_evenly: whether the events should be about the same size or not. Use this to split data to chunks faster. :return: Either None if running regularly or a list of asyncio task objects if running asynchronously:. In case of running asynchronously:, the list of tasks will hold the number of events sent and can be accessed by: await asyncio.gather(*tasks) :rtype: ``List[Task]`` or ``None`` """ data_size = 0 params = demisto.params() url = params.get(url_key) calling_context = demisto.callingContext.get("context", {}) instance_name = calling_context.get("IntegrationInstance", "") collector_name = calling_context.get("IntegrationBrand", "") if not items_count: items_count = len(data) if isinstance(data, list) else 1 if data_type not in DATA_TYPES: demisto.debug(f"data type must be one of these values: {DATA_TYPES}") return None if not data: demisto.debug( f"send_data_to_xsiam function received no {data_type}, skipping the API call to send {data_type} (data_type) to XSIAM" ) # noqa: E501 demisto.updateModuleHealth({f"{data_type}Pulled": data_size}) return None # only in case we have data to send to XSIAM we continue with this flow. # Correspond to case 1: List of strings or dicts where each string or dict represents an one event or asset or snapshot. if isinstance(data, list): # In case we have list of dicts we set the data_format to json and parse each dict to a stringify each dict. demisto.debug(f"Sending {len(data)} {data_type} (data type) to XSIAM") if isinstance(data[0], dict): data = [json.dumps(item) for item in data] data_format = "json" # Separating each event with a new line data = "\n".join(data) elif not isinstance(data, str): raise DemistoException(f"Unsupported type: {type(data)} for the {data_type} parameter. Should be a string or list.") if not data_format: data_format = "text" xsiam_api_token = demisto.getLicenseCustomField("Http_Connector.token") xsiam_domain = demisto.getLicenseCustomField("Http_Connector.url") xsiam_url = f"https://api-{xsiam_domain}" headers = remove_empty_elements( { "authorization": xsiam_api_token, "format": data_format, "product": product, "vendor": vendor, "content-encoding": "gzip", "collector-name": collector_name, "instance-name": instance_name, "final-reporting-device": url, "collector-type": ASSETS if data_type == ASSETS else EVENTS, } ) if data_type == ASSETS: if not snapshot_id: snapshot_id = str(round(time.time() * 1000)) log(data_type, f"Did not get snapshot_id, will send {snapshot_id=}") # We are setting a time stamp ahead of the instance name since snapshot-ids must be configured in ascending # alphabetical order such that first_snapshot < second_snapshot etc. headers["snapshot-id"] = snapshot_id + instance_name headers["total-items-count"] = str(items_count) header_msg = f"Error sending new {data_type} into XSIAM.\n" def data_error_handler(res): """ Internal function to parse the XSIAM API errors """ try: response = res.json() error = res.reason if response.get("error").lower() == "false": xsiam_server_err_msg = response.get("error") error += ": " + xsiam_server_err_msg except ValueError: if res.text: error = f"\n{res.text}" else: error = "Received empty response from the server" api_call_info = ( "Parameters used:\n" f"\tURL: {xsiam_url}\n" f"\tHeaders: {json.dumps(headers, indent=8)}\n\n" f"Response status code: {res.status_code}\n" f"Error received:\n\t{error}" ) demisto.error(header_msg + api_call_info) raise DemistoException(header_msg + error, DemistoException) client = BaseClient(base_url=xsiam_url, proxy=add_proxy_to_request) if data_size_expected_to_split_evenly: data_chunks = split_data_by_slices(data, chunk_size) else: data_chunks = split_data_to_chunks(data, chunk_size) def send_events(data_chunk): chunk_size = len(data_chunk) data_chunk = "\n".join(data_chunk) zipped_data = gzip.compress(data_chunk.encode("utf-8")) # type: ignore[AttributeError,attr-defined] xsiam_api_call_with_retries( client=client, events_error_handler=data_error_handler, error_msg=header_msg, headers=headers, num_of_attempts=num_of_attempts, xsiam_url=xsiam_url, zipped_data=zipped_data, is_json_response=True, data_type=data_type, ) return chunk_size async def send_events_async(data_chunk): chunk_size = len(data_chunk) data_chunk = "\n".join(data_chunk) zipped_data = gzip.compress(data_chunk.encode("utf-8")) # type: ignore[AttributeError,attr-defined] _ = await xsiam_api_call_async_with_retries( headers=headers, num_of_attempts=num_of_attempts, xsiam_url=xsiam_url, zipped_data=zipped_data, data_type=data_type ) return chunk_size if send_events_asynchronously: demisto.info(f"Running in interval = {counter}. Sending events to xsiam asynchronously.") all_chunks = list(data_chunks) demisto.info(f"Running in interval = {counter}. Finished appending all data_chunks to a list.") tasks = [asyncio.create_task(send_events_async(chunk)) for chunk in all_chunks] demisto.info(f"Finished submiting {len(tasks)} tasks for the {counter} time") return tasks else: demisto.info("Sending events to xsiam synchronously.") for chunk in data_chunks: data_size += send_events(chunk) if should_update_health_module: demisto.updateModuleHealth({f"{data_type}Pulled": data_size}) return None def split_data_by_slices(data, target_chunk_size): # pragma: no cover """ Splits a string/list of data into chunks of an approximately specified size. The actual size can be lower. the slicing is based on the assumption that all entries have the same size. :type data: ``list`` or a ``string`` :param data: A list of data or a string delimited with \n to split to chunks. :type target_chunk_size: ``int`` :param target_chunk_size: The maximum size of each chunk. The maximal size allowed is 9MB. :return: An iterable of lists where each list contains events with approx size of chunk size. :rtype: ``collections.Iterable[list]`` """ target_chunk_size = min(target_chunk_size, XSIAM_EVENT_CHUNK_SIZE_LIMIT) if isinstance(data, str): data = data.split("\n") # Handle empty list case immediately if not data: return [] entry_size = sys.getsizeof(data[0]) num_of_entries_per_chunk = target_chunk_size // entry_size step_size = max(1, num_of_entries_per_chunk) for i in range(0, len(data), step_size): chunk = data[i : i + step_size] yield chunk # Lazily-initialized concurrency guard for send-to-XSIAM requests. It is created on first use inside the # running event loop (rather than at import time) so it binds to the correct loop across asyncio.run calls. _XSIAM_SEND_SEMAPHORE: "asyncio.Semaphore | None" = None def get_xsiam_send_semaphore() -> "asyncio.Semaphore": """Return a process-wide semaphore that caps concurrent send-to-XSIAM requests (XSUP-69895).""" global _XSIAM_SEND_SEMAPHORE if _XSIAM_SEND_SEMAPHORE is None: _XSIAM_SEND_SEMAPHORE = asyncio.Semaphore(MAX_CONCURRENT_XSIAM_SENDS) return _XSIAM_SEND_SEMAPHORE async def xsiam_api_call_async_with_retries( xsiam_url, zipped_data, headers, num_of_attempts, data_type=ASSETS, ): # pragma: no cover """ Send the fetched events or assets into the XDR data-collector private api. Concurrency is bounded by a shared semaphore (MAX_CONCURRENT_XSIAM_SENDS) so that, no matter how many batch tasks the stream loop spawns, only a limited number of HTTP requests are in flight at once. This prevents overloading the ingestion endpoint, which previously caused truncated responses (aiohttp.TransferEncodingError) and connection timeouts (XSUP-69895). Retries cover transient transport failures (connection errors/timeouts, truncated payloads, asyncio timeouts) and retryable status codes (429 + 5xx) using exponential backoff. Non-retryable HTTP errors (e.g. 4xx other than 429) fail immediately. :type xsiam_url: ``str`` :param xsiam_url: The URL of XSIAM to send the api request. :type zipped_data: ``bytes`` :param zipped_data: encoded events :type headers: ``dict`` :param headers: headers for the request :type num_of_attempts: ``int`` :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes). :type data_type: ``str`` :param data_type: events or assets :return: Response object or DemistoException :rtype: ``requests.Response`` or ``DemistoException`` """ status_code = None attempt_num = 1 response = None header_msg = f"Error sending new {data_type} into XSIAM.\n" timeout = aiohttp.ClientTimeout(total=XSIAM_SEND_TIMEOUT_SECONDS) # Bound the number of concurrent in-flight send requests across the whole run. async with get_xsiam_send_semaphore(): while status_code != 200 and attempt_num < num_of_attempts + 1: demisto.debug(f"Sending {data_type} into xsiam, attempt number {attempt_num}") is_last_attempt = attempt_num >= num_of_attempts # On the last attempt we want any error to propagate (raise) instead of being retried. ok_codes = (200, 429) if not is_last_attempt else None try: async with aiohttp.ClientSession(timeout=timeout) as session: # noqa: SIM117 async with session.post(urljoin(xsiam_url, "/logs/v1/xsiam"), data=zipped_data, headers=headers) as response: try: response.raise_for_status() # This raises an exception for non-2xx status codes status_code = response.status except aiohttp.ClientResponseError as e: # 429 (and, when not the last attempt, 5xx) are retryable; everything else is fatal. is_retryable_status = e.status == 429 or 500 <= e.status < 600 if ok_codes and is_retryable_status: demisto.debug(f"Got retryable status code {e.status} on attempt {attempt_num}, will retry.") status_code = e.status else: demisto.debug( f"XSIAM API returned a non-retryable error. Status: {e.status}, " f"Message: {e.message}, Request URL: {e.request_info}" ) serializable_headers = dict(e.headers) if e.headers else {} api_call_info = ( "Parameters used:\n" f"\tURL: {xsiam_url}\n" f"\tHeaders: {json.dumps(serializable_headers, indent=8)}\n\n" f"Response status code: {e.status}\n" f"Error received:\n\t{e.message}\n" f"additional request info: \n\t{e.request_info}" ) demisto.error(header_msg + api_call_info) demisto.updateModuleHealth(header_msg + e.message, is_error=True) raise except ( aiohttp.ClientConnectionError, aiohttp.ClientPayloadError, asyncio.TimeoutError, ) as e: # Transient transport-level failures: truncated responses (TransferEncodingError is a # ClientPayloadError), connection resets/timeouts, and request timeouts. These previously # bypassed the retry loop entirely and aborted the whole fetch (XSUP-69895). if is_last_attempt: demisto.error(f"{header_msg}Transport error after {attempt_num} attempts: {e!r}") demisto.updateModuleHealth(f"{header_msg}{e!r}", is_error=True) raise demisto.debug(f"Got retryable transport error on attempt {attempt_num}: {e!r}. Will retry.") status_code = None demisto.debug(f"received status code: {status_code}") if status_code != 200: # Exponential backoff (1s, 2s, 4s, ... capped at 30s) to let the endpoint recover. await asyncio.sleep(min(2 ** (attempt_num - 1), 30)) attempt_num += 1 return response def send_events_to_xsiam_rapid7( events, vendor, product, data_format=None, url_key="url", num_of_attempts=3, chunk_size=XSIAM_EVENT_CHUNK_SIZE, should_update_health_module=True, add_proxy_to_request=False, send_events_asynchronously=True, data_size_expected_to_split_evenly=False, counter=0, items_count=1, snapshot_id="", ): # pragma: no cover """ Send the fetched events into the XDR data-collector private api. :type events: ``Union[str, list]`` :param events: The events to send to XSIAM server. Should be of the following: 1. List of strings or dicts where each string or dict represents an event. 2. String containing raw events separated by a new line. :type vendor: ``str`` :param vendor: The vendor corresponding to the integration that originated the events. :type product: ``str`` :param product: The product corresponding to the integration that originated the events. :type data_format: ``str`` :param data_format: Should only be filled in case the 'events' parameter contains a string of raw events in the format of 'leef' or 'cef'. In other cases the data_format will be set automatically. :type url_key: ``str`` :param url_key: The param dict key where the integration url is located at. the default is 'url'. :type num_of_attempts: ``int`` :param num_of_attempts: The num of attempts to do in case there is an api limit (429 error codes) :type chunk_size: ``int`` :param chunk_size: Advanced - The maximal size of each chunk size we send to API. Limit of 9 MB will be inforced. :type should_update_health_module: ``bool`` :param should_update_health_module: whether to trigger the health module showing how many events were sent to xsiam :type add_proxy_to_request :``bool`` :param add_proxy_to_request: whether to add proxy to the send evnets request. :type send_events_asynchronously: ``bool`` :param send_events_asynchronously: whether to use asyncio to send the events to xsiam asynchronously or not. Note that when set to True, the debug should be done from the integration itself. :type data_size_expected_to_split_evenly: ``bool`` :param data_size_expected_to_split_evenly: whether the events should be about the same size or not. Use this to split data to chunks faster. :return: Either None if running regularly or a list of asyncio task objects if running asynchronously:. In case of running asynchronously:, the list of tasks will hold the number of events sent and can be accessed by: await asyncio.gather(*tasks) :rtype: ``List[Task]`` or ``None`` """ return rapid7_send_data_to_xsiam( events, vendor, product, data_format, url_key, num_of_attempts, chunk_size, data_type="assets", should_update_health_module=should_update_health_module, add_proxy_to_request=add_proxy_to_request, send_events_asynchronously=send_events_asynchronously, data_size_expected_to_split_evenly=data_size_expected_to_split_evenly, counter=counter, items_count=items_count, snapshot_id=snapshot_id, ) async def run_full_collector_workflow(client: InsightVMClient, event_type: str, batch_size: int = DEFAULT_BATCH_SIZE): """ Orchestrates the full report lifecycle: State Check, Create/Generate, Stream, Cleanup. The function signature is adapted to receive the client instance. """ integration_context = get_integration_context() log(event_type, f"Got integration_context: {integration_context}") # 1. RETRIEVE STATE event_integration_context = integration_context.get(event_type, {}) report_id = event_integration_context.get("report_id", "") instance_id = event_integration_context.get("instance_id", "") finish_current_report_stream = event_integration_context.get( "finish_current_report_stream", False ) # if we finished the current report, but the server crashed before removing the instance id and report id from the context if not finish_current_report_stream: try: if report_id and instance_id: log(event_type, f"State found in context. Resuming check for Report ID: {report_id}, Instance ID: {instance_id}") # If IDs exist, go straight to checking the status of the ongoing report instance_id = await check_status_of_report(client, report_id, instance_id, event_type) else: log(event_type, "No state found. Starting new report generation.") if not report_id: raise DemistoException(f"Report ID for {event_type} is missing from context despite setup phase.") # 2. GENERATE REPORT instance_id = await generate_report(client, report_id, event_type) update_integration_context_by_event_type(event_type, {"instance_id": instance_id}) # 3. CHECK STATUS (Will wait asynchronously until finished or retry if failed) instance_id = await check_status_of_report(client, report_id, instance_id, event_type) update_integration_context_by_event_type(event_type, {"instance_id": instance_id}) # 4. STREAM DATA AND SEND EVENTS log(event_type, "\n--- Starting Data Streaming Phase ---") await stream_and_parse_report(client, report_id, instance_id, event_integration_context, event_type, batch_size) update_integration_context_by_event_type( event_type, {"last_sent_line": 0, "finish_current_report_stream": True, "snapshot_id": "", "total_records_ingested": 0}, ) log(event_type, "--- Data Streaming Phase Complete ---") except Exception as e: log(event_type, f"\nFATAL WORKFLOW ERROR: {e}") # Preserve the original exception message to ensure test expectations are met raise DemistoException(f"Got the following error: {str(e)}") # 5. CLEANUP: log(event_type, "\n--- Starting Cleanup Phase ---") if report_id and instance_id: await delete_report_instance(client, report_id, instance_id, event_type) if report_id: await delete_report_configuration(client, report_id, event_type) # Clear all state markers in context for the next 12-hour run update_integration_context_by_event_type( event_type, {"instance_id": "", "report_id": "", "finish_current_report_stream": False} ) log(event_type, "--- Cleanup Phase Complete ---") async def ensure_report_config_exists(client: InsightVMClient, event_type: str, context: dict) -> None: """ Checks if a report configuration exists in the integration context. If not, it creates one sequentially and updates the context. """ existing_id = context.get(event_type, {}).get("report_id") if existing_id: log(event_type, f"Report config already exists (ID: {existing_id}). Skipping creation.") return log(event_type, "No report config found. Creating new configuration.") # We await this call so it finishes completely before moving on new_report_id = await create_report_config_from_template(client, event_type) update_integration_context_by_event_type(event_type, {"report_id": new_report_id}) log(event_type, f"Context updated with new Report ID: {new_report_id}") async def run_all_collectors( client: InsightVMClient, batch_size: int = DEFAULT_BATCH_SIZE, ): """ Runs the Asset and Vulnerability report collector workflows concurrently, using the provided, initialized InsightVMClient. """ try: context = get_integration_context() await ensure_report_config_exists(client, "asset", context) await ensure_report_config_exists(client, "vulnerability", context) except Exception as e: raise DemistoException(f"FATAL: Failed during report configuration setup. Aborting run. Error: {e}") demisto.debug("Starting concurrent execution of Asset and Vulnerability collectors") asset_task = run_full_collector_workflow(client=client, batch_size=batch_size, event_type="asset") vulnerability_task = run_full_collector_workflow(client=client, batch_size=batch_size, event_type="vulnerability") # Run both tasks concurrently. return_exceptions=True ensures one failure # doesn't immediately stop the other task. results = await asyncio.gather(asset_task, vulnerability_task, return_exceptions=True) demisto.debug("\n--- Concurrent Execution Complete ---") success = True exceptions = [] for i, result in enumerate(results): if isinstance(result, Exception): task_name = "Asset Collector" if i == 0 else "Vulnerability Collector" exceptions.append(f"!!! ERROR: {task_name} failed: {result}") success = False if not success: # Aggregate all errors into a single failure message error_message = "\n".join(exceptions) raise DemistoException(f"One or more concurrent collector workflows failed:\n{error_message}") demisto.debug("All collector workflows finished successfully.") async def fetch_assets_command(params: dict, token: str) -> None: """Fetch assets command - replaces long-running-execution. The platform's fetch-assets scheduler handles re-invocation at the configured interval. Each invocation runs the full collector pipeline once. """ demisto.debug("Starting fetch-assets execution") async with InsightVMClient( base_url=params["server"], username=params["credentials"].get("identifier"), password=params["credentials"].get("password"), token=token, verify=not params.get("unsecure"), ) as client: await run_all_collectors(client, batch_size=DEFAULT_BATCH_SIZE) demisto.debug("Finished fetch-assets execution") def main(): # pragma: no cover try: args = demisto.args() params = demisto.params() command = demisto.command() handle_proxy() # A workaround for fixing compatibility issues when upgrading existing instances that are < 1.2.0. # ('token' field was converted from type 0 to type 9) token = "" if params.get("token", ""): if isinstance(params["token"], str): token = params["token"] elif params["token"].get("identifier"): token = params["token"]["identifier"] client = Client( url=params["server"], username=params["credentials"].get("identifier"), password=params["credentials"].get("password"), token=token, verify=not params.get("unsecure"), connection_error_retries=arg_to_number(params.get("connection_error_retries", CONNECTION_ERRORS_RETRIES)) or CONNECTION_ERRORS_RETRIES, ) results: CommandResults | list[CommandResults] | dict | str if command == "test-module": client.get_assets(page_size=1, limit=1) results = "ok" elif command == "fetch-assets": asyncio.run(fetch_assets_command(params, token)) return elif command == "nexpose-create-asset": results = create_asset_command(client=client, **args) elif command == "nexpose-create-assets-report": results = create_assets_report_command(client=client, report_format=args.pop("format", None), **args) elif command == "nexpose-create-scan-report": results = create_scan_report_command(client=client, report_format=args.pop("format", None), **args) elif command == "nexpose-create-scan-schedule": results = create_scan_schedule_command(client=client, **args) elif command == "nexpose-create-shared-credential": results = create_shared_credential_command(client=client, **args) elif command == "nexpose-create-site": results = create_site_command(client=client, template_id=args.pop("scanTemplateId", None), **args) elif command == "nexpose-create-sites-report": results = create_sites_report_command(client=client, report_format=args.pop("format", None), **args) elif command == "nexpose-create-site-scan-credential": results = create_site_scan_credential_command(client=client, **args) elif command == "nexpose-create-vulnerability-exception": results = create_vulnerability_exception_command(client=client, **args) elif command == "nexpose-delete-asset": results = delete_asset_command(client=client, asset_id=args.pop("id")) elif command == "nexpose-delete-scan-schedule": results = delete_scan_schedule_command(client=client, **args) elif command == "nexpose-delete-shared-credential": results = delete_shared_credential_command(client=client, shared_credential_id=args.pop("id")) elif command == "nexpose-delete-site-scan-credential": results = delete_site_scan_credential_command(client=client, **args) elif command == "nexpose-delete-vulnerability-exception": results = delete_vulnerability_exception_command(client=client, vulnerability_exception_id=args.pop("id")) elif command == "nexpose-delete-site": results = delete_site_command(client=client, site_id=args.pop("id", None), **args) elif command == "nexpose-disable-shared-credential": results = set_assigned_shared_credential_status_command(client=client, enabled=False, **args) elif command == "nexpose-download-report": results = download_report_command(client=client, report_format=args.pop("format"), **args) elif command == "nexpose-enable-shared-credential": results = set_assigned_shared_credential_status_command(client=client, enabled=True, **args) elif command == "nexpose-get-asset": results = get_asset_command(client=client, asset_id=args.pop("id")) elif command == "nexpose-get-asset-tags": results = get_asset_tags_command(client=client, asset_id=args.pop("asset_id")) elif command == "nexpose-get-asset-vulnerability": results = get_asset_vulnerability_command( client=client, asset_id=args.pop("id"), vulnerability_id=args.pop("vulnerabilityId") ) elif command == "nexpose-get-assets": results = get_assets_command(client=client, **args) elif command == "nexpose-get-report-templates": results = get_report_templates_command(client=client) elif command == "nexpose-get-report-status": results = get_generated_report_status_command(client=client, **args) elif command == "nexpose-get-scan": results = get_scan_command(client=client, scan_ids=args.pop("id")) elif command == "nexpose-get-scans": results = get_scans_command(client=client, **args) elif command == "nexpose-get-sites": results = get_sites_command(client=client, **args) elif command == "nexpose-list-assigned-shared-credential": results = list_assigned_shared_credential_command(client=client, **args) elif command == "nexpose-list-site-scan-credential": results = list_site_scan_credential_command(client=client, **args) elif command == "nexpose-list-vulnerability": results = list_vulnerability_command(client=client, vulnerability_id=args.pop("id", None), **args) elif command == "nexpose-list-vulnerability-exceptions": results = list_vulnerability_exceptions_command( client=client, vulnerability_exception_id=args.pop("id", None), **args ) elif command == "nexpose-list-scan-schedule": results = list_scan_schedule_command(client=client, **args) elif command == "nexpose-list-shared-credential": results = list_shared_credential_command(client=client, credential_id=args.pop("id", None), **args) elif command == "nexpose-pause-scan": results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.PAUSE) elif command == "nexpose-resume-scan": results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.RESUME) elif command == "nexpose-update-shared-credential": results = update_shared_credential_command(client=client, shared_credential_id=args.pop("id"), **args) elif command == "nexpose-update-site-scan-credential": results = update_site_scan_credential_command(client=client, **args) elif command == "nexpose-update-scan-schedule": results = update_scan_schedule_command(client=client, **args) elif command == "nexpose-update-vulnerability-exception-expiration": results = update_vulnerability_exception_expiration_command( client=client, vulnerability_exception_id=args.pop("id"), **args ) elif command == "nexpose-update-vulnerability-exception-status": results = update_vulnerability_exception_status_command( client=client, vulnerability_exception_id=args.pop("id"), **args ) elif command == "nexpose-search-assets": results = search_assets_command( client=client, query=args.pop("query", None), ip_address_is=args.pop("ipAddressIs", None), host_name_is=args.pop("hostNameIs", None), risk_score_higher_than=args.pop("riskScoreHigherThan", None), vulnerability_title_contains=args.pop("vulnerabilityTitleContains", None), site_id_in=args.pop("siteIdIn", None), site_name_in=args.pop("siteNameIn", None), **args, ) elif command == "nexpose-start-assets-scan": results = start_assets_scan_command( client=client, ip_addresses=args.pop("IPs", None), hostnames=args.pop("hostNames", None), **args ) elif command == "nexpose-start-site-scan": results = start_site_scan_command(client=client, site_id=args.pop("site", None), **args) elif command == "nexpose-stop-scan": results = update_scan_command(client=client, scan_id=args.pop("id"), scan_status=ScanStatus.STOP) elif command == "nexpose-create-tag": results = create_tag_command(client=client, **args) elif command == "nexpose-delete-tag": results = delete_tag_command(client=client, **args) elif command == "nexpose-list-tag": results = get_list_tag_command(client=client, **args) elif command == "nexpose-update-tag-search-criteria": results = update_tag_search_criteria_command(client=client, **args) elif command == "nexpose-list-tag-asset-group": results = get_list_tag_asset_group_command(client=client, **args) elif command == "nexpose-add-tag-asset-group": results = add_tag_asset_group_command(client=client, **args) elif command == "nexpose-remove-tag-asset-group": results = remove_tag_asset_group_command(client=client, **args) elif command == "nexpose-list-tag-asset": results = get_list_tag_asset_command(client=client, **args) elif command == "nexpose-add-tag-asset": results = add_tag_asset_command(client=client, **args) elif command == "nexpose-remove-tag-asset": results = remove_tag_asset_command(client=client, **args) elif command == "nexpose-add-site-included-asset": results = add_site_asset_command(client=client, target_type="included", **args) elif command == "nexpose-remove-site-included-asset": results = remove_site_asset_command(client=client, target_type="included", **args) elif command == "nexpose-list-site-included-asset": results = list_site_assets_command(client=client, asset_type="assets", target_type="included", **args) elif command == "nexpose-list-site-included-asset-group": results = list_site_assets_command(client=client, asset_type="asset_groups", target_type="included", **args) elif command == "nexpose-add-site-excluded-asset": results = add_site_asset_command(client=client, target_type="excluded", **args) elif command == "nexpose-remove-site-excluded-asset": results = remove_site_asset_command(client=client, target_type="excluded", **args) elif command == "nexpose-list-site-excluded-asset": results = list_site_assets_command(client=client, asset_type="assets", target_type="excluded", **args) elif command == "nexpose-list-site-excluded-asset-group": results = list_site_assets_command(client=client, asset_type="asset_groups", target_type="excluded", **args) elif command == "nexpose-list-asset-group": results = get_list_asset_group_command(client=client, **args) elif command == "nexpose-create-asset-group": results = create_asset_group_command(client=client, **args) else: raise NotImplementedError(f"Command {command} not implemented.") if isinstance(results, list) and len(results) == 1: return_results(results[0]) else: return_results(results) except Exception as e: return_error(str(e)) if __name__ in ("__main__", "builtin", "builtins"): # pragma: no cover main()