Tenable.io
A comprehensive asset-centric solution to accurately track resources while accommodating dynamic assets such as cloud, mobile devices, containers, and web applications. Fetch capabilities are only available for certain licenses.
Vulnerability Management · Tenable Vulnerability Management (formerly Tenable.io)
Details
| ID | Tenable.io |
|---|---|
| Provider | Tenable |
| Category | Vulnerability Management |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM Exposure Management |
README
A comprehensive asset-centric solution to accurately track resources while accommodating dynamic assets such as cloud, mobile devices, containers, and web applications.
This integration was integrated and tested with January 2023 release of Tenable.io.
Configure Tenable Vulnerability Management on Cortex XSOAR/XSIAM
- After installing the Tenable Vulnerability Management (formerly Tenable.io) content pack, do one of the following:
- Cortex XSIAM: Go to Settings > Configurations > Automation & Feed Integrations.
- Cortex XSOAR 8: Go to Settings & Info > Instances.
- Cortex XSOAR 6: Go to Settings > Integrations > Servers & Services.
-
Search for Tenable Vulnerability Management.
Click Add instance to create and configure a new integration instance.Parameter Description Required URL Tenable URL. True Access Key Tenable API access key. True Secret Key Tenable API secret key. True Events Fetch Interval Fetch interval in minutes for events. False Assets Fetch Interval Fetch interval in minutes for assets and vulnerabilities. False Severity The severity of the vulnerabilities to include in the export. False First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) False Max Fetch The maximum number of audit logs to retrieve for each event type. For more information about event types see the help section. False Trust any certificate (not secure) False Use system proxy settings False -
Click Test to validate the URLs, token, and connection.
Permissions
| Command Name | Required Permissions |
|---|---|
| tenable-io-list-scans | BASIC [16] user permissions and CAN VIEW [16] scan permissions. |
| tenable-io-launch-scan | SCAN OPERATOR [24] user permissions. |
| tenable-io-get-scan-report | BASIC [16] user permissions. |
| tenable-io-get-vulnerability-details | BASIC [16] user permissions. |
| tenable-io-get-vulnerabilities-by-asset | BASIC [16] user permissions. |
| tenable-io-get-scan-status | BASIC [16] user permissions and CAN VIEW [16] scan permissions. |
| tenable-io-resume-scan | SCAN OPERATOR [24] user permissions and CAN EXECUTE [32] scan permissions. |
| tenable-io-pause-scan | SCAN OPERATOR [24] user permissions and CAN EXECUTE [32] scan permissions. |
| tenable-io-get-asset-details | BASIC [16] user permissions. |
| tenable-io-export-assets | ADMINISTRATOR [64] user permissions. |
| tenable-io-export-vulnerabilities | ADMINISTRATOR [64] user permissions. |
| tenable-io-list-scan-filters | BASIC [16] user permissions |
| tenable-io-get-scan-history | SCAN OPERATOR [24] user permissions and CAN VIEW [16] scan permissions |
| tenable-io-export-scan | SCAN OPERATOR [24] user permissions and CAN VIEW [16] scan permissions |
Concurrency Limits
| Limitations | Commands name |
|---|---|
| Three concurrent requests per Tenable.io customer instance. Note: This limit is subject to change. |
tenable-io-list-scans tenable-io-launch-scan tenable-io-get-scan-report tenable-io-get-vulnerability-details tenable-io-get-vulnerabilities-by-asset tenable-io-get-scan-status tenable-io-resume-scan tenable-io-pause-scan tenable-io-get-asset-details |
| Two concurrent asset exports per container. Tenable.io also prevents duplicate exports from running concurrently. For example, export requests with the same filters. |
tenable-io-export-assets tenable-io-export-vulnerabilities |
Notes
- Fetch assets and vulnerabilities (Beta) command fetches assets and vulnerabilities from the last 90 days only.
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.
tenable-io-list-scans
Retrieves scans from the Tenable platform.
Base Command
tenable-io-list-scans
Input
| Argument Name | Description | Required |
|---|---|---|
| folderId | The ID of the folder whose scans should be listed. Scans are stored in specific folders on Tenable, e.g.: folderId=8. |
Optional |
| lastModificationDate | Limit the results to those that have only changed since this time. Date format will be YYYY-MM-DD format or relational expressions like “7 days ago”. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | number | The unique ID of the scan. |
| TenableIO.Scan.Name | string | The name of the scan. |
| TenableIO.Scan.Target | string | The targets to scan. |
| TenableIO.Scan.Status | string | The status of the scan (completed, aborted, imported, pending, running, resuming, canceling, cancelled, pausing, paused, stopping, stopped). |
| TenableIO.Scan.StartTime | date | The scheduled start time for the scan. |
| TenableIO.Scan.EndTime | date | The scheduled end time for the scan. |
| TenableIO.Scan.Enabled | boolean | If true, the schedule for the scan is enabled. |
| TenableIO.Scan.Type | string | The type of scan (local, remote, or agent). |
| TenableIO.Scan.Owner | string | The owner of the scan. |
| TenableIO.Scan.Scanner | string | The scanner assigned for the scan. |
| TenableIO.Scan.Policy | string | The policy assigned for the scan. |
| TenableIO.Scan.CreationDate | date | The creation date for the scan in Unix time. |
| TenableIO.Scan.LastModificationDate | date | The last modification date for the scan in Unix time. |
| TenableIO.Scan.FolderId | number | The unique ID of the folder where the scan has been stored. |
Command example
#### Context Example
```json
{
"TenableIO": {
"Scan": [
{
"CreationDate": "2024-11-07T11:11:05Z",
"Enabled": false,
"EndTime": "2024-11-07T11:11:05Z",
"FolderId": 5,
"Id": 10,
"LastModificationDate": "2024-05-07T11:11:05Z",
"Name": "some_name",
"Owner": "some_owner",
"Policy": "Host Discovery",
"StartTime": "2024-11-07T11:11:05Z",
"Status": "aborted",
"Targets": "1.1.1.1, 0.0.0.0",
"Type": "remote"
},
]
}
}
Human Readable Output
Tenable.io - List of Scans
FolderId Id Name Targets Status StartTime EndTime Enabled Type Owner Scanner Policy CreationDate LastModificationDate 5 10 some_name 1.1.1.1, 0.0.0.0 aborted Thu Nov 07 11:11:05 2024 Thu Nov 07 11:11:05 2024 false remote some_owner Host Discovery Thu Nov 07 11:11:05 2024 Thu Nov 07 11:11:05 2024
tenable-io-launch-scan
Launches a scan with existing or custom targets. You can specify custom targets in the command arguments.
Base Command
tenable-io-launch-scan
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | The ID of the scan to launch. | Required |
| scanTargets | If specified, targets to be scanned instead of the default. This value can be an array where each index is a target, or an array with a single index of comma-separated targets. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | number | The unique ID of the scan. |
| TenableIO.Scan.Targets | string | The targets to scan. |
| TenableIO.Scan.Status | string | The status of the scan (completed, aborted, imported, pending, running, resuming, canceling, cancelled, pausing, paused, stopping, stopped). |
Command example
!tenable-io-launch-scan scanId="10"
Context Example
{
"TenableIO": {
"Scan": {
"Id": "10",
"Status": "pending",
"Targets": "target_1,target_2,target_3"
}
}
}
The requested scan was launched successfully
Id Targets Status 10 target_1,target_2,target_3 pending
tenable-io-get-scan-report
Retrieves a scan report for the specified scan.
Base Command
tenable-io-get-scan-report
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | The ID of the scan to retrieve. | Required |
| detailed | If true, the report will contain remediation and host information for the specified scan. Otherwise, the report will only contain vulnerabilities. Possible values: “yes” and “no”. Possible values are: yes, no. Default is no. | Optional |
| info | Whether to return the basic details of the specified scan. Possible values: “yes” and “no”. Possible values are: yes, no. Default is no. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | number | The unique ID of the scan. |
| TenableIO.Scan.Name | string | The name of the scan. |
| TenableIO.Scan.Targets | string | The targets to scan. |
| TenableIO.Scan.Status | string | The status of the scan (“completed”, “aborted”, “imported”, “pending”, “running”, “resuming”, “canceling”, “cancelled”, “pausing”, “paused”, “stopping”, “stopped”). |
| TenableIO.Scan.StartTime | string | The scheduled start time for the scan. |
| TenableIO.Scan.EndTime | string | The scheduled end time for the scan. |
| TenableIO.Scan.Scanner | string | The scanner assigned for the scan. |
| TenableIO.Scan.Policy | string | The policy assigned to the scan. |
| TenableIO.Vulnerabilities.Id | string | The unique ID of the vulnerability. |
| TenableIO.Vulnerabilities.Name | string | The name of the vulnerability. |
| TenableIO.Vulnerabilities.Severity | number | The severity level of the vulnerability. |
| TenableIO.Vulnerabilities.Description | string | The description of the vulnerability. |
| TenableIO.Vulnerabilities.Synopsis | string | A brief summary of the vulnerability. |
| TenableIO.Vulnerabilities.Solution | string | Information on how to fix the vulnerability. |
| TenableIO.Vulnerabilities.FirstSeen | date | When the vulnerability was first seen. |
| TenableIO.Vulnerabilities.LastSeen | date | When the vulnerability was last seen. |
| TenableIO.Vulnerabilities.VulnerabilityOccurences | number | A count of the vulnerability occurrences. |
| TenableIO.Assets.Hostname | string | The name of the host. |
| TenableIO.Assets.Score | number | The overall score for the host. |
| TenableIO.Assets.Critical | number | The percentage of critical findings on the host. |
| TenableIO.Assets.High | number | The number of high findings on the host. |
| TenableIO.Assets.Medium | number | The number of medium findings on the host. |
| TenableIO.Assets.Low | number | The number of low findings on the host. |
| TenableIO.Remediations.Id | string | The unique ID of the remediation. |
| TenableIO.Remediations.Description | string | Specific information related to the vulnerability and steps to remediate. |
| TenableIO.Remediations.AffectedHosts | number | The number of hosts affected. |
| TenableIO.Remediations.AssociatedVulnerabilities | number | The number of vulnerabilities associated with the remedy. |
Command example
!tenable-io-get-scan-report scanId="10"
Context Example
{
"TenableIO": {
"Vulnerabilities": [
{
"Description": "description",
"FirstSeen": "2024-11-07T11:11:05Z",
"Id": 00000,
"LastSeen": "2024-11-07T11:11:05Z",
"Name": "some_name",
"Severity": "None",
"Solution": "Solution",
"Synopsis": "Synopsis",
"VulnerabilityOccurences": 26
},
{
"Description": "description",
"FirstSeen": "2024-11-07T11:11:05Z",
"Id": 11111,
"LastSeen": "2024-11-07T11:11:05Z",
"Name": "some_name",
"Severity": "None",
"Synopsis": "Synopsis",
"VulnerabilityOccurences": 12
},
]
}
}
Human Readable Output
Vulnerabilities
Id Name Severity Description Synopsis Solution FirstSeen LastSeen VulnerabilityOccurences 00000 some_name None description Synopsis Solution 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 26 11111 some_name None description Synopsis 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 12
tenable-io-get-vulnerability-details
Retrieves details for the specified vulnerability.
Base Command
tenable-io-get-vulnerability-details
Input
| Argument Name | Description | Required |
|---|---|---|
| vulnerabilityId | The unique ID of the vulnerability. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Vulnerabilities.Name | string | The name of the vulnerability. |
| TenableIO.Vulnerabilities.Severity | number | Integer [0-4] indicating how severe the vulnerability is, where 0 is info only. |
| TenableIO.Vulnerabilities.Type | string | The type of the vulnerability. |
| TenableIO.Vulnerabilities.Family | string | Object containing plugin information such as family, type, and publication and modification dates. |
| TenableIO.Vulnerabilities.Description | string | The description of the vulnerability. |
| TenableIO.Vulnerabilities.Synopsis | string | A brief summary of the vulnerability. |
| TenableIO.Vulnerabilities.Solution | string | Information on how to fix the vulnerability. |
| TenableIO.Vulnerabilities.FirstSeen | date | When the vulnerability was first seen. |
| TenableIO.Vulnerabilities.LastSeen | date | When the vulnerability was last seen. |
| TenableIO.Vulnerabilities.PublicationDate | date | The publication date of the vulnerability. |
| TenableIO.Vulnerabilities.ModificationDate | date | The last modification date for the vulnerability in Unix time. |
| TenableIO.Vulnerabilities.VulnerabilityOccurences | number | A count of the vulnerability occurrences. |
| TenableIO.Vulnerabilities.CvssVector | string | The Common Vulnerability Scoring System vector. |
| TenableIO.Vulnerabilities.CvssBaseScore | string | The Common Vulnerability Scoring System allotted base score. |
| TenableIO.Vulnerabilities.Cvss3Vector | string | The Common Vulnerability Scoring System version 3 vector. |
| TenableIO.Vulnerabilities.Cvss3BaseScore | string | The Common Vulnerability Scoring System version 3 allotted base score. |
Command example
!tenable-io-get-vulnerability-details vulnerabilityId=fake_id
Context Example
{
"TenableIO": {
"Vulnerabilities": {
"Description": "Description",
"Family": "General",
"FirstSeen": "2024-11-07T11:11:05Z",
"LastSeen": "2024-11-07T11:11:05Z",
"ModificationDate": "2024-11-07T11:11:05Z",
"Name": "Name",
"PublicationDate": "2024-11-07T11:11:05Z",
"Severity": "None",
"Synopsis": "Synopsis",
"Type": "remote",
"VulnerabilityOccurences": 1
}
}
}
Human Readable Output
Vulnerability details - fake_id
Name Severity Type Family Description Synopsis FirstSeen LastSeen PublicationDate ModificationDate VulnerabilityOccurences Name None remote General Description Synopsis 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 1
tenable-io-get-vulnerabilities-by-asset
Gets a list of up to 5000 of the vulnerabilities recorded for a specified asset.
Base Command
tenable-io-get-vulnerabilities-by-asset
Input
| Argument Name | Description | Required |
|---|---|---|
| hostname | Hostname of the asset. | Optional |
| ip | IP of the asset. | Optional |
| dateRange | The number of days of data prior to and including today that should be returned. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Assets.Hostname | number | Hostname of the asset. |
| TenableIO.Assets.Vulnerabilities | number | A list of all the vulnerability IDs associated with the asset. |
| TenableIO.Vulnerabilities.Id | number | The vulnerability unique ID. |
| TenableIO.Vulnerabilities.Name | string | The name of the vulnerability. |
| TenableIO.Vulnerabilities.Severity | number | Integer [0-4] indicating how severe the vulnerability is, where 0 is info only. |
| TenableIO.Vulnerabilities.Family | string | The vulnerability family. |
| TenableIO.Vulnerabilities.VulnerabilityOccurences | number | The number of times the vulnerability was found. |
| TenableIO.Vulnerabilities.VulnerabilityState | string | The current state of the reported vulnerability (“Active”, “Fixed”, “New”, etc.). |
Command example
!tenable-io-get-vulnerabilities-by-asset hostname="debian8628.aspadmin.net"
Context Example
{
"TenableIO": {
"Assets": {
"Hostname": "debian8628.aspadmin.net",
"Vulnerabilities": [
11111,
22222,
]
},
"Vulnerabilities": [
{
"Family": "General",
"Id": 11111,
"Name": "Name_01",
"Severity": "None",
"VulnerabilityOccurences": 2,
"VulnerabilityState": "Active"
},
{
"Family": "General",
"Id": 22222,
"Name": "Name_02",
"Severity": "None",
"VulnerabilityOccurences": 2,
"VulnerabilityState": "Active"
},
]
}
}
Human Readable Output
Vulnerabilities for asset debian8628.aspadmin.net
Id Name Severity Family VulnerabilityOccurences VulnerabilityState 11111 Name_01 None General 2 Active 22222 Name_02 None General 2 Active
tenable-io-get-scan-status
Checks the status of a specific scan using the scan ID. Possible values: “Running”, “Completed”, and “Empty” (Ready to run).
Base Command
tenable-io-get-scan-status
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | The unique ID of the scan. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | string | The unique ID of the scan specified. |
| TenableIO.Scan.Status | string | The status of the scan specified. |
Command example
!tenable-io-get-scan-status scanId="10"
Context Example
{
"TenableIO": {
"Scan": {
"Id": "10",
"Status": "aborted"
}
}
}
Human Readable Output
Scan status for 10
Id Status 10 aborted
tenable-io-resume-scan
Resumes all scans inputted as an array. Will resume scans whose status is ‘Paused’.
Base Command
tenable-io-resume-scan
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | Comma-separated list of scan IDs. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | String | The unique ID of the scan. |
| TenableIO.Scan.Status | String | The status of the scan (completed, aborted, imported, pending, running, resuming, canceling, cancelled, pausing, paused, stopping, stopped). |
Command example
!tenable-io-resume-scan scanId="13"
Context Example
{
"TenableIO": {
"Scan": {
"Id": "13",
"Status": "Resuming"
}
}
}
Human Readable Output
The requested scan was resumed successfully
Id Status 13 Resuming
tenable-io-pause-scan
Pauses all scans inputted as an array. Will pause scans whose status is ‘Running’.
Base Command
tenable-io-pause-scan
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | Comma-separated list of scan IDs. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Scan.Id | String | The unique id of the scan. |
| TenableIO.Scan.Status | String | The status of the scan (completed, aborted, imported, pending, running, resuming, canceling, cancelled, pausing, paused, stopping, stopped). |
Command example
!tenable-io-pause-scan scanId="10"
Context Example
{
"TenableIO": {
"Scan": {
"Id": "10",
"Status": "Pausing"
}
}
}
Human Readable Output
The requested scan was paused successfully
Id Status 13 Pausing
tenable-io-get-asset-details
Retrieves details for the specified asset including custom attributes.
Base Command
tenable-io-get-asset-details
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | IP Address of the asset. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.AssetDetails.attributes | unknown | Array of custom attributes of asset. |
| TenableIO.AssetDetails.counts | unknown | Array of audit statuses and vulnerabilities by type. |
| TenableIO.AssetDetails.created_at | date | Date asset was created. |
| TenableIO.AssetDetails.first_seen | date | Date asset was first seen. |
| TenableIO.AssetDetails.fqdn | unknown | Array of fully-qualified domain names. |
| TenableIO.AssetDetails.id | string | GUID of tenable.io asset. |
| TenableIO.AssetDetails.interfaces | unknown | Array of interface information. |
| TenableIO.AssetDetails.ipv4 | unknown | Array of IPv4 addresses. |
| TenableIO.AssetDetails.operating_system | unknown | Array of operating systems. |
| TenableIO.AssetDetails.tags | unknown | Array of tags added to asset. |
| TenableIO.AssetDetails.updated_at | date | Date the asset was last updated. |
Command example
!tenable-io-get-asset-details ip=1.3.2.1"
Context Example
{
"TenableIO": {
"AssetDetails": {
"agent_name": [],
"attributes": [],
"aws_availability_zone": [],
"aws_ec2_instance_ami_id": [],
"aws_ec2_instance_group_name": [],
"aws_ec2_instance_id": [],
"aws_ec2_instance_state_name": [],
"aws_ec2_instance_type": [],
"aws_ec2_name": [],
"aws_ec2_product_code": [],
"aws_owner_id": [],
"aws_region": [],
"aws_subnet_id": [],
"aws_vpc_id": [],
"azure_location": [],
"azure_resource_group": [],
"azure_resource_id": [],
"azure_subscription_id": [],
"azure_type": [],
"azure_vm_id": [],
"bigfix_asset_id": [],
"bios_uuid": [],
"counts": {
"audits": {
"statuses": [
{
"count": 0,
"level": 1,
"name": "Passed"
},
{
"count": 0,
"level": 2,
"name": "Warning"
},
{
"count": 0,
"level": 3,
"name": "Failed"
}
],
"total": 0
},
"vulnerabilities": {
"severities": [
{
"count": 17,
"level": 0,
"name": "Info"
},
{
"count": 0,
"level": 1,
"name": "Low"
},
{
"count": 0,
"level": 2,
"name": "Medium"
},
{
"count": 0,
"level": 3,
"name": "High"
},
{
"count": 1,
"level": 4,
"name": "Critical"
}
],
"total": 18
}
},
"created_at": "2024-11-07T11:11:05Z",
"exposure_confidence_value": null,
"first_seen": "2024-11-07T11:11:05Z",
"fqdn": [
"test.com"
],
"gcp_instance_id": [],
"gcp_project_id": [],
"gcp_zone": [],
"has_agent": false,
"hostname": [
"test.com"
],
"id": "fake_asset_id",
"installed_software": [
"cpe:/a:test:0.0.0",
],
"interfaces": [
{
"fqdn": [
"test.com"
],
"ipv4": [
"1.3.2.1"
],
"ipv6": [],
"mac_address": [],
"name": "UNKNOWN"
}
],
"ipv4": [
"1.3.2.1"
],
"ipv6": [],
"last_authenticated_scan_date": null,
"last_licensed_scan_date": "2024-11-07T11:11:05Z",
"last_scan_id": "fake_scan_id",
"last_scan_target": "test.com'",
"last_schedule_id": "fake_schedule_id",
"last_seen": "2024-11-07T11:11:05Z",
"mac_address": [],
"mcafee_epo_agent_guid": [],
"mcafee_epo_guid": [],
"netbios_name": [],
"network_name": "Default",
"operating_system": [
"Linux Kernel 2.6"
],
"qualys_asset_id": [],
"qualys_host_id": [],
"security_protection_level": null,
"security_protections": [],
"servicenow_sysid": [],
"sources": [
{
"first_seen": "2024-11-07T11:11:05.739Z",
"last_seen": "2024-11-07T11:11:05.739Z",
"name": "name"
}
],
"ssh_fingerprint": [],
"system_type": [
"general-purpose"
],
"tags": [
{
"added_at": "2024-11-07T11:11:05Z",
"added_by": "fake_id",
"source": "static",
"tag_key": "some_key",
"tag_uuid": "fake_uuid",
"tag_value": "test.com"
}
],
"tenable_uuid": [],
"time_end": "2024-11-07T11:11:05Z",
"time_start": "2024-11-07T11:11:05Z",
"updated_at": "2024-11-07T11:11:05Z",
"uuid": "fake_asset_id"
}
}
}
Human Readable Output
Asset Info for 1.3.2.1
attributes fqdn interfaces ipv4 id last_seen test.com {‘name’: ‘UNKNOWN’, ‘fqdn’: [‘test.com’], ‘mac_address’: [], ‘ipv4’: [‘1.3.2.1’], ‘ipv6’: []} 1.3.2.1 fake_asset_id 2024-11-07T11:11:05.739Z
tenable-io-export-assets
Retrieves details for the specified asset to include custom attributes.
Limitations
When inserting invalid arguments, an error message could be returned.
Base Command
tenable-io-export-assets
Input
| Argument Name | Description | Required |
|---|---|---|
| chunkSize | Specifies the number of assets per exported chunk. The range is 100-10000. Default is 100. | Optional |
| intervalInSeconds | The number of seconds until the next run. Default is 10. | Optional |
| timeOut | The timeout for the polling in seconds. Default is 600. | Optional |
| createdAt | When specified, the results returned in the list are limited to assets created later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| updatedAt | When specified, the results returned in the list are limited to assets updated later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| terminatedAt | When specified, the results returned in the list are limited to assets terminated later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| isTerminated | When set to true, returns assets which have any value for the terminatedAt attribute. | Optional |
| deletedAt | When specified, the results returned in the list are limited to assets deleted later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| isDeleted | When set to true, returns assets which have any value for the deletedAt attribute. Possible values are: true, false. | Optional |
| isLicensed | Specifies whether the asset is included in the asset count for the Tenable.io instance. If true, returns only licensed assets. If false, returns only unlicensed assets. Possible values are: true, false. | Optional |
| firstScanTime | When specified, the results returned in the list are limited to assets with a first scan time later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| lastAuthenticatedScanTime | When specified, the results returned in the list are limited to assets with a last credentialed scan time later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| lastAssessed | When specified, the results returned in the list are limited to assets with a last assessed time later than the date specified. Date format will be epoch date format or relational expressions like “7 days ago”.’. | Optional |
| serviceNowSysId | If true, returns all assets that have a ServiceNow Sys ID, regardless of value. If false, returns all assets that do not have a ServiceNow Sys ID. Possible values are: true, false. | Optional |
| sources | A comma-separated list of sources. Possible values are: AWS, NESSUS_AGENT, PVS,NESSUS_SCAN, WAS. When specified, the results returned in the list are limited to assets that have the specified source. | Optional |
| hasPluginResults | If true, returns all assets that have a plugin results associated with it. Possible values are: true, false. | Optional |
| tagCategory | When specified, the results returned in the list are limited to assets with the specified tag category. | Optional |
| tagValue | When specified, the results returned in the list are limited to assets with the specified tag value. Can be comma-delimited for multiple items. | Optional |
| exportUuid | The export uuid. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Asset.id | String | The UUID of the asset in Tenable.io. Use this value as the unique key for the asset. |
| TenableIO.Asset.has_agent | Boolean | Specifies whether a Nessus agent scan identified the asset. |
| TenableIO.Asset.has_plugin_results | Boolean | Specifies whether the asset has plugin results associated with it. |
| TenableIO.Asset.created_at | Date | The time and date when Tenable.io created the asset record. |
| TenableIO.Asset.terminated_at | Date | The time and date when a user terminated the Amazon Web Service (AWS) virtual machine instance of the asset. |
| TenableIO.Asset.terminated_by | String | The user who terminated the AWS instance of the asset. |
| TenableIO.Asset.updated_at | Date | The time and date when the asset record was last updated. |
| TenableIO.Asset.deleted_at | Date | The time and date when a user deleted the asset record. When a user deletes an asset record, Tenable.io retains the record until the asset ages out of the license count. |
| TenableIO.Asset.deleted_by | String | The user who deleted the asset record. |
| TenableIO.Asset.first_seen | Date | The time and date when a scan first identified the asset. |
| TenableIO.Asset.last_seen | Date | The time and date of the scan that most recently identified the asset. |
| TenableIO.Asset.first_scan_time | Date | The time and date of the first scan run against the asset. |
| TenableIO.Asset.last_scan_time | Date | The time and date of the last scan run against the asset. |
| TenableIO.Asset.last_authenticated_scan_date | Date | The time and date of the last credentialed scan run on the asset. |
| TenableIO.Asset.last_licensed_scan_date | Date | The time and date of the last scan that identified the asset as licensed. Tenable.io categorizes an asset as licensed if a scan of that asset has returned results from a non-discovery plugin within the last 90 days. |
| TenableIO.Asset.last_scan_id | String | The UUID of the scan configuration used during the last scan of the asset. |
| TenableIO.Asset.last_schedule_id | String | The schedule id for the last scan of the asset. |
| TenableIO.Asset.azure_vm_id | String | The unique identifier of the Microsoft Azure virtual machine instance. For more information, see “Accessing and Using Azure VM Unique ID” in the Microsoft Azure documentation. |
| TenableIO.Asset.azure_resource_id | String | The unique identifier of the resource in the Azure Resource Manager. For more information, see the Azure Resource Manager Documentation. |
| TenableIO.Asset.gcp_project_id | String | The unique identifier of the virtual machine instance in Google Cloud Platform (GCP). |
| TenableIO.Asset.gcp_zone | String | The customized name of the project to which the virtual machine instance belongs in GCP. For more information see “Creating and Managing Projects” in the GCP documentation. |
| TenableIO.Asset.gcp_instance_id | String | The zone where the virtual machine instance runs in GCP. For more information, see “Regions and Zones” in the GCP documentation. |
| TenableIO.Asset.aws_ec2_instance_ami_id | String | The unique identifier of the Linux AMI image in Amazon Elastic Compute Cloud (Amazon EC2). For more information, see the Amazon Elastic Compute Cloud Documentation. |
| TenableIO.Asset.aws_ec2_instance_id | String | The unique identifier of the Linux instance in Amazon EC2. For more information, see the Amazon Elastic Compute Cloud Documentation. |
| TenableIO.Asset.agent_uuid | String | The unique identifier of the Nessus agent that identified the asset. |
| TenableIO.Asset.bios_uuid | String | The BIOS UUID of the asset. |
| TenableIO.Asset.network_id | String | The ID of the network object associated with scanners that identified the asset. |
| TenableIO.Asset.network_name | String | The ID of the network object associated with scanners that identified the asset. |
| TenableIO.Asset.aws_owner_id | String | The canonical user identifier for the AWS account associated with the virtual machine instance. |
| TenableIO.Asset.aws_availability_zone | String | The availability zone where Amazon Web Services hosts the virtual machine instance. |
| TenableIO.Asset.aws_region | String | The region where AWS hosts the virtual machine instance. |
| TenableIO.Asset.aws_vpc_id | String | The unique identifier for the virtual public cloud that hosts the AWS virtual machine instance. |
| TenableIO.Asset.aws_ec2_instance_group_name | String | The virtual machine instance’s group in AWS. |
| TenableIO.Asset.aws_ec2_instance_state_name | String | The state of the virtual machine instance in AWS at the time of the scan. |
| TenableIO.Asset.aws_ec2_instance_type | String | The type of instance in AWS EC2. |
| TenableIO.Asset.aws_subnet_id | String | The unique identifier of the AWS subnet where the virtual machine instance was running at the time of the scan. |
| TenableIO.Asset.aws_ec2_product_code | String | The product code associated with the AMI used to launch the virtual machine instance in AWS EC2. |
| TenableIO.Asset.aws_ec2_name | String | The name of the virtual machine instance in AWS EC2. |
| TenableIO.Asset.mcafee_epo_guid | String | The unique identifier of the asset in McAfee ePolicy Orchestrator (ePO). |
| TenableIO.Asset.mcafee_epo_agent_guid | String | The unique identifier of the McAfee ePO agent that identified the asset. |
| TenableIO.Asset.servicenow_sysid | String | The unique record identifier of the asset in ServiceNow. |
| TenableIO.Asset.bigfix_asset_id | String | The unique identifiers of the asset in HCL BigFix. |
| TenableIO.Asset.agent_names | String | The names of any Nessus agents that scanned and identified the asset. |
| TenableIO.Asset.installed_software | String | A list of Common Platform Enumeration (CPE) values that represent software applications a scan identified as present on an asset. |
| TenableIO.Asset.ipv4s | String | The IPv4 addresses that scans have associated with the asset record. |
| TenableIO.Asset.ipv6s | String | The IPv6 addresses that scans have associated with the asset record. |
| TenableIO.Asset.fqdns | String | The fully-qualified domain names that scans have associated with the asset record. |
| TenableIO.Asset.mac_addresses | String | The MAC addresses that scans have associated with the asset record. |
| TenableIO.Asset.netbios_names | String | The NetBIOS names that scans have associated with the asset record. |
| TenableIO.Asset.operating_systems | String | The operating systems that scans have associated with the asset record. |
| TenableIO.Asset.system_types | String | The system types as reported by Plugin ID 54615. Possible values include router, general-purpose, scan-host, and embedded. |
| TenableIO.Asset.hostnames | String | The hostnames that scans have associated with the asset record. |
| TenableIO.Asset.ssh_fingerprints | String | The SSH key fingerprints that scans have associated with the asset record. |
| TenableIO.Asset.qualys_asset_ids | String | The Asset ID of the asset in Qualys. For more information, see the Qualys documentation. |
| TenableIO.Asset.qualys_host_ids | String | The Host ID of the asset in Qualys. For more information, see the Qualys documentation. |
| TenableIO.Asset.manufacturer_tpm_ids | String | The manufacturer’s unique identifiers of the Trusted Platform Module (TPM) associated with the asset. |
| TenableIO.Asset.symantec_ep_hardware_keys | String | The hardware keys for the asset in Symantec Endpoint Protection. |
| TenableIO.Asset.sources.name | String | The name of the entity that reported the asset details. Sources can include sensors, connectors, and API imports. |
| TenableIO.Asset.sources.first_seen | Date | The ISO timestamp when the source first reported the asset. |
| TenableIO.Asset.sources.last_seen | Date | The ISO timestamp when the source last reported the asset. |
| TenableIO.Asset.tags.uuid | String | The UUID of the tag. |
| TenableIO.Asset.tags.key | String | The tag category (the first half of the category:value pair). |
| TenableIO.Asset.tags.value | String | The tag value (the second half of the category:value pair). |
| TenableIO.Asset.tags.added_by | String | The UUID of the user who assigned the tag to the asset. |
| TenableIO.Asset.tags.added_at | Date | The ISO timestamp when the tag was assigned to the asset. |
| TenableIO.Asset.network_interfaces.name | String | The name of the interface. |
| TenableIO.Asset.network_interfaces.mac_address | String | The MAC addresses of the interface. |
| TenableIO.Asset.network_interfaces.ipv6 | String | One or more IPv6 addresses belonging to the interface. |
| TenableIO.Asset.network_interfaces.fqdns | String | One or more FQDNs belonging to the interface. |
| TenableIO.Asset.network_interfaces.ipv4s | String | One or more IPv4 addresses belonging to the interface. |
| TenableIO.Asset.acr_score | String | The Asset Criticality Rating (ACR) for the asset. |
| TenableIO.Asset.exposure_score | String | The Asset Exposure Score (AES) for the asset. |
Command example
!tenable-io-export-assets chunkSize=500
Context Example
{
"TenableIO": {
"Asset": [
{
"created_at": "2024-11-07T11:11:05Z",
"first_scan_time": "2024-11-07T11:11:05Z",
"first_seen": "2024-11-07T11:11:05Z",
"fqdns": [
"test.com"
],
"has_agent": false,
"has_plugin_results": true,
"hostnames": [
"test.com"
],
"id": "fake_uuid",
"installed_software": [],
"ipv4s": [
"1.3.2.1"
],
"last_licensed_scan_date": "2024-11-07T11:11:05Z",
"last_scan_id": "fake_uuid",
"last_scan_time": "2024-11-07T11:11:05Z",
"last_schedule_id": "fake_uuid",
"last_seen": "2024-11-07T11:11:05Z",
"network_id": "00000000-0000-0000-0000-000000000000",
"network_interfaces": [
{
"aliased": null,
"fqdns": [
"test.com"
],
"ipv4s": [
"1.3.2.1"
],
"ipv6s": [],
"mac_addresses": [],
"name": "UNKNOWN",
"virtual": null
}
],
"network_name": "Default",
"operating_systems": [
"Linux Kernel 2.6"
],
"sources": [
{
"first_seen": "2024-11-07T11:11:05Z",
"last_seen": "2024-11-07T11:11:05Z",
"name": "NESSUS_SCAN"
}
],
"system_types": [
"general-purpose"
],
"tags": [
{
"added_at": "2024-11-07T11:11:05Z",
"added_by": "fake_uuid",
"key": "some_key",
"uuid": "fake_uuid",
"value": "test.com"
}
],
"updated_at": "2024-11-07T11:11:05Z"
},
{
"created_at": "2024-11-07T11:11:05Z",
"first_scan_time": "2024-11-07T11:11:05Z",
"first_seen": "2024-11-07T11:11:05Z",
"fqdns": [
"test.net"
],
"has_agent": false,
"has_plugin_results": true,
"hostnames": [
"test.net"
],
"id": "fake_uuid",
"installed_software": [],
"ipv4s": [
"1.3.2.1"
],
"last_licensed_scan_date": "2024-11-07T11:11:05Z",
"last_scan_id": "fake_uuid",
"last_scan_time": "2024-11-07T11:11:05Z",
"last_schedule_id": "fake_uuid",
"last_seen": "2024-11-07T11:11:05Z",
"network_id": "00000000-0000-0000-0000-000000000000",
"network_interfaces": [
{
"aliased": null,
"fqdns": [
"test.net"
],
"ipv4s": [
"1.3.2.1"
],
"ipv6s": [],
"mac_addresses": [],
"name": "UNKNOWN",
"virtual": null
}
],
"network_name": "Default",
"operating_systems": [
"Linux Kernel 2.6"
],
"sources": [
{
"first_seen": "2024-11-07T11:11:05Z",
"last_seen": "2024-11-07T11:11:05Z",
"name": "NESSUS_SCAN"
}
],
"ssh_fingerprints": [
"fake_ssh_fingerprints"
],
"system_types": [
"general-purpose"
],
"tags": [
{
"added_at": "2024-11-07T11:11:05Z",
"added_by": "fake_uuid",
"key": "some_key",
"uuid": "fake_uuid",
"value": "test.com"
}
],
"updated_at": "2024-11-07T11:11:05Z"
},
]
}
}
Human Readable Output
Export Assets Results
ASSET ID DNS NAME (FQDN) SYSTEM TYPE OPERATING SYSTEM IPV4 ADDRESS NETWORK FIRST SEEN LAST SEEN LAST LICENSED SCAN SOURCE TAGS fake_uuid test.com general-purpose Linux Kernel 2.6 1.3.2.1 Default 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z NESSUS_SCAN some_key:test.com fake_uuid test.net general-purpose Linux Kernel 2.6 1.3.2.1 Default 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z 2024-11-07T11:11:05Z NESSUS_SCAN some_key:test.com
tenable-io-export-vulnerabilities
Retrieves details for the specified asset to include custom attributes.
Limitations
When inserting invalid arguments, an error message could be returned.
Base Command
tenable-io-export-vulnerabilities
Input
| Argument Name | Description | Required |
|---|---|---|
| numAssets | The number of assets used to chunk the vulnerabilities. The range for number of assets in a chunk is 50-5000. Default is 50. | Optional |
| intervalInSeconds | The number of seconds until the next run. Default is 10. | Optional |
| timeOut | The timeout for the polling in seconds. Default is 600. | Optional |
| includeUnlicensed | Specifies whether or not to include unlicensed assets. Possible values are: true, false. | Optional |
| cidrRange | When specified, restricts the search for vulnerabilities to assets assigned an IP address within the specified CIDR range. | Optional |
| firstFound | When specified, the results returned in the list are limited to vulnerabilities that were first found between the specified date and now. Date format will be epoch date format or relational expressions like “7 days ago”. | Optional |
| lastFixed | When specified, the results returned in the list are limited to vulnerabilities that were fixed between the specified date and now. Date format will be epoch date format or relational expressions like “7 days ago”. | Optional |
| lastFound | When specified, the results returned in the list are limited to vulnerabilities that were last found between the specified date and now. Date format will be epoch date format or relational expressions like “7 days ago”. | Optional |
| networkId | The ID of the network object associated with scanners that detected the vulnerabilities you want to export. | Optional |
| pluginId | A comma-separated list of plugin IDs for which you want to filter the vulnerabilities. | Optional |
| pluginType | The plugin type for which you want to filter the vulnerabilities. If not set, export includes all vulnerabilities regardless of plugin type. Possible values are: remote, local, combined, settings, summary, third-party, reputation. | Optional |
| severity | The severity of the vulnerabilities to include in the export. Defaults to all severity levels. The severity of a vulnerability is defined using the Common Vulnerability Scoring System (CVSS) base score. Supported array values are: info, low, medium, high, critical. | Optional |
| since | The start date for the range of data you want to export. Date format will be epoch date format or relational expressions like “7 days ago”. Note: This filter cannot be used in conjunction with the firstFound, lastFound, or lastFixed. | Optional |
| state | A comma-separated list of states of the vulnerabilities you want the export to include. Supported, case-insensitive values are: open, reopened, fixed. This parameter is required if your request includes firstFound, lastFound, or lastFixed parameters. If your request omits this parameter, the export includes default states open and reopened only. | Optional |
| tagCategory | When specified, the results returned in the list are limited to assets with the specified tag category. | Optional |
| tagValue | When specified, the results returned in the list are limited to assets with the specified tag value. Can be comma-delimited for multiple items. | Optional |
| vprScoreOperator | An operator that determines the limitation on Vulnerability Priority Rating (VPR), scores value specified at vprScoreValue argument. Supported values are: equal, not equal, lt-lesser, lte-lesser than or equal , gt-greater than , gte-greater than or equal. Possible values are: gte, gt, lte, lt, equal, not equal. | Optional |
| vprScoreValue | When specified, the results returned in the list are limited to vulnerabilities with the specified Vulnerability Priority Rating (VPR), score or scores according to the score operator (vprScoreOperator) argument. | Optional |
| vprScoreRange | When specified, the results returned in the list are limited to vulnerabilities with the specified Vulnerability Priority Rating (VPR) score range. Example value: 2.5-3.5. | Optional |
| exportUuid | The export UUID. | Optional |
| should_push_events | Set this argument to True in order to create vulnerabilities, otherwise the command will only display the vulnerabilities. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.Vulnerability.asset.agent_uuid | String | The UUID of the agent that performed the scan where the vulnerability was found. |
| TenableIO.Vulnerability.asset.bios_uuid | String | The BIOS UUID of the asset where the vulnerability was found. |
| TenableIO.Vulnerability.asset.device_type | String | The type of asset where the vulnerability was found. |
| TenableIO.Vulnerability.asset.fqdn | String | The fully-qualified domain name of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.hostname | String | The host name of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.uuid | String | The UUID of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.ipv6 | String | The IPv6 address of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.last_authenticated_results | Date | The last date credentials that were used successfully to scan the asset. |
| TenableIO.Vulnerability.asset.last_unauthenticated_results | Date | The last date when the asset was scanned without using credentials |
| TenableIO.Vulnerability.asset.mac_address | String | The MAC address of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.netbios_name | String | The NETBIOS name of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.netbios_workgroup | String | The NETBIOS workgroup of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.operating_system | String | The operating system of the asset where a scan found the vulnerability. |
| TenableIO.Vulnerability.asset.network_id | String | The ID of the network object associated with scanners that identified the asset. |
| TenableIO.Vulnerability.asset.tracked | Boolean | A value specifying whether Tenable.io tracks the asset in the asset management system. |
| TenableIO.Vulnerability.output | String | The text output of the Nessus scanner. |
| TenableIO.Vulnerability.plugin.bid | Number | The Bugtraq ID for the plugin. |
| TenableIO.Vulnerability.plugin.canvas_package | String | The name of the CANVAS exploit pack that includes the vulnerability. |
| TenableIO.Vulnerability.plugin.checks_for_default_account | Boolean | A value specifying whether the plugin checks for default accounts. |
| TenableIO.Vulnerability.plugin.checks_for_malware | Boolean | A value specifying whether the plugin checks for malware. |
| TenableIO.Vulnerability.plugin.cpe | String | The Common Platform Enumeration (CPE) number for the plugin. |
| TenableIO.Vulnerability.plugin.cve | String | The Common Vulnerability and Exposure (CVE) ID for the plugin. |
| TenableIO.Vulnerability.plugin.cvss3_base_score | Number | The CVSSv3 base score (intrinsic and fundamental characteristics of a vulnerability that are constant over time and user environments). |
| TenableIO.Vulnerability.plugin.cvss3_temporal_score | Number | The CVSSv3 temporal score (characteristics of a vulnerability that change over time but not among user environments). |
| TenableIO.Vulnerability.plugin.cvss3_temporal_vector.exploitability | String | The CVSSv3 Exploit Maturity Code (E) for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_temporal_vector.remediation_level | String | The CVSSv3 Remediation Level (RL) temporal metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_temporal_vector.report_confidence | String | The CVSSv3 Report Confidence (RC) temporal metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_temporal_vector.raw | String | The complete CVSSv3 temporal vector metrics and result values for the vulnerability the plugin covers in a condensed and coded format. |
| TenableIO.Vulnerability.plugin.cvss3_vector.access_vector | String | The CVSSv3 Attack Vector (AV) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.access_complexity | String | The CVSSv3 Access Complexity (AC) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.authentication | String | The CVSSv3 Authentication (Au) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.confidentiality_impact | String | The CVSSv3 integrity impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.integrity_impact | String | The CVSSv3 integrity impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.availability_impact | String | The CVSSv3 availability impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss3_vector.raw | String | The complete cvss3_vector metrics and result values for the vulnerability the plugin covers in a condensed and coded format. |
| TenableIO.Vulnerability.plugin.cvss_temporal_vector.exploitability | String | The CVSSv2 Exploitability (E) temporal metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_temporal_vector.remediation_level | String | The CVSSv2 Remediation Level (RL) temporal metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_temporal_vector.report_confidence | String | The CVSSv2 Report Confidence (RC) temporal metric for the vulnerability the plugin covers |
| TenableIO.Vulnerability.plugin.cvss_temporal_vector.raw | String | The complete CVSS temporal vector metrics and result values for the vulnerability the plugin covers in a condensed and coded format. |
| TenableIO.Vulnerability.plugin.cvss_vector.access_vector | String | The CVSSv2 Access Vector (AV) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.access_complexity | String | The CVSSv2 Access Complexity (AC) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.authentication | String | The CVSSv2 Authentication (Au) metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.confidentiality_impact | String | The CVSSv2 confidentiality impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.integrity_impact | String | The CVSSv2 integrity impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.availability_impact | String | The CVSSv2 availability impact metric for the vulnerability the plugin covers. |
| TenableIO.Vulnerability.plugin.cvss_vector.raw | String | The complete CVSSv2 vector metrics and result values for the vulnerability the plugin covers in a condensed and coded format. |
| TenableIO.Vulnerability.plugin.cvss_base_score | Number | The CVSSv2 base score (intrinsic and fundamental characteristics of a vulnerability that are constant over time and user environments). |
| TenableIO.Vulnerability.plugin.cvss_temporal_score | Number | The CVSSv2 temporal score (characteristics of a vulnerability that change over time but not among user environments). |
| TenableIO.Vulnerability.plugin.d2_elliot_name | String | The name of the exploit in the D2 Elliot Web Exploitation framework. |
| TenableIO.Vulnerability.plugin.description | String | Full text description of the vulnerability. |
| TenableIO.Vulnerability.plugin.exploit_available | Boolean | A value specifying whether a public exploit exists for the vulnerability. |
| TenableIO.Vulnerability.plugin.exploit_framework_canvas | Boolean | A value specifying whether an exploit exists in the Immunity CANVAS framework. |
| TenableIO.Vulnerability.plugin.exploit_framework_core | Boolean | A value specifying whether an exploit exists in the CORE Impact framework. |
| TenableIO.Vulnerability.plugin.exploit_framework_d2_elliot | Boolean | A value specifying whether an exploit exists in the D2 Elliot Web Exploitation framework. |
| TenableIO.Vulnerability.plugin.exploit_framework_exploithub | Boolean | A value specifying whether an exploit exists in the ExploitHub framework. |
| TenableIO.Vulnerability.plugin.exploit_framework_metasploit | Boolean | A value specifying whether an exploit exists in the Metasploit framework. |
| TenableIO.Vulnerability.plugin.exploitability_ease | String | Description of how easy it is to exploit the issue. |
| TenableIO.Vulnerability.plugin.exploited_by_malware | Boolean | Whether the vulnerability discovered by this plugin is known to be exploited by malware. |
| TenableIO.Vulnerability.plugin.exploited_by_nessus | Boolean | A value specifying whether Nessus exploited the vulnerability during the process of identification. |
| TenableIO.Vulnerability.plugin.exploithub_sku | String | The SKU number of the exploit in the ExploitHub framework. |
| TenableIO.Vulnerability.plugin.family | String | The family to which the plugin belongs. |
| TenableIO.Vulnerability.plugin.family_id | Number | The ID of the plugin family. |
| TenableIO.Vulnerability.plugin.has_patch | Boolean | A value specifying whether the vendor has published a patch for the vulnerability. |
| TenableIO.Vulnerability.plugin.id | Number | The ID of the plugin that identified the vulnerability. |
| TenableIO.Vulnerability.plugin.in_the_news | Boolean | A value specifying whether this plugin has received media attention (for example, ShellShock, Meltdown). |
| TenableIO.Vulnerability.plugin.metasploit_name | String | The name of the related exploit in the Metasploit framework. |
| TenableIO.Vulnerability.plugin.ms_bulletin | String | The Microsoft security bulletin that the plugin covers. |
| TenableIO.Vulnerability.plugin.name | String | The name of the plugin that identified the vulnerability. |
| TenableIO.Vulnerability.plugin.patch_publication_date | String | The date on which the vendor published a patch for the vulnerability. |
| TenableIO.Vulnerability.plugin.modification_date | Date | The date on which the plugin was last modified. |
| TenableIO.Vulnerability.plugin.publication_date | Date | The date on which the plugin was published. |
| TenableIO.Vulnerability.plugin.risk_factor | String | The risk factor associated with the plugin. Possible values are: Low, Medium, High, or Critical. |
| TenableIO.Vulnerability.plugin.see_also | String | Links to external websites that contain helpful information about the vulnerability. |
| TenableIO.Vulnerability.plugin.solution | String | Remediation information for the vulnerability. |
| TenableIO.Vulnerability.plugin.stig_severity | String | Security Technical Implementation Guide (STIG) severity code for the vulnerability. |
| TenableIO.Vulnerability.plugin.synopsis | String | Brief description of the plugin or vulnerability. |
| TenableIO.Vulnerability.plugin.type | String | The general type of plugin check (for example, local or remote). |
| TenableIO.Vulnerability.plugin.unsupported_by_vendor | Boolean | Whether software found by this plugin is unsupported by the software’s vendor (for example, Windows 95 or Firefox 3). |
| TenableIO.Vulnerability.plugin.usn | String | Ubuntu security notice that the plugin covers. |
| TenableIO.Vulnerability.plugin.version | String | The version of the plugin used to perform the check. |
| TenableIO.Vulnerability.plugin.vuln_publication_date | Date | The publication date of the plugin. |
| TenableIO.Vulnerability.plugin.xrefs.type | String | References to third-party information about the vulnerability, exploit, or update associated with the plugin. |
| TenableIO.Vulnerability.plugin.xrefs.id | String | References to third-party information about the vulnerability, exploit, or update associated with the plugin. |
| TenableIO.Vulnerability.plugin.vpr.score | Number | The Vulnerability Priority Rating (VPR) for the vulnerability. |
| TenableIO.Vulnerability.plugin.vpr.drivers.age_of_vuln | Number | A range representing the number of days since the National Vulnerability Database (NVD) published the vulnerability. |
| TenableIO.Vulnerability.plugin.vpr.drivers.age_of_vuln.lower_bound | Number | The lower bound of the range. |
| TenableIO.Vulnerability.plugin.vpr.drivers.age_of_vuln.upper_bound | Number | The upper bound of the range. |
| TenableIO.Vulnerability.plugin.vpr.drivers.exploit_code_maturity | String | The relative maturity of a possible exploit for the vulnerability based on the existence, sophistication, and prevalence of exploit intelligence from internal and external sources. |
| TenableIO.Vulnerability.plugin.vpr.drivers.cvss3_impact_score | Number | The NVD-provided CVSSv3 impact score for the vulnerability. |
| TenableIO.Vulnerability.plugin.vpr.drivers.cvss_impact_score_predicted | Boolean | A value specifying whether Tenable.io predicted the CVSSv3 impact score for the vulnerability. |
| TenableIO.Vulnerability.plugin.vpr.drivers.threat_intensity_last28 | String | The relative intensity based on the number and frequency of recently observed threat events related to this vulnerability: Very Low, Low, Medium, High, or Very High. |
| TenableIO.Vulnerability.plugin.vpr.drivers.threat_recency | String | A range representing the number of days since a threat event occurred for the vulnerability. |
| TenableIO.Vulnerability.plugin.vpr.drivers.threat_recency.lower_bound | String | The lower bound of the range. |
| TenableIO.Vulnerability.plugin.vpr.drivers.threat_recency.upper_bound | String | The upper bound of the range. |
| TenableIO.Vulnerability.plugin.vpr.drivers.threat_sources_last28 | String | A list of all sources (for example, social media channels, the dark web, etc.) where threat events related to this vulnerability occurred. |
| TenableIO.Vulnerability.plugin.vpr.drivers.product_coverage | String | The relative number of unique products affected by the vulnerability: ‘Low’, ‘Medium’, ‘High’, or ‘Very High’. |
| TenableIO.Vulnerability.plugin.vpr.updated | Date | The ISO timestamp when Tenable.io last imported the VPR for this vulnerability. |
| TenableIO.Vulnerability.port.port | Number | The port the scanner used to communicate with the asset. |
| TenableIO.Vulnerability.port.protocol | String | The protocol the scanner used to communicate with the asset. |
| TenableIO.Vulnerability.port.service | String | The service the scanner used to communicate with the asset. |
| TenableIO.Vulnerability.recast_reason | String | The text that appears in the Comment field of the recast rule in the Tenable.io user interface. |
| TenableIO.Vulnerability.recast_rule_uuid | String | The UUID of the recast rule that applies to the plugin. |
| TenableIO.Vulnerability.scan.completed_at | Date | The ISO timestamp when the scan completed. |
| TenableIO.Vulnerability.scan.schedule_uuid | String | The schedule UUID for the scan that found the vulnerability. |
| TenableIO.Vulnerability.scan.started_at | Date | The ISO timestamp when the scan started. |
| TenableIO.Vulnerability.scan.uuid | String | The UUID of the scan that found the vulnerability. |
| TenableIO.Vulnerability.severity | String | The severity of the vulnerability as defined using the Common Vulnerability Scoring System (CVSS) base score. |
| TenableIO.Vulnerability.severity_id | Number | The code for the severity assigned when a user recast the risk associated with the vulnerability. |
| TenableIO.Vulnerability.severity_default_id | Number | The code for the severity originally assigned to a vulnerability before a user recast the risk associated with the vulnerability. |
| TenableIO.Vulnerability.severity_modification_type | String | The type of modification a user made to the vulnerability’s severity. |
| TenableIO.Vulnerability.first_found | Date | The ISO date when a scan first detected the vulnerability on the asset. |
| TenableIO.Vulnerability.last_fixed | Date | The ISO date when a scan no longer detects the previously detected vulnerability on the asset. |
| TenableIO.Vulnerability.last_found | Date | The ISO date when a scan last detected the vulnerability on the asset. |
| TenableIO.Vulnerability.state | String | The state of the vulnerability as determined by the Tenable.io state service. |
| TenableIO.Vulnerability.indexed | Date | The date and time (in Unix time) when the vulnerability was indexed into Tenable.io. |
Command example
!tenable-io-export-vulnerabilities numAssets=500
Context Example
{
"TenableIO": {
"Vulnerability": [
{
"asset": {
"device_type": "general-purpose",
"fqdn": "fqdn",
"hostname": "1.1.1.1",
"ipv4": "1.1.1.1",
"last_unauthenticated_results": "2024-11-07T11:11:05.906Z",
"network_id": "00000000-0000-0000-0000-000000000000",
"operating_system": [
"Linux Kernel 3.13 on Ubuntu 14.04 (trusty)"
],
"tracked": true,
"uuid": "fake_uuid"
},
"first_found": "2024-11-07T11:11:05.906Z",
"indexed": "2024-11-07T11:11:05.906Z",
"last_fixed": "2024-11-07T11:11:05.906Z",
"last_found": "2024-11-07T11:11:05.906Z",
"output": "outputs",
"plugin": {
"checks_for_default_account": false,
"checks_for_malware": false,
"cvss3_base_score": 0,
"cvss3_temporal_score": 0,
"cvss_base_score": 0,
"cvss_temporal_score": 0,
"description": "Description",
"exploit_available": false,
"exploit_framework_canvas": false,
"exploit_framework_core": false,
"exploit_framework_d2_elliot": false,
"exploit_framework_exploithub": false,
"exploit_framework_metasploit": false,
"exploited_by_malware": false,
"exploited_by_nessus": false,
"family": "General",
"family_id": 30,
"has_patch": false,
"id": 00000,
"in_the_news": false,
"modification_date": "2024-11-07T11:11:05Z",
"name": "Name",
"publication_date": "2024-11-07T11:11:05Z",
"risk_factor": "None",
"see_also": [
""
],
"solution": "N/A",
"synopsis": "synopsis",
"type": "remote",
"unsupported_by_vendor": false,
"version": "$Revision: 1.16 $"
},
"port": {
"port": 0,
"protocol": "TCP"
},
"scan": {
"completed_at": "2024-11-07T11:11:05.906Z",
"schedule_uuid": "fake_uuid",
"started_at": "2024-11-07T11:11:05.906Z",
"uuid": "fake_uuid"
},
"severity": "info",
"severity_default_id": 0,
"severity_id": 0,
"severity_modification_type": "NONE",
"state": "OPEN"
},
{
"asset": {
"device_type": "general-purpose",
"fqdn": "fqdn",
"hostname": "1.3.2.1",
"ipv4": "1.3.2.1",
"last_unauthenticated_results": "2024-11-07T11:11:05Z",
"network_id": "00000000-0000-0000-0000-000000000000",
"operating_system": [
"Nutanix"
],
"tracked": true,
"uuid": "fake_uuid"
},
"first_found": "2024-11-07T11:11:05.906Z",
"indexed": "2024-11-07T11:11:05.906Z",
"last_fixed": "2024-11-07T11:11:05.906Z",
"last_found": "2024-11-07T11:11:05.906Z",
"output": "outputs",
"plugin": {
"checks_for_default_account": false,
"checks_for_malware": false,
"cvss3_base_score": 0,
"cvss3_temporal_score": 0,
"cvss_base_score": 0,
"cvss_temporal_score": 0,
"description": "Description",
"exploit_available": false,
"exploit_framework_canvas": false,
"exploit_framework_core": false,
"exploit_framework_d2_elliot": false,
"exploit_framework_exploithub": false,
"exploit_framework_metasploit": false,
"exploited_by_malware": false,
"exploited_by_nessus": false,
"family": "SMTP problems",
"family_id": 12,
"has_patch": false,
"id": 00000,
"in_the_news": false,
"modification_date": "2024-11-07T11:11:05Z",
"name": "Name",
"publication_date": "2024-11-07T11:11:05Z",
"risk_factor": "None",
"see_also": [],
"solution": "N/A",
"synopsis": "synopsis.",
"type": "remote",
"unsupported_by_vendor": false,
"version": "1.12"
},
"port": {
"port": 25,
"protocol": "TCP",
"service": "smtp"
},
"scan": {
"completed_at": "2024-11-07T11:11:05.906Z",
"schedule_uuid": "fake_uuid",
"started_at": "2024-11-07T11:11:05.906Z",
"uuid": "fake_uuid"
},
"severity": "info",
"severity_default_id": 0,
"severity_id": 0,
"severity_modification_type": "NONE",
"state": "OPEN"
},
]
}
}
Human Readable Output
Export Vulnerabilities Results
ASSET ID ASSET NAME IPV4 ADDRESS OPERATING SYSTEM SYSTEM TYPE DNS NAME (FQDN) SEVERITY PLUGIN ID PLUGIN NAME VULNERABILITY PRIORITY RATING CVSSV2 BASE SCORECVE PROTOCOL PORT FIRST SEEN LAST SEEN DESCRIPTION SOLUTION fake_uuid 1.1.1.1 1.1.1.1 Linux Kernel 3.13 on Ubuntu 14.04 (trusty) general-purpose fqdn info 00000 Name TCP 22 2024-11-07T11:11:05.906Z 2024-11-07T11:11:05.906Z Description N/A fake_uuid 1.3.2.1 1.3.2.1 Nutanix general-purpose fqdn info 00000 Name TCP 0 2024-11-07T11:11:05.906Z 2024-11-07T11:11:05.906Z Description N/A tenable-io-list-scan-filters
Lists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them.
Base Command
tenable-io-list-scan-filters
Input
There are no inputs for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.ScanFilter.name | String | The name of the scan filter. |
| TenableIO.ScanFilter.readable_name | String | The readable name of the scan filter. |
| TenableIO.ScanFilter.control.type | String | The type of control associated with the scan filter. |
| TenableIO.ScanFilter.control.regex | String | The regular expression used by the scan filter. |
| TenableIO.ScanFilter.control.readable_regex | String | An example expression that the filter’s regular expression would match. |
| TenableIO.ScanFilter.operators | String | The operators available for the scan filter. |
| TenableIO.ScanFilter.group_name | String | The group name associated with the scan filter. |
Command example
#### Context Example
```json
{
"TenableIO": {
"ScanFilter": [
{
"control": {
"readable_regex": "01234567-abcd-ef01-2345-6789abcdef01",
"regex": "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})*",
"type": "entry"
},
"group_name": null,
"name": "host.id",
"operators": [
"eq",
"neq",
"match",
"nmatch"
],
"readable_name": "Asset ID"
},
{
"control": {
"maxlength": 18,
"readable_regex": "NUMBER",
"regex": "^[0-9]+(,[0-9]+)*",
"type": "entry"
},
"group_name": null,
"name": "plugin.attributes.bid",
"operators": [
"eq",
"neq",
"match",
"nmatch"
],
"readable_name": "Bugtraq ID"
}
]
}
}
Human Readable Output
Tenable IO Scan Filters
Filter name Filter Readable name Filter Control type Filter regex Readable regex Filter operators host.id Asset ID entry [0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})* 01234567-abcd-ef01-2345-6789abcdef01 eq,
neq,
match,
nmatchplugin.attributes.bid Bugtraq ID entry ^[0-9]+(,[0-9]+)* NUMBER eq,
neq,
match,
nmatch
tenable-io-get-scan-history
Lists the individual runs of the specified scan.
Base Command
tenable-io-get-scan-history
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | The ID of the scan of which to get the runs. | Required |
| sortFields | A comma-separated list of fields by which to sort, in the order defined by “sortOrder”. Possible values are: start_date, end_date, status. | Optional |
| sortOrder | A comma-separated list of directions in which to sort the fields defined by “sortFields”. If multiple directions are chosen, they will be sequentially matched with “sortFields”. If only one direction is chosen it will be used to sort all values in “sortFields”. For example: If sortFields is “start_date,status” and sortOrder is “asc,desc”, then start_date is sorted in ascending order and status in descending order. If sortFields is “start_date,status” and sortOrder is simply “asc”, then both start_date and status are sorted in ascending order. . Possible values are: asc, desc. Default is asc. |
Optional |
| excludeRollover | Whether to exclude rollover scans from the scan history. Possible values are: true, false. Default is false. | Optional |
| page | The page number of scan records to retrieve (used for pagination) starting from 1. The page size is defined by the “pageSize” argument. | Optional |
| pageSize | The number of scan records per page to retrieve (used for pagination). The page number is defined by the “page” argument. | Optional |
| limit | The maximum number of records to retrieve. If “pageSize” is defined, this argument is ignored. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| TenableIO.ScanHistory.time_end | Number | The end time of the scan. |
| TenableIO.ScanHistory.scan_uuid | String | The UUID (Universally Unique Identifier) of the scan. |
| TenableIO.ScanHistory.id | Number | The ID of the scan history. |
| TenableIO.ScanHistory.is_archived | Boolean | Indicates whether the scan is archived or not. |
| TenableIO.ScanHistory.time_start | Number | The start time of the scan. |
| TenableIO.ScanHistory.visibility | String | The visibility of the scan. |
| TenableIO.ScanHistory.targets.custom | Boolean | Indicates whether custom targets were used in the scan. |
| TenableIO.ScanHistory.targets.default | Boolean | Indicates whether the default targets were used in the scan. |
| TenableIO.ScanHistory.status | String | The status of the scan. |
Command example
!tenable-io-get-scan-history scanId=16 excludeRollover=true sortFields=end_date,status sortOrder=desc page=2 pageSize=4
Context Example
{
"TenableIO": {
"ScanHistory": [
{
"id": 17235445,
"is_archived": true,
"reindexing": null,
"scan_uuid": "69a55b8e-0d52-427a-81e0-7dfe4dc6eda6",
"status": "completed",
"targets": {
"custom": null,
"default": false
},
"time_end": 1677425182,
"time_start": 1677424566,
"visibility": "public"
},
{
"id": 17235342,
"is_archived": true,
"reindexing": null,
"scan_uuid": "2c592d52-df56-42e0-9f18-d892bdeb1e18",
"status": "completed",
"targets": {
"custom": null,
"default": false
},
"time_end": 1677424556,
"time_start": 1677423906,
"visibility": "public"
},
{
"id": 17235033,
"is_archived": true,
"reindexing": null,
"scan_uuid": "44586b4f-1051-415c-b375-db86f6bd8c13",
"status": "completed",
"targets": {
"custom": null,
"default": false
},
"time_end": 1677423865,
"time_start": 1677423247,
"visibility": "public"
},
{
"id": 17234969,
"is_archived": true,
"reindexing": null,
"scan_uuid": "06c12bf7-436f-489d-bb04-aae511ea9f5c",
"status": "completed",
"targets": {
"custom": null,
"default": false
},
"time_end": 1677423205,
"time_start": 1677422585,
"visibility": "public"
}
]
}
}
Human Readable Output
Tenable IO Scan History
History id History uuid Status Is archived Targets default Visibility Time start Time end 17235445 69a55b8e-0d52-427a-81e0-7dfe4dc6eda6 completed true false public 1677424566 1677425182 17235342 2c592d52-df56-42e0-9f18-d892bdeb1e18 completed true false public 1677423906 1677424556 17235033 44586b4f-1051-415c-b375-db86f6bd8c13 completed true false public 1677423247 1677423865 17234969 06c12bf7-436f-489d-bb04-aae511ea9f5c completed true false public 1677422585 1677423205
tenable-io-export-scan
Export and download a scan report.
Scan results older than 35 days are supported in Nessus and CSV formats only, and filters cannot be applied.
Scans that are actively running cannot be exported (run “tenable-io-list-scans” to view scan statuses)
Base Command
tenable-io-export-scan
Input
| Argument Name | Description | Required |
|---|---|---|
| scanId | The identifier for the scan to export. Run the “tenable-io-list-scans” command to get all available scans. | Required |
| historyId | The unique identifier of the historical data to export. Run the “tenable-io-get-scan-history” command to get history IDs. | Optional |
| historyUuid | The UUID of the historical data to export. Run the “tenable-io-get-scan-history” command to get history UUIDs. | Optional |
| format | The file format to export the scan in. Scans can be export in the HTML and PDF formats for up to 35 days. For scans that are older than 35 days, only the Nessus and CSV formats are supported. The “chapters” argument must be defined if the chosen format is HTML or PDF. . Possible values are: Nessus, HTML, PDF, CSV. Default is CSV. |
Required |
| chapters | A comma-separated list of chapters to include in the export. This argument is required if the file format is PDF or HTML. Possible values are: vuln_hosts_summary, vuln_by_host, compliance_exec, remediations, vuln_by_plugin, compliance. | Optional |
| filter | A comma-separated list of filters, in the format of “name quality value” to apply to the exported scan report. Example: “port.protocol eq tcp, plugin_id eq 1234567” Note: when used literally, commas and spaces should be escaped. (i.e. “\\,” for comma and “\\s” for space) Filters cannot be applied to scans older than 35 days. Run “tenable-io-list-scan-filters” to get all available filters, (“Filter name” (name), “Filter operators” (quality) and “Readable regex” (value) in response). For more information: https://developer.tenable.com/docs/scan-export-filters-tio . |
Optional |
| filterSearchType | For multiple filters, specifies whether to use the AND or the OR logical operator. Possible values are: AND, OR. Default is AND. | Optional |
| assetId | The ID of the asset scanned. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.Size | number | The size of the file in bytes. |
| InfoFile.Name | string | The name of the file. |
| InfoFile.EntryID | string | The War Room entry ID of the file. |
| InfoFile.Info | string | The format and encoding of the file. |
| InfoFile.Type | string | The type of the file. |
| InfoFile.Extension | unknown | The file extension of the file. |
Command example
!tenable-io-export-scan scanId=16 format=HTML chapters="compliance_exec,remediations,vuln_by_plugin" historyId=19540157 historyUuid=f7eaad37-23bd-4aac-a979-baab0e9a465b filterSearchType=OR filter="port.protocol eq tcp, plugin_id eq 1234567" assetId=10
Human Readable Output
Preparing scan report:
Returned file: scan_16_SSE-144f3dc6-cb2d-42fc-b6cc-dd20b807735f-html.html Download
tenable-io-get-audit-logs
Returns audit logs extracted from Tenable io.
Base Command
tenable-io-get-audit-logs
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | Set this argument to True in order to create events, otherwise the command will only display the events. Possible values are: true, false. Default is false. | Required |
| limit | The maximum number of alerts to return (maximum value - 5000). | Optional |
| from_date | Return events that occurred after the specified date. | Optional |
| to_date | Return events that occurred before the specified date. | Optional |
| actor_id | Return events that contain the specified actor UUID. | Optional |
| target_id | Return events matching the specified target UUID. | Optional |
Context Output
There is no context output for this command.
Command example
!tenable-io-get-audit-logs limit=1
Human Readable Output
Audit Logs List
Action Actor Crud Description Fields Id Is Anonymous Is Failure Received Target user.create id: test c {‘key’: ‘X-Access-Type’, ‘value’: ‘apikey’},
{‘key’: ‘X-Forwarded-For’, ‘value’: ‘1.2.3.4’},
{‘key’: ‘X-Request-Uuid’, ‘value’: ‘12:12:12:12:12’}12 true false 2022-05-18T16:33:02Z id: 12-1-1-1-1
name: test@test.com
type: User
<~PLATFORM>
License Requirements
The following configuration parameters require the Cortex XSIAM license:
- Fetch events
The following configuration parameters require Cortex XSIAM with the Exposure Management add-on:
- Fetch assets and vulnerabilities
</~PLATFORM>
Configuration parameters
url— Server URL (required)access-key— Access keycredentials_access_key— (required)secret-key— Secret keycredentials_secret_key— (required)unsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetchEvents— Fetch eventsisFetchAssets— Fetch assets and vulnerabilitiesfirst_fetch— Events first fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)max_fetch— Events max fetcheventFetchInterval— Events Fetch IntervalassetsFetchInterval— Assets and vulnerabilities fetch interval
Commands (15)
-
tenable-io-export-assetsRetrieves details for the specified asset to include custom attributes.
-
tenable-io-export-scanExport and download a scan report. Scan results older than 35 days are supported in Nessus and CSV formats only, and filters cannot be applied. Scans that are actively running cannot be exported (run "tenable-io-list-scans" to view scan statuses).
-
tenable-io-export-vulnerabilitiesRetrieves details for the specified asset to include custom attributes.
-
tenable-io-get-asset-detailsRetrieves details for the specified asset including custom attributes.
-
tenable-io-get-audit-logsReturns audit logs extracted from Tenable io.
-
tenable-io-get-scan-historyLists the individual runs of the specified scan.
-
tenable-io-get-scan-reportRetrieves a scan report for the specified scan.
-
tenable-io-get-scan-statusChecks the status of a specific scan using the scan ID. Possible values: "Running", "Completed", and "Empty" (Ready to run).
-
tenable-io-get-vulnerabilities-by-assetGets a list of up to 5000 of the vulnerabilities recorded for a specified asset.
-
tenable-io-get-vulnerability-detailsRetrieves details for the specified vulnerability.
-
tenable-io-launch-scanLaunches a scan with existing or custom targets. You can specify custom targets in the command arguments.
-
tenable-io-list-scan-filtersLists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them.
-
tenable-io-list-scansRetrieves scans from the Tenable platform.
-
tenable-io-pause-scanPauses all scans inputted as an array. Will pause scans whose status is 'Running'.
-
tenable-io-resume-scanResumes all scans inputted as an array. Will resume scans whose status is 'Paused'.
import gc import re # pylint: disable=W9011 import sys import time import traceback from datetime import datetime import demistomock as demisto # noqa: F401 import requests import urllib3 from CommonServerPython import * # noqa: F401 from requests.exceptions import HTTPError # Disable insecure warnings urllib3.disable_warnings() DEFAULT_POLLING_TIMEOUT = 600 DEFAULT_POLLING_INTERVAL = 10 FIELD_NAMES_MAP = { "ScanType": "Type", "ScanStart": "StartTime", "ScanEnd": "EndTime", "ScannerName": "Scanner", "SeenLast": "LastSeen", "SeenFirst": "FirstSeen", "PluginId": "Id", "Count": "VulnerabilityOccurences", } REMEDIATIONS_NAMES_MAP = { "Value": "Id", "Vulns": "AssociatedVulnerabilities", "Hosts": "AffectedHosts", "Remediation": "Description", } ASSET_VULNS_NAMES_MAP = {"PluginId": "Id", "PluginFamily": "Family", "PluginName": "Name", "Count": "VulnerabilityOccurences"} GET_SCANS_HEADERS = [ "FolderId", "Id", "Name", "Targets", "Status", "StartTime", "EndTime", "Enabled", "Type", "Owner", "Scanner", "Policy", "CreationDate", "LastModificationDate", ] LAUNCH_SCAN_HEADERS = ["Id", "Targets", "Status"] SCAN_REPORT_INFO_HEADERS = ["Id", "Name", "Targets", "Status", "StartTime", "EndTime", "Scanner", "Policy"] SCAN_REPORT_VULNERABILITIES_HEADERS = [ "Id", "Name", "Severity", "Description", "Synopsis", "Solution", "FirstSeen", "LastSeen", "VulnerabilityOccurences", ] SCAN_REPORT_HOSTS_HEADERS = ["Hostname", "Score", "Severity", "Critical", "High", "Medium", "Low"] SCAN_REPORT_REMEDIATIONS_HEADERS = ["Id", "Description", "AffectedHosts", "AssociatedVulnerabilities"] VULNERABILITY_DETAILS_HEADERS = [ "Name", "Severity", "Type", "Family", "Description", "Synopsis", "Solution", "FirstSeen", "LastSeen", "PublicationDate", "ModificationDate", "VulnerabilityOccurences", "CvssVector", "CvssBaseScore", "Cvss3Vector", "Cvss3BaseScore", ] ASSET_VULNS_HEADERS = ["Id", "Name", "Severity", "Family", "VulnerabilityOccurences", "VulnerabilityState"] severity_to_text = ["None", "Low", "Medium", "High", "Critical"] FETCH_COMMAND = {"events": 0, "assets": 1} PARAMS = demisto.params() # pylint: disable=W9016 BASE_URL = PARAMS["url"] # pylint: disable=W9019 ACCESS_KEY = PARAMS.get("credentials_access_key", {}).get("password") or PARAMS.get("access-key") SECRET_KEY = PARAMS.get("credentials_secret_key", {}).get("password") or PARAMS.get("secret-key") USER_AGENT_HEADERS_VALUE = "Integration/1.0 (PAN; Cortex-XSOAR; Build/2.0)" AUTH_HEADERS = {"X-ApiKeys": f"accessKey={ACCESS_KEY}; secretKey={SECRET_KEY}"} HEADERS = AUTH_HEADERS | { "accept": "application/json", "content-type": "application/json", "User-Agent": USER_AGENT_HEADERS_VALUE, } USE_SSL = not PARAMS["unsecure"] USE_PROXY = PARAMS.get("proxy", False) if not USE_PROXY: # Remove proxy environment variables if they exist for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]: os.environ.pop(proxy_var, None) DATE_FORMAT = "%Y-%m-%d" VENDOR = "tenable" PRODUCT = "io" CHUNK_SIZE = 5000 ASSETS_NUMBER = 100 MAX_CHUNKS_PER_FETCH = 8 MAX_VULNS_CHUNKS_PER_FETCH = 8 ASSETS_FETCH_FROM = "90 days" VULNS_FETCH_FROM = "3 days" MIN_ASSETS_INTERVAL = 60 NOT_FOUND_ERROR = "404" XSIAM_EVENT_CHUNK_SIZE_LIMIT = 4 * (10**6) # 4 MB MAX_404_RETRIES = 3 # Maximum number of 404 retries before giving up on current export class Client(BaseClient): def list_scan_filters(self): return self._http_request("GET", "filters/scans/reports") def get_scan_history(self, scan_id, params) -> dict: # pylint: disable=W9014 remove_nulls_from_dictionary(params) return self._http_request("GET", f"scans/{scan_id}/history", params=params) def initiate_export_scan(self, scan_id: str, params: dict, body: dict) -> dict: remove_nulls_from_dictionary(params) remove_nulls_from_dictionary(body) return self._http_request("POST", f"scans/{scan_id}/export", params=params, json_data=body) def check_export_scan_status(self, scan_id: str, file_id: str) -> dict: return self._http_request("GET", f"scans/{scan_id}/export/{file_id}/status") def download_export_scan(self, scan_id: str, file_id: str, file_format: str) -> dict: return fileResult( f"scan_{scan_id}_{file_id}.{file_format.lower()}", self._http_request("GET", f"scans/{scan_id}/export/{file_id}/download", resp_type="content"), EntryType.ENTRY_INFO_FILE, ) @staticmethod def add_query(query, param_to_add): # pylint: disable=W9014 if query: return f"{query}&{param_to_add}" return f"?{param_to_add}" def get_audit_logs_request( self, from_date: str = None, to_date: str = None, actor_id: str = None, target_id: str = None, limit: int = None ): """ Args: limit: limit number of audit logs to get. from_date: date to fetch audit logs from. to_date: date which until to fetch audit logs. actor_id: fetch audit logs with matching actor id. target_id:fetch audit logs with matching target id. Returns: audit logs fetched from the API. """ query = "" if from_date: query = self.add_query(query, f"f=date.gt:{from_date}") if to_date: query = self.add_query(query, f"f=date.lt:{to_date}") if actor_id: query = self.add_query(query, f"f=actor_id.match:{actor_id}") if target_id: query = self.add_query(query, f"f=target_id.match:{target_id}") if limit: query = self.add_query(query, f"limit={limit}") else: query = self.add_query(query, "limit=5000") res = self._http_request(method="GET", url_suffix=f"/audit-log/v1/events{query}", headers=self._headers) return res.get("events", []) def get_vuln_export_uuid(self, num_assets: int, last_found: Optional[float]): """ Args: num_assets: number of assets used to chunk the vulnerabilities. last_found: vulnerabilities that were last found between the specified date (in Unix time) and now. Returns: The UUID of the vulnerabilities export job. """ payload: dict[str, Any] = {"filters": {"last_found": last_found}, "num_assets": num_assets} demisto.debug(f"my payload is: {payload}") res = self._http_request(method="POST", url_suffix="/vulns/export", headers=self._headers, json_data=payload) return res.get("export_uuid", "") def get_vuln_export_status(self, export_uuid: str): """ Args: export_uuid: The UUID of the vulnerabilities export job. Returns: The status of the job, and number of chunks available if succeeded. """ res = self._http_request( method="GET", url_suffix=f"/vulns/export/{export_uuid}/status", headers=self._headers, ok_codes=(200, 404) ) if isinstance(res, dict) and (res.get("status") == 404 or res.get("error")): return "ERROR", [] return res.get("status"), res.get("chunks_available") or [] def download_vulnerabilities_chunk(self, export_uuid: str, chunk_id: int): """ Args: export_uuid: The UUID of the vulnerabilities export job. chunk_id: The ID of the chunk you want to export. Returns: Chunk of vulnerabilities from API. """ result = self._http_request( method="GET", url_suffix=f"/vulns/export/{export_uuid}/chunks/{chunk_id}", headers=self._headers, ok_codes=(200, 404) ) if isinstance(result, dict) and (result.get("status") == 404 or result.get("error")): demisto.debug(f"404 error was received, result from api: {result}") return NOT_FOUND_ERROR return result def get_asset_export_uuid(self, fetch_from): # pylint: disable=W9014 """ Args: fetch_from: the last asset that was fetched previously. Returns: The UUID of the assets export job. """ payload = {"chunk_size": CHUNK_SIZE, "filters": {"updated_at": fetch_from}} demisto.debug(f"my payload is: {payload}") res = self._http_request(method="POST", url_suffix="assets/export", json_data=payload, headers=self._headers) return res.get("export_uuid") def get_assets_export_status(self, export_uuid): # pylint: disable=W9014 """ Args: export_uuid: The UUID of the assets export job. Returns: The assets' chunk id. """ res = self._http_request( method="GET", url_suffix=f"assets/export/{export_uuid}/status", headers=self._headers, ok_codes=(200, 404) ) if isinstance(res, dict) and (res.get("status") == 404 or res.get("error")): return "ERROR", [] return res.get("status"), res.get("chunks_available") def download_assets_chunk(self, export_uuid: str, chunk_id: int): """ Args: export_uuid: The UUID of the assets export job. chunk_id: The ID of the chunk you want to export. Returns: Chunk of assets from API. """ result = self._http_request( method="GET", url_suffix=f"/assets/export/{export_uuid}/chunks/{chunk_id}", headers=self._headers, ok_codes=(404, 200) ) # export uuid has expired if isinstance(result, dict) and (result.get("status") == 404 or result.get("error")): demisto.debug(f"404 error was received, result from api: {result}") return NOT_FOUND_ERROR return result def flatten(d): # pylint: disable=W9014 r = {} # type: ignore for v in d.values(): if isinstance(v, dict): r.update(flatten(v)) d.update(r) return d def filter_dict_null(d): # pylint: disable=W9014 if isinstance(d, dict): return {k: v for k, v in d.items() if v is not None} return d def filter_dict_keys(d, keys): # pylint: disable=W9014 if isinstance(d, list): return [filter_dict_keys(x, keys) for x in d] if isinstance(d, dict): return {k: v for k, v in d.items() if k in keys} return d def convert_severity_values(d): # pylint: disable=W9014 if isinstance(d, list): return list(map(convert_severity_values, d)) if isinstance(d, dict): return {k: (severity_to_text[v] if k == "Severity" else v) for k, v in d.items()} return d def convert_dict_context_dates(d): # pylint: disable=W9014 def convert_epoch_to_date(k, v): # pylint: disable=W9014 if any(s in k.lower() for s in ("date", "time")): try: return datetime.utcfromtimestamp(int(v)).strftime("%Y-%m-%dT%H:%M:%SZ") except Exception: pass return v if isinstance(d, list): return list(map(convert_dict_context_dates, d)) if isinstance(d, dict): return {k: convert_dict_context_dates(convert_epoch_to_date(k, v)) for k, v in d.items()} return d def convert_dict_readable_dates(d): # pylint: disable=W9014 def convert_epoch_to_date(k, v): # pylint: disable=W9014 return formatEpochDate(v) if isinstance(v, int) and any(s in k.lower() for s in ("date", "time")) else v if isinstance(d, list): return list(map(convert_dict_readable_dates, d)) if isinstance(d, dict): return {k: convert_dict_readable_dates(convert_epoch_to_date(k, v)) for k, v in d.items()} return d def get_entry_for_object(title, context_key, obj, headers=None, remove_null=False): # pylint: disable=W9014 def intersection(lst1, lst2): # pylint: disable=W9014 return [value for value in lst1 if value in lst2] if len(obj) == 0: return "There is no output result" filtered_obj = filter_dict_null(obj) if isinstance(filtered_obj, list): filtered_obj = list(map(filter_dict_null, filtered_obj)) if headers and isinstance(filtered_obj, dict): headers = intersection(headers, list(filtered_obj.keys())) hr_obj = convert_dict_readable_dates(filtered_obj) context_obj = convert_dict_context_dates(filter_dict_keys(filtered_obj, headers) if headers else filtered_obj) return { "Type": entryTypes["note"], "Contents": obj, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, hr_obj, headers, removeNull=remove_null), "EntryContext": {context_key: context_obj}, } def replace_keys(src, trans_map=FIELD_NAMES_MAP, camelize=True): # pylint: disable=W9014 def snake_to_camel(snake_str): # pylint: disable=W9014 components = snake_str.split("_") return "".join([x.title() for x in components]) def replace(key, trans_map): # pylint: disable=W9014 if key in trans_map: return trans_map[key] return key if isinstance(src, list): return [replace_keys(x, trans_map, camelize) for x in src] if camelize: src = {snake_to_camel(k): v for k, v in src.items()} if trans_map: src = {replace(k, trans_map): v for k, v in src.items()} return src def date_range_to_param(date_range): # pylint: disable=W9014 params = {} if date_range: try: date_range = int(date_range) params["date_range"] = date_range except ValueError: raise DemistoException(f"Invalid date range: {date_range}") return params def get_scan_error_message(response, scan_id): # pylint: disable=W9014 code = response.status_code message = "Error processing request" if scan_id: message += f" for scan with id {scan_id}" message += f". Got response status code: {code}" if code == 401: message += " - Scan is disabled." elif code == 403: message += f" - {response.json()['error']}" elif code == 404: message += " - Scan does not exist." elif code == 409: message += " - Scan cannot be launched in its current status." return message # Request/Response methods # kwargs: request parameters def send_scan_request(scan_id="", endpoint="", method="GET", ignore_license_error=False, body=None, **kwargs): # pylint: disable=W9014 if endpoint: endpoint = "/" + endpoint full_url = f"{BASE_URL}scans/{scan_id!s}{endpoint}" try: res = requests.request(method, full_url, headers=AUTH_HEADERS, verify=USE_SSL, json=body, params=kwargs) res.raise_for_status() return res.json() except HTTPError as e: demisto.debug(str(e)) if ignore_license_error and res.status_code in (403, 500): return None err_msg = get_scan_error_message(res, scan_id) if demisto.command() != "test-module": raise DemistoException(err_msg) else: demisto.results(err_msg) # pylint: disable=W9008 demisto.error(traceback.format_exc()) sys.exit(0) except ValueError: return "No JSON to decode." def get_scan_info(scans_result_elem): # pylint: disable=W9014 response = send_scan_request(scans_result_elem["id"], ignore_license_error=True) if response: response["info"].update(scans_result_elem) return response["info"] return None def send_vuln_details_request(plugin_id, date_range=None): # pylint: disable=W9014 full_url = f"{BASE_URL}workbenches/vulnerabilities/{plugin_id}/info" res = requests.get(full_url, headers=AUTH_HEADERS, verify=USE_SSL, params=date_range_to_param(date_range)) return res.json() def get_vuln_info(vulns): # pylint: disable=W9014 vulns_info = {v["plugin_id"]: v for v in vulns} infos = [] errors = [] for pid, info in vulns_info.items(): vuln_details = send_vuln_details_request(pid) if "error" in vuln_details: errors.append(info) else: info.update(flatten(vuln_details["info"])) infos.append(info) return infos, errors def send_assets_request(params): # pylint: disable=W9014 full_url = f"{BASE_URL}workbenches/assets" res = requests.request("GET", full_url, headers=AUTH_HEADERS, params=params, verify=USE_SSL) return res.json() def get_asset_id(params): # pylint: disable=W9014 assets = send_assets_request(params) if "error" in assets: raise DemistoException(assets["error"]) if assets.get("assets"): return assets["assets"][0]["id"] return None def send_asset_vuln_request(asset_id, date_range): # pylint: disable=W9014 full_url = f"{BASE_URL}workbenches/assets/{asset_id}/vulnerabilities/" res = requests.get(full_url, headers=AUTH_HEADERS, verify=USE_SSL, params=date_range_to_param(date_range)) res.raise_for_status() return res.json() def send_asset_details_request(asset_id: str) -> Dict[str, Any]: """Gets asset details using the '{BASE_URL}workbenches/assets/{asset_id}/info' endpoint. Args: asset_id (string): id of the asset. Returns: dict: dict containing information on an asset. """ full_url = f"{BASE_URL}workbenches/assets/{asset_id}/info" try: res = requests.get(full_url, headers=AUTH_HEADERS, verify=USE_SSL) res.raise_for_status() except HTTPError as exc: raise DemistoException(f"Error calling for url {full_url}: error message {exc}") return res.json() def send_asset_attributes_request(asset_id: str) -> Dict[str, Any]: """Gets asset attributes using the '{BASE_URL}api/v3/assets/{asset_id}/attributes' endpoint. Args: asset_id (string): id of the asset. Returns: dict: dict containing information on an asset. """ full_url = f"{BASE_URL}api/v3/assets/{asset_id}/attributes" try: res = requests.get(full_url, headers=AUTH_HEADERS, verify=USE_SSL) res.raise_for_status() except HTTPError as exc: raise DemistoException(f"Error calling for url {full_url}: error message {exc}") return res.json() def get_timestamp(timestamp): # pylint: disable=W9014 return time.mktime(timestamp.timetuple()) def generate_snapshot_id() -> str: """ Generate a unique snapshot ID for XSIAM dataset snapshots. Uses current timestamp in milliseconds to ensure uniqueness across fetch cycles. This ID is used to group assets/vulnerabilities that belong to the same snapshot, allowing XSIAM to properly track complete vs incomplete snapshots. Returns: str: A unique snapshot ID based on current timestamp in milliseconds. """ return str(round(time.time() * 1000)) def generate_export_uuid(client: Client, last_run): # pylint: disable=W9014 """ Generate a job export uuid in order to fetch vulnerabilities. Args: client: Client class object. first_fetch: time to first fetch from. last_run: last run object. """ demisto.info("Getting vulnerabilities export uuid for report.") last_found: float = get_timestamp(arg_to_datetime(VULNS_FETCH_FROM)) # type: ignore export_uuid = client.get_vuln_export_uuid(num_assets=ASSETS_NUMBER, last_found=last_found) demisto.info(f"vulnerabilities export uuid is {export_uuid}") # Ensure snapshot_id exists (should already be set by generate_assets_export_uuid) if "snapshot_id" not in last_run: snapshot_id = generate_snapshot_id() demisto.debug(f"Generated new snapshot_id for vulnerabilities: {snapshot_id}") last_run["snapshot_id"] = snapshot_id last_run.update({"vuln_export_uuid": export_uuid}) def generate_assets_export_uuid(client: Client, assets_last_run): # pylint: disable=W9014 """ Generate a job export uuid in order to fetch assets. Args: client: Client class object. first_fetch: time to first fetch assets from. assets_last_run: assets last run object. """ demisto.info("Generating assets export uuid.") fetch_from = round(get_timestamp(arg_to_datetime(ASSETS_FETCH_FROM))) export_uuid = client.get_asset_export_uuid(fetch_from=fetch_from) demisto.debug(f"assets export uuid is {export_uuid}") # Generate a new snapshot_id for this fetch cycle (used for XSIAM dataset snapshots) snapshot_id = generate_snapshot_id() demisto.debug(f"Generated new snapshot_id: {snapshot_id}") assets_last_run.update({"assets_export_uuid": export_uuid, "snapshot_id": snapshot_id, "total_assets": 0}) def handle_assets_chunks(client: Client, assets_last_run): # pylint: disable=W9014 """ Handle assets chunks stored in the last run object. Args: client: Client class object. assets_last_run: assets last run object. """ demisto.debug("in handle assets chunks") stored_chunks = assets_last_run.get("assets_available_chunks", []) updated_stored_chunks = stored_chunks.copy() export_uuid = assets_last_run.get("assets_export_uuid") assets = [] for chunk_id in stored_chunks[:MAX_CHUNKS_PER_FETCH]: result = client.download_assets_chunk(export_uuid=export_uuid, chunk_id=chunk_id) if result == NOT_FOUND_ERROR: # Track 404 retries to prevent infinite loop retry_count = assets_last_run.get("assets_404_retry_count", 0) + 1 if retry_count >= MAX_404_RETRIES: demisto.error( f"Assets export failed after {MAX_404_RETRIES} retries due to 404 errors. " "The export UUID may be expiring before all chunks can be downloaded. " "Consider increasing the fetch frequency or reducing the data volume. " "Clearing export state to start fresh on next fetch cycle." ) # Clear all assets export state to start fresh on next cycle assets_last_run.pop("assets_export_uuid", None) assets_last_run.pop("assets_available_chunks", None) assets_last_run.pop("assets_404_retry_count", None) return [], assets_last_run demisto.info( f"404 error received (retry {retry_count}/{MAX_404_RETRIES}). " "Generating new export uuid to start new fetch." ) fetch_from = round(get_timestamp(arg_to_datetime(ASSETS_FETCH_FROM))) export_uuid = client.get_asset_export_uuid(fetch_from=fetch_from) # Generate a new snapshot_id when resetting the fetch snapshot_id = generate_snapshot_id() assets_last_run.update( { "assets_export_uuid": export_uuid, "snapshot_id": snapshot_id, "total_assets": 0, "assets_404_retry_count": retry_count, } ) assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) assets_last_run.pop("assets_available_chunks", None) demisto.debug(f"after resetting last run sending lastrun: {assets_last_run}") return [], assets_last_run assets.extend(result) updated_stored_chunks.remove(chunk_id) # Reset retry count on successful chunk download assets_last_run.pop("assets_404_retry_count", None) # Note: total_assets counter is updated in main() after successful send_data_to_xsiam() # to ensure the counter only reflects assets that were actually sent to XSIAM if updated_stored_chunks: assets_last_run.update( {"assets_available_chunks": updated_stored_chunks, "nextTrigger": "30", "type": FETCH_COMMAND.get("assets")} ) else: assets_last_run.pop("assets_available_chunks", None) assets_last_run.pop("assets_export_uuid", None) return assets, assets_last_run def handle_vulns_chunks(client: Client, assets_last_run): # pragma: no cover # pylint: disable=W9014 """ Handle vulns chunks stored in the last run object. Args: client: Client class object. assets_last_run: assets last run object. """ demisto.debug("in handle vulns chunks") stored_chunks = assets_last_run.get("vulns_available_chunks", []) updated_stored_chunks = stored_chunks.copy() export_uuid = assets_last_run.get("vuln_export_uuid") vulnerabilities = [] for chunk_id in stored_chunks[:MAX_VULNS_CHUNKS_PER_FETCH]: result = client.download_vulnerabilities_chunk(export_uuid=export_uuid, chunk_id=chunk_id) if result == NOT_FOUND_ERROR: # Track 404 retries to prevent infinite loop retry_count = assets_last_run.get("vulns_404_retry_count", 0) + 1 if retry_count >= MAX_404_RETRIES: demisto.error( f"Vulnerabilities export failed after {MAX_404_RETRIES} retries due to 404 errors. " "The export UUID may be expiring before all chunks can be downloaded. " "Consider increasing the fetch frequency or reducing the data volume. " "Clearing export state to start fresh on next fetch cycle." ) # Clear all vuln export state to start fresh on next cycle assets_last_run.pop("vuln_export_uuid", None) assets_last_run.pop("vulns_available_chunks", None) assets_last_run.pop("vulns_404_retry_count", None) return [], assets_last_run demisto.info( f"404 error received (retry {retry_count}/{MAX_404_RETRIES}). " "Generating new export uuid to start new fetch." ) export_uuid = client.get_vuln_export_uuid( num_assets=ASSETS_NUMBER, last_found=round(get_timestamp(arg_to_datetime(VULNS_FETCH_FROM))) ) # Note: snapshot_id and total_assets are managed by the assets flow, not vulnerabilities # We only update the vuln_export_uuid and retry count here assets_last_run.update( { "vuln_export_uuid": export_uuid, "vulns_404_retry_count": retry_count, } ) assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) assets_last_run.pop("vulns_available_chunks", None) demisto.debug(f"after resetting last run sending lastrun: {assets_last_run}") return [], assets_last_run vulnerabilities.extend(result) updated_stored_chunks.remove(chunk_id) # Reset retry count on successful chunk download assets_last_run.pop("vulns_404_retry_count", None) for vuln in vulnerabilities: vuln["_time"] = vuln.get("received") or vuln.get("indexed") if updated_stored_chunks: assets_last_run.update( {"vulns_available_chunks": updated_stored_chunks, "nextTrigger": "30", "type": FETCH_COMMAND.get("assets")} ) else: assets_last_run.pop("vulns_available_chunks", None) assets_last_run.pop("vuln_export_uuid", None) # Note: snapshot_id and total_assets are NOT cleaned up here. # They belong to the assets snapshot lifecycle and must only be cleaned up # in main() AFTER the snapshot has been successfully sealed with the correct items_count. # Cleaning them here was causing the snapshot to never be sealed because: # 1. snapshot_id would be regenerated (new ID with no matching data rows) # 2. total_assets would reset to 0 (sealing path skipped since cumulative_total=0) return vulnerabilities, assets_last_run def get_asset_export_job_status(client: Client, assets_last_run): # pylint: disable=W9014 """ If job has succeeded (status FINISHED) get all information from all chunks available. Args: client: Client class object. assets_last_run: The last run. Returns: All information from all chunks available. """ status, chunks_available = client.get_assets_export_status(export_uuid=assets_last_run.get("assets_export_uuid")) demisto.info(f"Assets report status is {status}, and number of available chunks is {chunks_available}") if status == "FINISHED": assets_last_run.update({"assets_available_chunks": chunks_available}) return status def get_vulnerabilities_export_status(client: Client, assets_last_run): # pylint: disable=W9014 """ If job has succeeded (status FINISHED) get all information from all chunks available. Args: client: Client class object. export_uuid: The UUID of the vulnerabilities export job. Returns: All information from all chunks available. """ status, chunks_available = client.get_vuln_export_status(export_uuid=assets_last_run.get("vuln_export_uuid")) demisto.info(f"Report status is {status}, and number of available chunks is {chunks_available}") if status == "FINISHED": demisto.debug(f"returned {len(chunks_available)} vulns chunks") assets_last_run.update({"vulns_available_chunks": chunks_available}) return status def test_module(client: Client, params): # pylint: disable=W9014 # Use default of 720 minutes (12 hours) if assetsFetchInterval is not set or empty # This matches the defaultvalue in the YML configuration assets_fetch_interval = params.get("assetsFetchInterval") or MIN_ASSETS_INTERVAL if int(assets_fetch_interval) < MIN_ASSETS_INTERVAL: raise DemistoException( f"Assets and vulnerabilities fetch Interval is supposed to be {MIN_ASSETS_INTERVAL} minutes (1 hour) minimum." ) client.list_scan_filters() return "ok" def relational_date_to_epoch_date_format(date: Optional[str]) -> Optional[int]: """Retrieves date string or relational expression to date YYYY-MM-DD format. Example arg is "7 days ago". Args: date: str - date or relational expression. Returns: A str in epoch date format or None. """ if date: if date.isnumeric(): return int(date) else: date_datetime = dateparser.parse(date) # parser for human readable dates if date_datetime: # dateparser.parse returns datetime representing parsed date if successful, else returns None if date := date_datetime.strftime("%Y-%m-%d"): date_int = int(time.mktime(datetime.strptime(date, "%Y-%m-%d").timetuple())) return date_int else: raise DemistoException("Tenable.io: Date format is invalid") return None def get_scans_command(): folder_id = (demisto.args().get("folderId"),) # pylint: disable=W9017 last_modification_date = relational_date_to_epoch_date_format(demisto.getArg("lastModificationDate")) response = send_scan_request(folder_id=folder_id, last_modification_date=last_modification_date) scan_entries = list(map(get_scan_info, response["scans"])) valid_scans = [x for x in scan_entries if x is not None] invalid_scans = [k for k, v in zip(response["scans"], scan_entries) if v is None] res = [ get_entry_for_object( "Tenable.io - List of Scans", "TenableIO.Scan(val.Id && val.Id === obj.Id)", replace_keys(valid_scans), GET_SCANS_HEADERS, ) ] if invalid_scans: res.append( get_entry_for_object( "Inactive Web Applications Scans - Renew WAS license to use these scans", "TenableIO.Scan(val.Id && val.Id === obj.Id)", replace_keys(invalid_scans), GET_SCANS_HEADERS, remove_null=True, ) ) return res def launch_scan_command(): scan_id, targets = demisto.getArg("scanId"), demisto.getArg("scanTargets") scan_info = send_scan_request(scan_id)["info"] if not targets: targets = scan_info.get("targets", "") target_list = argToList(targets) body = assign_params(alt_targets=target_list) res = send_scan_request(scan_id, "launch", "POST", body=body) res.update({"id": scan_id, "targets": targets, "status": "pending"}) return get_entry_for_object( "The requested scan was launched successfully", "TenableIO.Scan(val.Id && val.Id === obj.Id)", replace_keys(res), LAUNCH_SCAN_HEADERS, ) def get_report_command(): scan_id, info, detailed = demisto.getArg("scanId"), demisto.getArg("info"), demisto.getArg("detailed") results = [] scan_details = send_scan_request(scan_id) if info == "yes": scan_details["info"]["id"] = scan_id scan_details["info"] = replace_keys(scan_details["info"]) results.append( get_entry_for_object( "Scan basic info", "TenableIO.Scan(val.Id && val.Id === obj.Id)", scan_details["info"], SCAN_REPORT_INFO_HEADERS ) ) if "vulnerabilities" not in scan_details: return "No vulnerabilities found." vuln_info, vulns_not_found = get_vuln_info(scan_details["vulnerabilities"]) vuln_info = convert_severity_values(replace_keys(vuln_info)) results.append( get_entry_for_object("Vulnerabilities", "TenableIO.Vulnerabilities", vuln_info, SCAN_REPORT_VULNERABILITIES_HEADERS) ) if len(vulns_not_found) > 0: vulns_not_found = replace_keys(vulns_not_found) results.append( get_entry_for_object( "Vulnerabilities - Missing From Workbench", "TenableIO.Vulnerabilities", vulns_not_found, SCAN_REPORT_VULNERABILITIES_HEADERS, True, ) ) if detailed == "yes": assets = replace_keys(scan_details["hosts"] + scan_details["comphosts"]) results.append(get_entry_for_object("Assets", "TenableIO.Assets", assets, SCAN_REPORT_HOSTS_HEADERS)) if ( "remediations" in scan_details and "remediations" in scan_details["remediations"] and len(scan_details["remediations"]["remediations"]) > 0 ): remediations = replace_keys(scan_details["remediations"]["remediations"], REMEDIATIONS_NAMES_MAP) results.append( get_entry_for_object("Remediations", "TenableIO.Remediations", remediations, SCAN_REPORT_REMEDIATIONS_HEADERS) ) return results def get_vulnerability_details_command(): plugin_id, date_range = demisto.getArg("vulnerabilityId"), demisto.getArg("dateRange") info = send_vuln_details_request(plugin_id, date_range) if "error" in info: raise DemistoException(info["error"]) return get_entry_for_object( f"Vulnerability details - {plugin_id}", "TenableIO.Vulnerabilities", convert_severity_values(replace_keys(flatten(info["info"]))), VULNERABILITY_DETAILS_HEADERS, ) def args_to_request_params(hostname, ip, date_range): # pylint: disable=W9014 if not hostname and not ip: raise DemistoException("Please provide one of the following arguments: hostname, ip") indicator = hostname if hostname else ip params = {"filter.0.filter": "host.target", "filter.0.quality": "eq", "filter.0.value": indicator} if date_range: if not date_range.isdigit(): raise DemistoException(f"Invalid date range: {date_range}") else: params["date_range"] = date_range return params, indicator def get_asset_details_command() -> CommandResults: """ tenable-io-get-asset-details: Retrieves details for the specified asset to include custom attributes. Args: None Returns: CommandResults: A ``CommandResults`` object that is then passed to ``return_results``, that contains asset details. """ ip = demisto.getArg("ip") if not ip: raise DemistoException("Please provide an IP address") params = {"filter.0.filter": "host.target", "filter.0.quality": "eq", "filter.0.value": ip} asset_id = get_asset_id(params) if not asset_id: return CommandResults(readable_output=f"Asset not found: {ip}") try: info = send_asset_details_request(asset_id) attrs = send_asset_attributes_request(asset_id) if attrs: info["info"]["attributes"] = [{attr.get("name", ""): attr.get("value", "")} for attr in attrs.get("attributes", [])] except DemistoException as e: raise DemistoException(f"Failed to include custom attributes. {e}") readable_output = tableToMarkdown( f"Asset Info for {ip}", info["info"], headers=["attributes", "fqdn", "interfaces", "ipv4", "id", "last_seen"] ) return CommandResults( readable_output=readable_output, raw_response=info["info"], outputs_prefix="TenableIO.AssetDetails", outputs_key_field="id", outputs=info["info"], ) def get_vulnerabilities_by_asset_command(): hostname, ip, date_range = demisto.getArg("hostname"), demisto.getArg("ip"), demisto.getArg("dateRange") params, indicator = args_to_request_params(hostname, ip, date_range) asset_id = get_asset_id(params) if not asset_id: return f"No Vulnerabilities for asset {indicator}" info = send_asset_vuln_request(asset_id, date_range) if "error" in info: raise DemistoException(info["error"]) vulns = convert_severity_values(replace_keys(info["vulnerabilities"], ASSET_VULNS_NAMES_MAP)) if vulns: entry = get_entry_for_object( f"Vulnerabilities for asset {indicator}", "TenableIO.Vulnerabilities", vulns, ASSET_VULNS_HEADERS ) entry["EntryContext"]["TenableIO.Assets(val.Hostname === obj.Hostname)"] = { "Vulnerabilities": [x["plugin_id"] for x in info["vulnerabilities"]], "Hostname": indicator, } return entry return None def get_scan_status_command(): scan_id = demisto.getArg("scanId") scan_details = send_scan_request(scan_id) scan_status = {"Id": scan_id, "Status": scan_details["info"]["status"]} return get_entry_for_object(f"Scan status for {scan_id}", "TenableIO.Scan(val.Id && val.Id === obj.Id)", scan_status) def pause_scan_command(): scan_ids = str(demisto.getArg("scanId")).split(",") results = [] for scan_id in scan_ids: scan_id = scan_id.strip() scan_details = send_scan_request(scan_id) scan_status = {"Id": scan_id, "Status": scan_details["info"]["status"]} if scan_status["Status"].lower() == "running": send_scan_request(scan_id, "pause", "POST") resumed_scan = {"Id": scan_id, "Status": "Pausing"} results.append( get_entry_for_object( "The requested scan was paused successfully", "TenableIO.Scan(val.Id && val.Id === obj.Id)", replace_keys(resumed_scan), ["Id", "Status"], ) ) else: results.append( f"Command 'tenable-io-pause-scan' cannot be called while scan status is {scan_status['Status']} for scanID" " {scan_id}" ) return results def resume_scan_command(): scan_ids = str(demisto.getArg("scanId")).split(",") results = [] for scan_id in scan_ids: scan_id = scan_id.strip() scan_details = send_scan_request(scan_id) scan_status = {"Id": scan_id, "Status": scan_details["info"]["status"]} if scan_status["Status"].lower() == "paused": send_scan_request(scan_id, "resume", "POST") resumed_scan = {"Id": scan_id, "Status": "Resuming"} results.append( get_entry_for_object( "The requested scan was resumed successfully", "TenableIO.Scan(val.Id && val.Id === obj.Id)", replace_keys(resumed_scan), ["Id", "Status"], ) ) else: results.append( f"Command 'tenable-io-resume-scan' cannot be called while scan status is {scan_status['Status']} for scanID " "{scan_id}" ) return results def export_request(request_params: dict, assets_or_vulns: str) -> dict: """Gets the UUID of the assets/vulnerabilities export job. Args: request_params (dict): The request params. assets_or_vulns (string): A string represents part of the endpoint according to the requested (assets or vulnerabilities) Returns: dict: The UUID of the assets export job or raise DemistoException. """ full_url = f"{BASE_URL}{assets_or_vulns}/export" res = requests.post(full_url, headers=HEADERS, verify=USE_SSL, json=request_params) if res.status_code != 200: raise DemistoException(res.text) return res.json() def export_request_with_export_uuid(export_uuid: str, assets_or_vulns: str) -> dict: """Gets status details of the export job. Args: export_uuid (string): The UUID of the assets/vulnerabilities export job. assets_or_vulns (string): A string represents part of the endpoint according to the requested (assets or vulnerabilities) Returns: dict: Status of the export job or raise DemistoException. """ full_url = f"{BASE_URL}{assets_or_vulns}/export/{export_uuid}/status" res = requests.get(full_url, headers=HEADERS, verify=USE_SSL) if res.status_code != 200: raise DemistoException(res.text) return res.json() def get_chunks_request(export_uuid: str, chunk_id: str, assets_or_vulns: str) -> dict: """Gets chunks of assets or vulnerabilities Args: export_uuid (string): The UUID of the assets/vulnerabilities export job. assets_or_vulns (string): A string represents part of the endpoint according to the requested data (assets or vulnerabilities) chunk_id (string): the id of assets/vulnerabilities the chunk requested to export. Returns: dict: Status of the export job or raise DemistoException. """ full_url = f"{BASE_URL}{assets_or_vulns}/export/{export_uuid}/chunks/{chunk_id}" res = requests.get(full_url, headers=HEADERS, verify=USE_SSL) if res.status_code != 200: raise DemistoException(res.text) return res.json() def get_export_chunks_details(export_uuid_status_response: dict, export_uuid: str, assets_or_vulns: str) -> list[Dict]: """Gets All chunks of assets or vulnerabilities export. Args: export_uuid_status_response (dict): The response with the chunks details. export_uuid (string): The UUID of the assets/vulnerabilities export job. assets_or_vulns (string): A string represents part of the endpoint according to the requested data (assets or vulnerabilities) Returns: dict: Status of the export job. """ chunks_list_id = export_uuid_status_response.get("chunks_available") chunks_response_list: list = [] if chunks_list_id: for chunk_id in chunks_list_id: chunk_response = get_chunks_request(export_uuid, chunk_id, assets_or_vulns) chunks_response_list.extend(chunk_response) return chunks_response_list def export_assets_build_command_result(chunks_details_list: list[dict]) -> CommandResults: """Builds command result object from chunks details list Args: chunks_details_list (list[dict]): a list[dict] of assets details. Returns: CommandResults: Command Results object with the relevant data. """ headers = [ "ASSET ID", "DNS NAME (FQDN)", "SYSTEM TYPE", "OPERATING SYSTEM", "IPV4 ADDRESS", "NETWORK", "FIRST SEEN", "LAST SEEN", "LAST LICENSED SCAN", "SOURCE", "TAGS", ] human_readable = [] for chunk_details in chunks_details_list: human_readable_to_append = {} if fqdns := chunk_details.get("fqdns"): human_readable_to_append["DNS NAME (FQDN)"] = fqdns[0] if (tag := chunk_details.get("tags")) and (first_tag := tag[0]): human_readable_to_append["TAGS"] = f'{first_tag.get("key")}:{first_tag.get("value")}' if (sources := chunk_details.get("sources")) and (first_source := sources[0]): human_readable_to_append["SOURCE"] = first_source.get("name") if (network_interfaces := chunk_details.get("network_interfaces")) and ( first_network_interfaces := network_interfaces[0] ): human_readable_to_append["IPV4 ADDRESS"] = first_network_interfaces.get("ipv4s") human_readable_to_append.update( { "ASSET ID": chunk_details.get("id"), "SYSTEM TYPE": chunk_details.get("system_types"), "OPERATING SYSTEM": chunk_details.get("operating_systems"), "NETWORK": chunk_details.get("network_name"), "FIRST SEEN": chunk_details.get("first_seen"), "LAST SEEN": chunk_details.get("last_seen"), "LAST LICENSED SCAN": chunk_details.get("last_licensed_scan_date"), } ) remove_nulls_from_dictionary(chunk_details) human_readable.append(human_readable_to_append) return CommandResults( outputs_key_field="id", outputs_prefix="TenableIO.Asset", outputs=chunks_details_list, raw_response=chunks_details_list, readable_output=tableToMarkdown("Assets", human_readable, headers=headers, removeNull=True), ) def request_uuid_export_assets(args: Dict[str, Any]) -> PollResult: """ Gets the UUID of the assets export job. Args: args (Dict[str, Any]): Arguments passed down by the CLI to provide in the HTTP request. Returns: PollResult: A result to return to the user which will be set as a CommandResults. """ tag_category = args.get("tagCategory") tag_value = args.get("tagValue") request_params = remove_empty_elements( { "chunk_size": arg_to_number(args.get("chunkSize")), "include_unlicensed": args.get("isLicensed"), "filters": { "created_at": relational_date_to_epoch_date_format(args.get("createdAt")), "updated_at": relational_date_to_epoch_date_format(args.get("updatedAt")), "terminated_at": relational_date_to_epoch_date_format(args.get("terminatedAt")), "is_terminated": argToBoolean(args.get("isTerminated")) if args.get("isTerminated") else None, "deleted_at": relational_date_to_epoch_date_format(args.get("deletedAt")), "is_deleted": argToBoolean(args.get("isDeleted")) if args.get("isDeleted") else None, "is_licensed": argToBoolean(args.get("isLicensed")) if args.get("isLicensed") else None, "first_scan_time": relational_date_to_epoch_date_format(args.get("firstScanTime")), "last_authenticated_scan_time": relational_date_to_epoch_date_format(args.get("lastAuthenticatedScanTime")), "last_assessed": relational_date_to_epoch_date_format(args.get("lastAssessed")), "servicenow_sysid": argToBoolean(args.get("serviceNowSysId")) if args.get("serviceNowSysId") else None, "sources": argToList(args.get("sources")), "has_plugin_results": argToBoolean(args.get("hasPluginResults")) if args.get("hasPluginResults") else None, }, } ) if tag_category and tag_value: if request_params.get("filters"): request_params.get("filters")[f"tag.{tag_category}"] = tag_value else: request_params["filters"] = {f"tag.{tag_category}": tag_value} if (tag_category and not tag_value) or (not tag_category and tag_value): raise DemistoException("Please specify tagCategory and tagValue") demisto.debug("request params export assets", request_params) api_response = export_request(request_params, "assets") export_uuid = api_response.get("export_uuid") demisto.debug(f"export_uuid: {export_uuid}") status = api_response.get("status") return PollResult( response=None, partial_result=CommandResults( outputs_prefix="TenableIO.Asset", outputs_key_field="id", readable_output="Waiting for export assets to finish...", ), continue_to_poll=True, args_for_next_run={"exportUuid": export_uuid, "status": status, **args}, ) def build_vpr_score(args: Dict[str, Any]) -> dict: """ Builds the vpr score request body. Args: args (Dict[str, Any]): Arguments vprScoreOperator, vprScoreRange, vprScoreValue passed down by the CLI to provide in the HTTP request. Returns: dict: vpr score dict. """ if not args.get("vprScoreValue") and args.get("vprScoreOperator"): raise DemistoException("Please specify vprScoreValue and vprScoreOperator") elif args.get("vprScoreRange") and args.get("vprScoreOperator"): raise DemistoException("Please specify only one of vprScoreRange or vprScoreOperator") elif args.get("vprScoreValue") and not args.get("vprScoreOperator"): raise DemistoException("Please specify vprScoreValue and vprScoreOperator") vpr_score_value = args.get("vprScoreValue") vpr_score = {} if vpr_score_value: vpr_score = { "eq": [float(x) for x in argToList(vpr_score_value)] if args.get("vprScoreOperator") == "equal" else None, "neq": [float(x) for x in argToList(vpr_score_value)] if args.get("vprScoreOperator") == "not equal" else None, "gt": float(vpr_score_value) if args.get("vprScoreOperator") == "gt" else None, "lt": float(vpr_score_value) if args.get("vprScoreOperator") == "lt" else None, "gte": float(vpr_score_value) if args.get("vprScoreOperator") == "gte" else None, "lte": float(vpr_score_value) if args.get("vprScoreOperator") == "lte" else None, } if args.get("vprScoreRange"): lower_range_bound, upper_range_bound = validate_range(args.get("vprScoreRange")) vpr_score["lte"] = upper_range_bound vpr_score["gte"] = lower_range_bound return vpr_score def request_uuid_export_vulnerabilities(args: Dict[str, Any]) -> PollResult: """ Gets the UUID of the vulnerabilities export job. Args: args (Dict[str, Any]): Arguments passed down by the CLI to provide in the HTTP request. Returns: PollResult: A result to return to the user which will be set as a CommandResults. """ tag_category = args.get("tagCategory") tag_value = argToList(args.get("tagValue")) request_params = remove_empty_elements( { "num_assets": arg_to_number(args.get("numAssets")), "include_unlicensed": argToBoolean(args.get("includeUnlicensed")) if args.get("includeUnlicensed") else None, "filters": { "cidr_range": args.get("cidrRange"), "first_found": relational_date_to_epoch_date_format(args.get("firstFound")), "last_fixed": relational_date_to_epoch_date_format(args.get("lastFixed")), "last_found": relational_date_to_epoch_date_format(args.get("lastFound")), "network_id": args.get("networkId"), "plugin_id": [arg_to_number(x) for x in argToList(args.get("pluginId"))], "plugin_type": args.get("pluginType"), "severity": argToList(args.get("severity")), "since": relational_date_to_epoch_date_format(args.get("since")), "state": argToList(args.get("state")), "vpr_score": build_vpr_score(args), }, } ) if tag_category and tag_value: if request_params.get("filters"): request_params.get("filters")[f"tag.{tag_category}"] = tag_value else: request_params["filters"] = {f"tag.{tag_category}": tag_value} if (tag_category and not tag_value) or (not tag_category and tag_value): raise DemistoException("Please specify tagCategory and tagValue") demisto.debug("request params export vulnerabilities", request_params) api_response = export_request(request_params, "vulns") export_uuid = api_response.get("export_uuid") demisto.debug(f"export_uuid: {export_uuid}") return PollResult( response=None, partial_result=CommandResults( outputs_prefix="TenableIO.Vulnerability", readable_output="Waiting for export vulnerabilities to finish...", ), continue_to_poll=True, args_for_next_run={"exportUuid": export_uuid, **args}, ) @polling_function( name=demisto.command(), timeout=arg_to_number(demisto.args().get("timeOut")) or DEFAULT_POLLING_TIMEOUT, interval=arg_to_number(demisto.args().get("intervalInSeconds")) or DEFAULT_POLLING_INTERVAL, requires_polling_arg=False, ) def export_assets_command(args: Dict[str, Any]) -> PollResult: """ Polling command to export_assets. After the first run, progress will be shown through the status QUEUED, PROCESSING, CANCELED, ERROR and FINISHED. Export assets command will run till its status is 'FINISHED'. Args: args (Dict[str, Any]): Arguments passed down by the CLI to provide in the HTTP request. Returns: PollResult: A result to return to the user which will be set as a CommandResults. The result itself will depend on the stage of polling. """ export_uuid = demisto.args().get("exportUuid") # pylint: disable=W9017 if export_uuid: demisto.debug(f"export_uuid: {export_uuid}") export_uuid_status_response = export_request_with_export_uuid(export_uuid, "assets") status = export_uuid_status_response.get("status") if status == "FINISHED": chunks_details_list = get_export_chunks_details(export_uuid_status_response, export_uuid, "assets") command_results = export_assets_build_command_result(chunks_details_list) return PollResult(command_results) elif status in ("PROCESSING", "QUEUED"): return PollResult( response=None, partial_result=CommandResults( outputs_prefix="TenableIO.Asset", outputs_key_field="id", readable_output="Waiting for export assets to finish...", ), continue_to_poll=True, args_for_next_run={"exportUuid": export_uuid, "status": status, **args}, ) else: return PollResult( response=CommandResults( outputs_key_field="id", outputs_prefix="TenableIO.Asset", readable_output=f"TenableIO: {status}", ), continue_to_poll=False, ) else: return request_uuid_export_assets(args) def export_vulnerabilities_build_command_result(chunks_details_list: list[dict]) -> CommandResults: """Builds command result object from chunks details list Args: chunks_details_list (list[dict]): a list[dict] of assets details. Returns: CommandResults: Command Results object with the relevant data. """ headers = [ "ASSET ID", "ASSET NAME", "IPV4 ADDRESS", "OPERATING SYSTEM", "SYSTEM TYPE", "DNS NAME (FQDN)", "SEVERITY", "PLUGIN ID", "PLUGIN NAME", "VULNERABILITY PRIORITY RATING", "CVSSV2 BASE SCORECVE", "PROTOCOL", "PORT", "FIRST SEEN", "LAST SEEN", "DESCRIPTION", "SOLUTION", ] human_readable = [] for chunk_details in chunks_details_list: asset_details = chunk_details.get("asset") plugin_details = chunk_details.get("plugin") port_details = chunk_details.get("port") human_readable_to_append = {} if asset_details: human_readable_to_append.update( { "ASSET ID": asset_details.get("uuid"), "ASSET NAME": asset_details.get("hostname"), "IPV4 ADDRESS": asset_details.get("ipv4"), "OPERATING SYSTEM": asset_details.get("operating_system"), "SYSTEM TYPE": asset_details.get("device_type"), "DNS NAME (FQDN)": asset_details.get("fqdn"), } ) if plugin_details: human_readable_to_append.update( { "PLUGIN ID": plugin_details.get("id"), "PLUGIN NAME": plugin_details.get("name"), "VULNERABILITY PRIORITY RATING": plugin_details.get("vpr").get("score") if plugin_details.get("vpr") else None, "CVSSV2 BASE SCORE": plugin_details.get("cvss_base_score"), "CVE": plugin_details.get("cve"), "DESCRIPTION": plugin_details.get("description"), "SOLUTION": plugin_details.get("solution"), } ) if port_details: human_readable_to_append.update({"PORT": port_details.get("port"), "PROTOCOL": port_details.get("protocol")}) human_readable_to_append.update( { "SEVERITY": chunk_details.get("severity"), "FIRST SEEN": chunk_details.get("first_found"), "LAST SEEN": chunk_details.get("last_found"), } ) remove_nulls_from_dictionary(chunk_details) human_readable.append(human_readable_to_append) return CommandResults( outputs_prefix="TenableIO.Vulnerability", outputs=chunks_details_list, raw_response=chunks_details_list, readable_output=tableToMarkdown("Vulnerabilities", human_readable, headers=headers, removeNull=True), ) def validate_range(range: Optional[str]) -> tuple[Optional[float], Optional[float]]: """ Validates the vprScoreRange argument for export asset command Args: range (str): A str represents a range for example 3-5. Returns: Range if valid else raise DemistoException. """ if range: nums = tuple(map(float, range.split("-"))) if len(nums) != 2 or not 0.1 <= nums[0] <= nums[1] <= 10.0: raise DemistoException("Please specify a valid vprScoreRange. The VPR values range is 0.1-10.0.") return nums # type: ignore return None, None @polling_function( name=demisto.command(), timeout=arg_to_number(demisto.args().get("timeOut")) or DEFAULT_POLLING_TIMEOUT, interval=arg_to_number(demisto.args().get("intervalInSeconds")) or DEFAULT_POLLING_INTERVAL, requires_polling_arg=False, ) def export_vulnerabilities_command(args: Dict[str, Any]) -> PollResult: """ Polling command to export vulnerabilities. After the first run, progress will be shown through the status QUEUED, PROCESSING, CANCELED, ERROR and FINISHED. Export vulnerabilities command will run till its status is 'FINISHED' and all the data chunks are exoprted. Args: args (Dict[str, Any]): Arguments passed down by the CLI to provide in the HTTP request. Returns: PollResult: A result to return to the user which will be set as a CommandResults. The result itself will depend on the stage of polling. """ export_uuid = demisto.args().get("exportUuid") # pylint: disable=W9017 if export_uuid: demisto.debug(f"export_uuid: {export_uuid}") export_uuid_status_response = export_request_with_export_uuid(export_uuid, "vulns") status = export_uuid_status_response.get("status") if status == "FINISHED": chunks_details_list = get_export_chunks_details(export_uuid_status_response, export_uuid, "vulns") command_results = export_vulnerabilities_build_command_result(chunks_details_list) return PollResult(command_results) elif status in ("PROCESSING", "QUEUED"): return PollResult( response=None, partial_result=CommandResults( outputs_prefix="TenableIO.Vulnerability", readable_output="Waiting for export vulnerabilities to finish...", ), continue_to_poll=True, args_for_next_run={"exportUuid": export_uuid, "status": status, **args}, ) else: return PollResult( response=CommandResults( outputs_prefix="TenableIO.Vulnerability", readable_output=f"TenableIO: {status}", ), continue_to_poll=False, ) else: return request_uuid_export_vulnerabilities(args) def scan_filters_human_readable(filters: list) -> str: context_to_hr = { "name": "Filter name", "readable_name": "Filter Readable name", "type": "Filter Control type", "regex": "Filter regex", "readable_regex": "Readable regex", "operators": "Filter operators", "group_name": "Filter group name", } return tableToMarkdown( "Tenable IO Scan Filters", [d | d.get("control", {}) for d in filters], headers=list(context_to_hr), headerTransform=context_to_hr.get, removeNull=True, ) def list_scan_filters_command(client: Client) -> CommandResults: response_dict = client.list_scan_filters() filters = response_dict.get("filters", []) return CommandResults( outputs_prefix="TenableIO.ScanFilter", outputs_key_field="name", outputs=filters, readable_output=scan_filters_human_readable(filters), raw_response=response_dict, ) def scan_history_readable(history: list) -> str: context_to_hr = { "id": "History id", "scan_uuid": "History uuid", "status": "Status", "is_archived": "Is archived", "custom": "Targets custom", "default": "Targets default", "visibility": "Visibility", "time_start": "Time start", "time_end": "Time end", } return tableToMarkdown( "Tenable IO Scan History", [d | d.get("targets", {}) for d in history], headers=list(context_to_hr), headerTransform=context_to_hr.get, removeNull=True, ) def scan_history_pagination_params(args: dict) -> dict: """ Generate pagination parameters for scanning history based on the given arguments. This function calculates the 'limit' and 'offset' parameters for pagination based on the provided 'page' and 'pageSize' arguments. If 'page' and 'pageSize' are valid integer values, the function returns a dictionary containing 'limit' and 'offset' calculated accordingly. If 'page' or 'pageSize' are not valid integers, the function falls back to using the 'limit' argument or defaults to 50 with an 'offset' of 0. Args: args (dict): The demisto.args() dictionary containing the optional arguments for pagination: 'page', 'pageSize', 'limit'. Returns: dict: A dictionary containing the calculated 'limit' and 'offset' parameters for pagination. """ page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("pageSize")) if isinstance(page, int) and isinstance(page_size, int): return {"limit": page_size, "offset": (page - 1) * page_size} else: return {"limit": args.get("limit", 50), "offset": 0} def scan_history_params(args: dict) -> dict: sort_fields = argToList(args.get("sortFields")) sort_order = argToList(args.get("sortOrder")) if len(sort_order) == 1: sort_order *= len(sort_fields) return { "sort": ",".join(f"{field}:{order}" for field, order in zip(sort_fields, sort_order)), "exclude_rollover": args["excludeRollover"], } | scan_history_pagination_params(args) def get_scan_history_command(args: dict[str, Any], client: Client) -> CommandResults: response_json = client.get_scan_history(args["scanId"], scan_history_params(args)) history = response_json.get("history", "") return CommandResults( outputs_prefix="TenableIO.ScanHistory", outputs_key_field="id", outputs=history, readable_output=scan_history_readable(history), ) def build_filters(filters) -> dict: # pylint: disable=W9014 """ Build a dictionary of filter information from a filters string. Args: filters (str, optional): A string containing filters in the format "name quality value" separated by commas. Escaped commas (\\,) and spaces (\\s) are treated as literal characters. Defaults to None. Returns: dict: A dictionary where keys are in the format 'filter.i.filter', 'filter.i.quality', and 'filter.i.value', and values correspond to the name, quality, and value of each filter component. Example: filters = "name1 good value1\\,with\\,commas, name2\\swith\\sspaces excellent value2" result = build_filters(filters) # Output: # { # 'filter.0.filter': 'name1', # 'filter.0.quality': 'good', # 'filter.0.value': 'value1,with,commas', # 'filter.1.filter': 'name2 with spaces', # 'filter.1.quality': 'excellent', # 'filter.1.value': 'value2' # } """ if not filters: return {} # split by comma without escaped commas split_filters = re.split(r"(?<!\\),", filters) # remove delimiters and split into name, quality and value filters = (f.replace("\\,", ",").split() for f in split_filters) result: dict = {} for i, (name, quality, value) in enumerate(filters): result |= { f"filter.{i}.filter": re.sub(r"(?<!\\)\\s", " ", name), f"filter.{i}.quality": re.sub(r"(?<!\\)\\s", " ", quality), f"filter.{i}.value": re.sub(r"(?<!\\)\\s", " ", value), } return result def export_scan_body(args: dict) -> dict: if chapters := args.get("chapters"): chapters = ";".join(argToList(chapters)) elif args["format"] in ("PDF", "HTML"): raise DemistoException('The "chapters" field must be provided for PDF or HTML formats.') body = { "format": args["format"].lower(), "chapters": chapters, "filter.search_type": args["filterSearchType"].lower(), "asset_id": args.get("assetId"), } | build_filters(args.get("filter")) return body def initiate_export_scan(args: dict, client: Client) -> str: return client.initiate_export_scan( args["scanId"], params={"history_id": args.get("historyId"), "history_uuid": args.get("historyUuid")}, body=export_scan_body(args), ).get("file", "") @polling_function("tenable-io-export-scan", poll_message="Preparing scan report:", interval=15, requires_polling_arg=False) def export_scan_command(args: dict[str, Any], client: Client) -> PollResult: """ Calls three endpoints. The first (called with initiate_export_scan) initiates an export and returns a file ID. The second (called with client.check_export_scan_status) checks the status of the export and the function polls until the status is 'ready'. The third endpoint is then called (with client.download_export_scan) which downloads the file and returns a dict with it's contents (using fileResult). """ scan_id = args["scanId"] # pylint: disable=W9019 file_id = args.get("fileId") or initiate_export_scan(args, client) demisto.debug(f"{file_id=}") status_response = client.check_export_scan_status(scan_id, file_id) demisto.debug(f"{status_response=}") match status_response.get("status"): case "ready": return PollResult(client.download_export_scan(scan_id, file_id, args["format"]), continue_to_poll=False) case "loading": return PollResult( None, continue_to_poll=True, args_for_next_run={ "fileId": file_id, "scanId": scan_id, "format": args["format"], # not necessary but avoids confusion }, ) case _: raise DemistoException( "Tenable IO encountered an error while exporting the scan report file.\n" f"Scan ID: {scan_id}\n" f"File ID: {file_id}\n" ) def get_audit_logs_command( client: Client, from_date: Optional[str] = None, to_date: Optional[str] = None, actor_id: Optional[str] = None, target_id: Optional[str] = None, limit: Optional[int] = None, ): """ Args: client: Client class object. from_date: date to fetch audit logs from. to_date: date which until to fetch audit logs. actor_id: fetch audit logs with matching actor id. target_id:fetch audit logs with matching target id. limit: limit number of audit logs to get. Returns: CommandResults of audit logs from API. """ audit_logs = client.get_audit_logs_request( from_date=from_date, to_date=to_date, actor_id=actor_id, target_id=target_id, limit=limit ) readable_output = tableToMarkdown("Audit Logs List:", audit_logs, removeNull=True, headerTransform=string_to_table_header) results = CommandResults(readable_output=readable_output, raw_response=audit_logs) return results, audit_logs """ FETCH COMMANDS """ def set_index_audit_logs(dt_now: datetime, dt_start_date: datetime, audit_logs: List[dict], last_index_fetched: int) -> int: """ This function set the new index_audit_logs by the following logic: 1. if dt_now > dt_start_date that means we're starting a new day (the fetch is per day, so we need to restart the index). 2. same day with new audit_logs - adding the amount of the new events to the exists index. 3. same day without new audit_logs - leave the index as the same. Args: dt_now: the current datetime dt_start_date: the start day to fetch in the current cycle audit_logs: the audit logs are retrieved in this cycle of fetch last_index_fetched: the last index from the previous cycle Returns: The new last index fetched """ if dt_now > dt_start_date: return 0 elif audit_logs: return len(audit_logs) + last_index_fetched else: return last_index_fetched def fetch_events_command(client: Client, first_fetch: datetime, last_run: dict, limit: int = 1000): """ Fetches audit logs. Args: client: Client class object. first_fetch: time to first fetch from. last_run: last run object. limit: number of audit logs to max fetch. Returns: vulnerabilities, audit logs and updated last run object """ last_fetch = last_run.get("last_fetch_time") last_index_fetched = last_run.get("index_audit_logs", 0) if not last_fetch: start_date = first_fetch.strftime(DATE_FORMAT) else: start_date = last_fetch # type: ignore audit_logs: List[dict] = [] audit_logs_from_api = client.get_audit_logs_request(from_date=start_date) demisto.debug(f"got {len(audit_logs_from_api)} events from api") if last_index_fetched < len(audit_logs_from_api): audit_logs.extend(audit_logs_from_api[last_index_fetched : last_index_fetched + limit]) for audit_log in audit_logs: audit_log["_time"] = audit_log.get("received") or audit_log.get("indexed") # creating date now as a string and as a datetime object for comparing date_now_as_str = datetime.utcnow().date().strftime(DATE_FORMAT) date_now_as_dt = datetime.strptime(date_now_as_str, DATE_FORMAT) start_date_as_dt = datetime.strptime(start_date, DATE_FORMAT) # convert back the start_date to datetime object for comparing demisto.debug(f"Tenable_io - {date_now_as_str=}, {start_date=}, {len(audit_logs)}, {last_index_fetched=}") index_audit_logs = set_index_audit_logs(date_now_as_dt, start_date_as_dt, audit_logs, last_index_fetched) demisto.debug(f"Tenable_io - {index_audit_logs=}") last_run.update({"index_audit_logs": index_audit_logs, "last_fetch_time": date_now_as_str}) demisto.info(f"Done fetching {len(audit_logs)} audit logs, Setting {last_run=}.") return audit_logs, last_run def fetch_assets_command(client: Client, assets_last_run): # pragma: no cover # pylint: disable=W9014 """ Fetches assets. Args: assets_last_run: last run object. client: Client class object. Returns: assets fetched from the API. """ assets = [] # if already in assets_last_run meaning its still polling chunks from api export_uuid = assets_last_run.get("assets_export_uuid") # if exists, still downloading chunks from prev fetch call available_chunks = assets_last_run.get("assets_available_chunks", []) if available_chunks: assets, assets_last_run = handle_assets_chunks(client, assets_last_run) elif export_uuid: status = get_asset_export_job_status(client=client, assets_last_run=assets_last_run) if status in ["PROCESSING", "QUEUED"]: assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) # set params for next run if status == "FINISHED": assets, assets_last_run = handle_assets_chunks(client, assets_last_run) elif status in ["CANCELLED", "ERROR"]: export_uuid = client.get_asset_export_uuid(fetch_from=round(get_timestamp(arg_to_datetime(ASSETS_FETCH_FROM)))) assets_last_run.update({"assets_export_uuid": export_uuid}) assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) demisto.info(f"Done fetching {len(assets)} assets, {assets_last_run=}.") return assets def run_assets_fetch(client, last_run): # pragma: no cover # pylint: disable=W9014 demisto.info("fetch assets from the API") # starting new fetch for assets, not polling from prev call if not last_run.get("assets_export_uuid"): generate_assets_export_uuid(client, last_run) return fetch_assets_command(client, last_run) def fetch_vulnerabilities(client: Client, assets_last_run: dict): # pragma: no cover """ Fetches vulnerabilities if job has succeeded. Args: last_run: last run object. client: Client class object. Returns: Vulnerabilities fetched from the API. """ vulnerabilities = [] # if already in assets_last_run meaning its still polling chunks from api export_uuid = assets_last_run.get("vuln_export_uuid") # if exists, still downloading chunks from prev fetch call available_chunks = assets_last_run.get("vulns_available_chunks", []) if available_chunks: vulnerabilities, assets_last_run = handle_vulns_chunks(client, assets_last_run) elif export_uuid: status = get_vulnerabilities_export_status(client=client, assets_last_run=assets_last_run) if status in ["PROCESSING", "QUEUED"]: assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) # set params for next run if status == "FINISHED": vulnerabilities, assets_last_run = handle_vulns_chunks(client, assets_last_run) elif status in ["CANCELLED", "ERROR"]: export_uuid = client.get_vuln_export_uuid( num_assets=ASSETS_NUMBER, last_found=get_timestamp(arg_to_datetime(VULNS_FETCH_FROM)) ) assets_last_run.update({"vuln_export_uuid": export_uuid}) assets_last_run.update({"nextTrigger": "30", "type": FETCH_COMMAND.get("assets")}) demisto.info(f"Done fetching {len(vulnerabilities)} vulnerabilities, {assets_last_run=}.") return vulnerabilities def run_vulnerabilities_fetch(client, last_run): # pylint: disable=W9014 demisto.info("fetch vulnerabilies from the API") if not last_run.get("vuln_export_uuid"): generate_export_uuid(client, last_run) return fetch_vulnerabilities(client, last_run) def skip_fetch_assets(last_run): # pragma: no cover # pylint: disable=W9014 time_to_check = last_run.get("assets_last_fetch") if not time_to_check: return False passed_time = (time.time() - time_to_check) / 60 to_skip = not (last_run.get("vuln_export_uuid") or last_run.get("assets_export_uuid")) and (passed_time < MIN_ASSETS_INTERVAL) if to_skip: demisto.info( f"Skipping fetch-assets command. Only {passed_time} minutes have passed since the last fetch. " f"It should be a minimum of 1 hour." ) return to_skip def is_assets_fetch_in_progress(last_run: dict) -> bool: """ Check if assets fetch is still in progress. Args: last_run: The last run object containing fetch state. Returns: bool: True if there are pending asset chunks or an active asset export job. """ return bool(last_run.get("assets_available_chunks") or last_run.get("assets_export_uuid")) def is_vulns_fetch_in_progress(last_run: dict) -> bool: """ Check if vulnerabilities fetch is still in progress. Args: last_run: The last run object containing fetch state. Returns: bool: True if there are pending vuln chunks or an active vuln export job. """ return bool(last_run.get("vulns_available_chunks") or last_run.get("vuln_export_uuid")) def should_seal_empty_assets_snapshot(assets: list, assets_fetch_in_progress: bool, assets_last_run: dict) -> bool: """ Decide whether the assets snapshot should be sealed with an empty payload on this run. This guards against the XSUP-71765 regression: after the assets export finished and the assets were committed to XSIAM, subsequent fetch-assets runs (which happen every fetch interval while the vulnerabilities export is still processing) returned an empty assets list and re-sealed the already-sealed snapshot with an empty payload. Re-sealing an already-committed snapshot with empty data wipes the committed assets from the dataset. The empty seal must happen exactly once per snapshot, only when: - no new assets were fetched this run, and - the assets export is complete (not in progress), and - there is a committed snapshot to seal (cumulative total > 0), and - the snapshot has not already been sealed this cycle. Args: assets: The assets fetched on the current run. assets_fetch_in_progress: Whether the assets export still has pending work. assets_last_run: The assets last run state object. Returns: bool: True if the snapshot should be sealed with an empty payload now. """ if assets or assets_fetch_in_progress: return False if assets_last_run.get("assets_snapshot_sealed"): return False return assets_last_run.get("total_assets", 0) > 0 def parse_vulnerabilities(vulns): # pylint: disable=W9014 demisto.debug("Parse the vulnerabilities...") if not isinstance(vulns, list): demisto.debug(f"result is of type: {type(vulns)}") vulns = list(vulns) for vuln in vulns: vuln_str = json.dumps(vuln) if sys.getsizeof(vuln_str) > XSIAM_EVENT_CHUNK_SIZE_LIMIT: demisto.debug(f"found object with size: {sys.getsizeof(sys.getsizeof(vuln_str))}") if vuln.get("output"): demisto.debug("replacing output key") vuln["output"] = "" vuln["isTruncated"] = True else: demisto.debug("skipping object...") continue else: vuln["isTruncated"] = False return vulns def main(): # pragma: no cover # pylint: disable=W9018 """main function, parses params and runs command functions""" args = demisto.args() command = demisto.command() params = demisto.params() access_key = params.get("credentials_access_key", {}).get("password") or params.get("access-key") secret_key = params.get("credentials_secret_key", {}).get("password") or params.get("secret-key") url = params.get("url") verify_certificate = not params.get("unsecure", False) proxy = params.get("proxy", False) # Events Params max_fetch = arg_to_number(params.get("max_fetch")) or 1000 first_fetch: datetime = arg_to_datetime(params.get("first_fetch", "3 days")) # type: ignore demisto.debug(f"Command being called is {command}") try: headers = {"X-ApiKeys": f"accessKey={access_key}; secretKey={secret_key}", "Accept": "application/json"} client = Client(base_url=url, verify=verify_certificate, headers=headers, proxy=proxy) if command == "test-module": demisto.results(test_module(client, params)) # pylint: disable=W9008 elif command == "tenable-io-list-scans": demisto.results(get_scans_command()) # pylint: disable=W9008 elif command == "tenable-io-launch-scan": demisto.results(launch_scan_command()) # pylint: disable=W9008 elif command == "tenable-io-get-scan-report": demisto.results(get_report_command()) # pylint: disable=W9008 elif command == "tenable-io-get-vulnerability-details": demisto.results(get_vulnerability_details_command()) # pylint: disable=W9008 elif command == "tenable-io-get-vulnerabilities-by-asset": demisto.results(get_vulnerabilities_by_asset_command()) # pylint: disable=W9008 elif command == "tenable-io-get-scan-status": demisto.results(get_scan_status_command()) # pylint: disable=W9008 elif command == "tenable-io-pause-scan": demisto.results(pause_scan_command()) # pylint: disable=W9008 elif command == "tenable-io-resume-scan": demisto.results(resume_scan_command()) # pylint: disable=W9008 elif command == "tenable-io-get-asset-details": return_results(get_asset_details_command()) elif command == "tenable-io-export-assets": return_results(export_assets_command(args)) elif command == "tenable-io-export-vulnerabilities": vulnerabilities: list = [] results = export_vulnerabilities_command(args) if isinstance(results, CommandResults) and results.raw_response: # pylint: disable=E1101 vulnerabilities = results.raw_response # type: ignore # pylint: disable=E1101 return_results(results) if argToBoolean(args.get("should_push_events", "false")) and (is_xsiam() or is_platform()): send_data_to_xsiam(vulnerabilities, product=f"{PRODUCT}_vulnerabilities", vendor=VENDOR) elif command == "tenable-io-list-scan-filters": return_results(list_scan_filters_command(client)) elif command == "tenable-io-get-scan-history": return_results(get_scan_history_command(args, client)) elif command == "tenable-io-export-scan": return_results(export_scan_command(args, client)) elif command == "tenable-io-get-audit-logs": results, events = get_audit_logs_command( client, from_date=args.get("from_date"), to_date=args.get("to_date"), actor_id=args.get("actor_id"), target_id=args.get("target_id"), limit=args.get("limit"), ) return_results(results) if argToBoolean(args.get("should_push_events", "false")) and (is_xsiam() or is_platform()): send_data_to_xsiam(events, vendor=VENDOR, product=PRODUCT) # Fetch Commands elif command == "fetch-events": last_run = demisto.getLastRun() demisto.debug(f"saved lastrun events: {last_run}") events, new_last_run = fetch_events_command(client, first_fetch, last_run, max_fetch) send_data_to_xsiam(events, vendor=VENDOR, product=PRODUCT) demisto.debug(f"new lastrun events: {last_run}") demisto.setLastRun(new_last_run) elif command == "fetch-assets": assets = [] vulnerabilities = [] assets_last_run = demisto.getAssetsLastRun() demisto.debug(f"saved lastrun assets: {assets_last_run}") assets_last_run_copy = assets_last_run.copy() if skip_fetch_assets(assets_last_run): return elif not (assets_last_run.get("vuln_export_uuid") or assets_last_run.get("assets_export_uuid")): # starting a whole new fetch process for assets demisto.debug("starting new fetch") assets_last_run.update({"assets_last_fetch": time.time()}) # Determine which stages should run on this invocation BEFORE fetching anything. # These flags are computed from the pre-fetch state (assets_last_run_copy) so that # the assets and vulnerabilities stages are decided independently and consistently, # even though they are now executed sequentially (assets fully sent and freed before # vulnerabilities are fetched) to avoid holding both large datasets in memory at once # (XSUP-73037 OOM). should_fetch_assets = is_assets_fetch_in_progress(assets_last_run_copy) or not is_vulns_fetch_in_progress( assets_last_run_copy ) should_fetch_vulns = is_vulns_fetch_in_progress(assets_last_run_copy) or not is_assets_fetch_in_progress( assets_last_run_copy ) # Fetch Assets: Run if there's an ongoing assets export OR if starting a new fetch cycle if should_fetch_assets: assets = run_assets_fetch(client, assets_last_run) demisto.info(f"Received {len(assets)} assets.") demisto.debug(f"new lastrun assets: {assets_last_run}") demisto.setAssetsLastRun(assets_last_run) # Get snapshot_id for this fetch cycle # Fallback to generate_snapshot_id() if not in last_run - this handles edge cases # where the fetch cycle starts without a stored snapshot_id (e.g., first run or reset) snapshot_id = assets_last_run.get("snapshot_id") if not snapshot_id: snapshot_id = generate_snapshot_id() assets_last_run["snapshot_id"] = snapshot_id demisto.setAssetsLastRun(assets_last_run) # Check if assets fetch is still in progress (has more asset chunks or asset export job pending) # Vulnerabilities are separate and don't affect the assets snapshot completion assets_fetch_in_progress = is_assets_fetch_in_progress(assets_last_run) # Remember whether assets were fetched this run before we free the list below, # so downstream conditions (e.g. module health update) keep their original meaning. fetched_assets_this_run = bool(assets) if assets: # Calculate cumulative total BEFORE sending to XSIAM # Per the Fetch Assets Development Flow doc (Case 2 - Continuation Iteration): # items_count = 1 if not finished pulling all results, # otherwise items_count = last_run["total_assets"] + len(assets) cumulative_total = assets_last_run.get("total_assets", 0) + len(assets) items_count = 1 if assets_fetch_in_progress else cumulative_total demisto.debug( f"sending {len(assets)} assets to XSIAM with snapshot_id={snapshot_id}, " f"items_count={items_count}, cumulative_total={cumulative_total}, " f"assets_fetch_in_progress={assets_fetch_in_progress}" ) send_data_to_xsiam( data=assets, vendor=VENDOR, product=f"{PRODUCT}_assets", data_type="assets", snapshot_id=snapshot_id, items_count=str(items_count), # Disable automatic health module update here because we update it separately # below with the cumulative total_assets count across all fetch cycles, # rather than just the count from this single batch should_update_health_module=False, ) # Update cumulative asset count AFTER successful send_data_to_xsiam() # This ensures the counter only reflects assets that were actually sent to XSIAM assets_last_run["total_assets"] = cumulative_total # If this send completed the assets snapshot (no more asset work pending), mark it # as sealed so subsequent runs that only drain vulnerabilities do not re-seal it # with an empty payload (XSUP-71765). if not assets_fetch_in_progress: assets_last_run["assets_snapshot_sealed"] = True demisto.setAssetsLastRun(assets_last_run) elif should_seal_empty_assets_snapshot(assets, assets_fetch_in_progress, assets_last_run): # Asset fetch completed but this run returned an empty list of assets, and the # snapshot has not been sealed yet. Seal it once by sending an empty payload with # the final items_count so XSIAM knows the snapshot is complete. cumulative_total = assets_last_run.get("total_assets", 0) demisto.debug( f"[Fetch] Asset fetch completed with empty assets list. Sealing snapshot with " f"snapshot_id={snapshot_id}, items_count={cumulative_total}" ) send_data_to_xsiam( data=[], vendor=VENDOR, product=f"{PRODUCT}_assets", data_type="assets", snapshot_id=snapshot_id, items_count=str(cumulative_total), should_update_health_module=False, ) # Mark the snapshot as sealed so it is not re-sealed on the following runs # while the vulnerabilities export is still in progress (XSUP-71765). assets_last_run["assets_snapshot_sealed"] = True demisto.setAssetsLastRun(assets_last_run) elif not assets_fetch_in_progress: # Asset fetch is complete but there is nothing to seal: either nothing was ever # committed this cycle (cumulative_total == 0) or the snapshot was already sealed # on a previous run. Do NOT re-send an empty payload for an already-sealed # snapshot - doing so overwrites the committed assets in XSIAM (XSUP-71765). demisto.debug( f"[Fetch] No assets to send and snapshot already sealed or nothing committed " f"(snapshot_id={snapshot_id}, " f"total_assets={assets_last_run.get('total_assets', 0)}, " f"assets_snapshot_sealed={assets_last_run.get('assets_snapshot_sealed', False)}). " f"Skipping snapshot seal." ) # Release the assets from memory now that they have been sent to XSIAM, BEFORE fetching # vulnerabilities. Assets and vulnerabilities are each up to tens of thousands of records; # holding both in memory at once previously drove the fetch-assets container past its # memory limit and caused an OOM kill mid-run, so vulnerabilities were never sent and the # vulnerabilities dataset was never created (XSUP-73037). Freeing here caps the peak # footprint at roughly one dataset at a time. del assets gc.collect() # Fetch Vulnerabilities: Run if there's an ongoing vulns export OR if assets fetch is complete. # Executed only after assets have been sent and freed above. if should_fetch_vulns: vulnerabilities = run_vulnerabilities_fetch(client, last_run=assets_last_run) demisto.info(f"Received {len(vulnerabilities)} vulnerabilities.") demisto.debug(f"new lastrun assets after vulns fetch: {assets_last_run}") demisto.setAssetsLastRun(assets_last_run) if vulnerabilities: vulnerabilities = parse_vulnerabilities(vulnerabilities) demisto.debug(f"sending {len(vulnerabilities)} vulnerabilities to XSIAM.") send_data_to_xsiam(data=vulnerabilities, vendor=VENDOR, product=f"{PRODUCT}_vulnerabilities") # Release the vulnerabilities from memory once sent, mirroring the assets handling above. del vulnerabilities gc.collect() # Update module health separately to show the number of assets pulled. # Uses fetched_assets_this_run (captured before the assets list was freed above) # to preserve the original "fetched assets this run OR assets fetch complete" semantics. cumulative_total = assets_last_run.get("total_assets", 0) if fetched_assets_this_run or not assets_fetch_in_progress: demisto.updateModuleHealth({"assetsPulled": cumulative_total}) # Clean up snapshot state when the entire fetch cycle is complete # (both assets and vulnerabilities are done). This must happen AFTER # the snapshot has been sealed above, not in handle_vulns_chunks(). vulns_fetch_in_progress = is_vulns_fetch_in_progress(assets_last_run) if not assets_fetch_in_progress and not vulns_fetch_in_progress: demisto.debug( "Entire fetch cycle complete (assets + vulns). " "Cleaning up snapshot_id and total_assets for next cycle." ) assets_last_run.pop("snapshot_id", None) assets_last_run.pop("total_assets", None) assets_last_run.pop("assets_snapshot_sealed", None) demisto.setAssetsLastRun(assets_last_run) demisto.info("Done Sending data to XSIAM.") except Exception as e: return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}") if __name__ in ["__main__", "builtin", "builtins"]: main()