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

IDTenable.io
ProviderTenable
CategoryVulnerability Management
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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

  1. 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.
  1. 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
  2. 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,
nmatch
plugin.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 key
  • credentials_access_key — (required)
  • secret-key — Secret key
  • credentials_secret_key — (required)
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchEvents — Fetch events
  • isFetchAssets — Fetch assets and vulnerabilities
  • first_fetch — Events first fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • max_fetch — Events max fetch
  • eventFetchInterval — Events Fetch Interval
  • assetsFetchInterval — Assets and vulnerabilities fetch interval

Commands (15)

  • tenable-io-export-assets

    Retrieves details for the specified asset to include custom attributes.

  • 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).

  • tenable-io-export-vulnerabilities

    Retrieves details for the specified asset to include custom attributes.

  • tenable-io-get-asset-details

    Retrieves details for the specified asset including custom attributes.

  • tenable-io-get-audit-logs

    Returns audit logs extracted from Tenable io.

  • tenable-io-get-scan-history

    Lists the individual runs of the specified scan.

  • tenable-io-get-scan-report

    Retrieves a scan report for the specified scan.

  • 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).

  • tenable-io-get-vulnerabilities-by-asset

    Gets a list of up to 5000 of the vulnerabilities recorded for a specified asset.

  • tenable-io-get-vulnerability-details

    Retrieves details for the specified vulnerability.

  • tenable-io-launch-scan

    Launches a scan with existing or custom targets. You can specify custom targets in the command arguments.

  • tenable-io-list-scan-filters

    Lists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them.

  • tenable-io-list-scans

    Retrieves scans from the Tenable platform.

  • tenable-io-pause-scan

    Pauses all scans inputted as an array. Will pause scans whose status is 'Running'.

  • tenable-io-resume-scan

    Resumes 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()