Tenable.sc

With Tenable.sc (formerly SecurityCenter) you get a real-time, continuous assessment of your security posture so you can find and fix vulnerabilities faster.

Vulnerability Management · Tenable.sc

Details

IDTenable.sc
ProviderTenable
CategoryVulnerability Management
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM Exposure Management

README

Use the Tenable.sc integration to get a real-time, continuous assessment of your security posture so you can find and fix vulnerabilities faster.
All data in Tenable.sc is managed using group level permissions. If you have several groups, data (scans, scan results, assets, etc) can be viewable but not manageable. Users with Security Manager role can manage everything. These permissions come into play when multiple groups are in use.
It is important to know what data is manageable for the user in order to work with the integration.
This integration was integrated and tested with Tenable.sc v5.7.0.

Use cases

* Create and run scans.
* Launch and manage scan results and the found vulnerabilities.
* Create and view assets.
* View policies, repositories, credentials, users and more system information.
* View and real-time receiving of alerts.

Configure Tenable.sc in Cortex

Parameter Description Required
Server URL (e.g. https://192.168.0.1) The server URL. True
Access key See the help for instructions to generate the access key. False
Secret key   False
Username The Username is either admin or secman (depend on the role you want to log into) and your password to the tenable server. False
Password   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents   False
Incident type   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) The timestamp to start the fetch from. False

Commands

You can execute these commands from the 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-sc-list-scans


Requires security manager role. Get a list of Tenable.sc existing scans.

Base Command

tenable-sc-list-scans

Input

Argument Name Description Required
manageable Whether to return only manageable scans. Returns both usable and manageable scans by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.Scan.Name string Scan name.
TenableSC.Scan.ID number Scan ID.
TenableSC.Scan.Description string Scan description.
TenableSC.Scan.Policy string Scan policy name.
TenableSC.Scan.Group string Scan policy owner group name.
TenableSC.Scan.Owner string Scan policy owner user name.

Human Readable Output

Tenable.sc Scans

ID Name Description Policy Group Owner
3 test_scan_2023 Test scan Network Scan Full Access secman

tenable-sc-launch-scan


Requires security manager role. Launch an existing scan from Tenable.sc. Set polling to true to follow the scan and receive results when scan is over.

Base Command

tenable-sc-launch-scan

Input

Argument Name Description Required
scan_id Scan ID, can be retrieved from list-scans command. Required
diagnostic_target Valid IP/Hostname of a specific target to scan. Must be provided with diagnosticPassword. Optional
diagnostic_password Non empty string password. Optional
timeout_in_seconds Relevant only when polling is true. Default is 3 hours. The timeout in seconds until polling ends. Default is 10800. Optional
polling Default is false. When set to true, will keep polling results until scan is done and return the formatted scan results. Possible values are: true, false. Default is false. Optional
scan_results_id Deprecated. Scan results ID. Optional

Context Output

Path Type Description
TenableSC.ScanResults.Name string Scan name.
TenableSC.ScanResults.Status string Scan status.
TenableSC.ScanResults.ID string Scan Results ID.
TenableSC.ScanResults.OwnerID string Relevant only when polling is false. Scan owner ID.
TenableSC.ScanResults.JobID string Relevant only when polling is false. Job ID.
TenableSC.ScanResults.ScannedIPs number Relevant only when polling is true. Scan number of scanned IPs.
TenableSC.ScanResults.StartTime date Relevant only when polling is true. Scan start time.
TenableSC.ScanResults.EndTime date Relevant only when polling is true. Scan end time.
TenableSC.ScanResults.Checks number Relevant only when polling is true. Scan completed checks.
TenableSC.ScanResults.RepositoryName string Relevant only when polling is true. Scan repository name.
TenableSC.ScanResults.Description string Relevant only when polling is true. Scan description.
TenableSC.ScanResults.Vulnerability.ID number Relevant only when polling is true. Scan vulnerability ID.
TenableSC.ScanResults.Vulnerability.Name string Relevant only when polling is true. Scan vulnerability Name.
TenableSC.ScanResults.Vulnerability.Family string Relevant only when polling is true. Scan vulnerability family.
TenableSC.ScanResults.Vulnerability.Severity string Relevant only when polling is true. Scan vulnerability severity.
TenableSC.ScanResults.Vulnerability.Total number Relevant only when polling is true. Scan vulnerability total hosts.
TenableSC.ScanResults.Policy string Relevant only when polling is true. Scan policy.
TenableSC.ScanResults.Group string Relevant only when polling is true. Scan owner group name.
TenableSC.ScanResults.Owner string Relevant only when polling is true. Scan owner user name.
TenableSC.ScanResults.Duration number Relevant only when polling is true. Scan duration in minutes.
TenableSC.ScanResults.ImportTime date Relevant only when polling is true. Scan import time.

Human Readable Output

When polling is set to false:

Tenable.sc Scan

Name ID OwnerID JobID Status
test_scan_2023 169 38 118864 Queued

When polling is set to true:

Tenable.sc Scan 130 Report

ID Name Description Policy Group Owner ScannedIPs StartTime EndTime Duration Checks ImportTime RepositoryName Status Scan Type Completed IPs
130 test_scan_2023 Test scan 2023 Network Scan Full Access hayun_test_sec_man 156 2023-05-16T12:18:10Z 2023-05-16T17:20:00Z 301.8333333333333 22649640 2023-05-16T17:20:02Z Local Completed regular 156

tenable-sc-get-vulnerability


Requires security manager role. Get details about a given vulnerability from a given Tenable.sc scan.

Base Command

tenable-sc-get-vulnerability

Input

Argument Name Description Required
vulnerability_id Vulnerability ID from the scan-report command. Required
scan_results_id Scan results ID from the scan-report command. Optional
query_id Can be created via the Tenable.sc UI > Analysis > queries. Can be retrieved from the tenable-sc-list-query command. Optional
sort_direction The direction in which the results should be sorted. Requires companion parameter, sort_field. Possible values are: ASC, DESC. Default is ASC. Optional
sort_field Which field to sort by, For vulnerabilities data, Tenable recommends you sort by severity. Default is severity. Optional
source_type When the source_type is “individual”, a scan_results_id must be provided, otherwise “query_id” must be provided. cumulative — Analyzes cumulative vulnerabilities. patched — Analyzes mitigated vulnerabilities. Possible values are: individual, cumulative, patched. Default is individual. Optional
limit The number of objects to return in one response (maximum limit is 200). Default is 50. Optional
page The page to return, starting from 0. Default is 0. Optional

Context Output

Path Type Description
TenableSC.ScanResults.ID number Scan results ID.
TenableSC.ScanResults.Vulnerability.ID number Vulnerability plugin ID.
TenableSC.ScanResults.Vulnerability.Name string Vulnerability name.
TenableSC.ScanResults.Vulnerability.Description string Vulnerability description.
TenableSC.ScanResults.Vulnerability.Type string Vulnerability type.
TenableSC.ScanResults.Vulnerability.Severity string Vulnerability Severity.
TenableSC.ScanResults.Vulnerability.Synopsis string Vulnerability Synopsis.
TenableSC.ScanResults.Vulnerability.Solution string Vulnerability Solution.
TenableSC.ScanResults.Vulnerability.Published date Vulnerability publish date.
TenableSC.ScanResults.Vulnerability.CPE string Vulnerability CPE.
TenableSC.ScanResults.Vulnerability.CVE Unknown Vulnerability CVE.
TenableSC.ScanResults.Vulnerability.ExploitAvailable boolean Vulnerability exploit available.
TenableSC.ScanResults.Vulnerability.ExploitEase string Vulnerability exploit ease.
TenableSC.ScanResults.Vulnerability.RiskFactor string Vulnerability risk factor.
TenableSC.ScanResults.Vulnerability.CVSSBaseScore number Vulnerability CVSS base score.
TenableSC.ScanResults.Vulnerability.CVSSTemporalScore number Vulnerability CVSS temporal score.
TenableSC.ScanResults.Vulnerability.CVSSVector string Vulnerability CVSS vector.
TenableSC.ScanResults.Vulnerability.PluginDetails Unknown Vulnerability plugin details.
CVE.ID Unknown CVE ID.
TenableSC.ScanResults.Vulnerability.Host.IP string Vulnerability Host IP.
TenableSC.ScanResults.Vulnerability.Host.MAC string Vulnerability Host MAC.
TenableSC.ScanResults.Vulnerability.Host.Port number Vulnerability Host Port.
TenableSC.ScanResults.Vulnerability.Host.Protocol string Vulnerability Host Protocol.

Human Readable Output

Vulnerability: FTP Server Detection (10092)

Synopsis

An FTP server is listening on a remote port.

Description

It is possible to obtain the banner of the remote FTP server by connecting to a remote port.

Solution

Hosts

IP MAC Port Protocol
{IP} {MAC} 21 TCP

Risk Information

RiskFactor
None

Exploit Information

ExploitAvailable
false

Plugin Details

CheckType Family Modified Published
remote Service detection 2019-11-22T17:00:00Z 1999-10-12T16:00:00Z

Vulnerability Information

No entries.

tenable-sc-get-scan-status


Requires security manager role. Get the status of a specific scan in Tenable.sc.

Base Command

tenable-sc-get-scan-status

Input

Argument Name Description Required
scan_results_id Scan results ID from the launch-scan command. Required

Context Output

Path Type Description
TenableSC.ScanResults.Status string Scan status.
TenableSC.ScanResults.Name string Scan Name.
TenableSC.ScanResults.Description string Scan description.
TenableSC.ScanResults.ID string Scan results ID.
TenableSC.ScanResults.Error string Will appear only in case of error in the scan, include the cause for the failure.

Human Readable Output

Tenable.sc Scan Status

ID Name Status Description
169 test_scan_2023 Running Test scan 2023

tenable-sc-get-scan-report


Requires security manager role. Get a single report with Tenable.sc scan results. In case of Importstatus = Error (The results import wasn’t finished), the vulnerabilities section will not be added to the results.

Base Command

tenable-sc-get-scan-report

Input

Argument Name Description Required
scan_results_id Scan results ID. Required
vulnerability_severity Comma-separated list of severity values of vulnerabilities to retrieve. Default is Critical,High,Medium,Low,Info. Optional

Context Output

Path Type Description
TenableSC.ScanResults.ID number Scan results ID.
TenableSC.ScanResults.Name string Scan name.
TenableSC.ScanResults.Status string Scan status.
TenableSC.ScanResults.ScannedIPs number Scan number of scanned IPs.
TenableSC.ScanResults.StartTime date Scan start time.
TenableSC.ScanResults.EndTime date Scan end time.
TenableSC.ScanResults.Checks number Scan completed checks.
TenableSC.ScanResults.RepositoryName string Scan repository name.
TenableSC.ScanResults.Description string Scan description.
TenableSC.ScanResults.Vulnerability.ID number Scan vulnerability ID.
TenableSC.ScanResults.Vulnerability.Name string Scan vulnerability Name.
TenableSC.ScanResults.Vulnerability.Family string Scan vulnerability family.
TenableSC.ScanResults.Vulnerability.Severity string Scan vulnerability severity.
TenableSC.ScanResults.Vulnerability.Total number Scan vulnerability total hosts.
TenableSC.ScanResults.Policy string Scan policy.
TenableSC.ScanResults.Group string Scan owner group name.
TenableSC.ScanResults.Owner string Scan owner user name.
TenableSC.ScanResults.Duration number Scan duration in minutes.
TenableSC.ScanResults.ImportTime date Scan import time.
TenableSC.ScanResults.IsScanRunning boolean Whether the scan is still running.
TenableSC.ScanResults.ImportStatus string Scan import status.

Human Readable Output

Tenable.sc Scan 150 Report

ID Name Policy Group Owner ScannedIPs StartTime EndTime Duration Checks ImportTime RepositoryName Status Scan Type Completed IPs
150 my_Test_scan Plugin #1 Full Access yuv 115 2023-05-18T13:12:51Z 2023-05-18T13:45:53Z 33.03333333333333 21275 2023-05-18T13:45:57Z Local Completed regular 115

Vulnerabilities

ID Name Family Severity Total
11219 Nessus SYN scanner Port scanners Info 109

tenable-sc-list-credentials


Requires security manager role. Get a list of Tenable.sc credentials.

Base Command

tenable-sc-list-credentials

Input

Argument Name Description Required
manageable Whether to return only manageable scan credentials. Returns both usable and manageable by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.Credential.Name string Credential name.
TenableSC.Credential.ID number Credential ID.
TenableSC.Credential.Description string Credential description.
TenableSC.Credential.Type string Credential type.
TenableSC.Credential.Tag string Credential tag.
TenableSC.Credential.Group string Credential owner group name.
TenableSC.Credential.Owner string Credential owner user name.
TenableSC.Credential.LastModified date Credential last modified time.

Human Readable Output

Tenable.sc Credentials

ID Name Type Group LastModified
1 Windows server windows   2023-02-14T11:44:12Z
2 SSH linux ssh   2023-02-15T09:11:10Z
3 Windows clients windows   2023-02-15T12:32:45Z

tenable-sc-list-policies


Requires security manager role. Get a list of Tenable.sc scan policies.

Base Command

tenable-sc-list-policies

Input

Argument Name Description Required
manageable Whether to return only manageable scan policies. Returns both usable and manageable by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.ScanPolicy.Name string Scan policy name.
TenableSC.ScanPolicy.ID number Scan policy ID.
TenableSC.ScanPolicy.Description string Scan policy description.
TenableSC.ScanPolicy.Tag string Scan policy tag.
TenableSC.ScanPolicy.Group string Scan policy owner group name.
TenableSC.ScanPolicy.Owner string Scan policy owner user name.
TenableSC.ScanPolicy.LastModified date Scan policy last modified time.
TenableSC.ScanPolicy.Type string Scan policy type.

Human Readable Output

Tenable.sc Scan Policies

ID Name Description Type Group Owner LastModified
1 Network Scan   Basic Network Scan     2023-02-09T14:58:26Z
2 D Advanced Scan D Advanced Scan Advanced Scan     2023-02-13T13:02:22Z

tenable-sc-list-report-definitions


Requires security manager role. Get a list of Tenable.sc report definitions.

Base Command

tenable-sc-list-report-definitions

Input

Argument Name Description Required
manageable Whether to return only manageable reports. Returns both usable and manageable by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.ReportDefinition.Name string Report definition name.
TenableSC.ReportDefinition.ID number Report definition ID.
TenableSC.ReportDefinition.Description string Report definition description.
TenableSC.ReportDefinition.Type string Report definition type.
TenableSC.ReportDefinition.Group string Report definition owner group name.
TenableSC.ReportDefinition.Owner string Report definition owner user name.

Human Readable Output

Tenable.sc Report Definitions

ID Name Description Type Group Owner
2 Critical and Exploitable Vulnerabilities Report Test pdf Full Access test

tenable-sc-list-repositories


Requires security manager role. Get a list of Tenable.sc scan repositories.

Base Command

tenable-sc-list-repositories

Input

There are no input arguments for this command.

Context Output

Path Type Description
TenableSC.ScanRepository.Name string Scan Repository name.
TenableSC.ScanRepository.ID number Scan Repository ID.
TenableSC.ScanRepository.Description string Scan Repository.

Human Readable Output

Tenable.sc Scan Repositories

ID Name
1 Local

tenable-sc-list-zones


Requires admin role. Get a list of Tenable.sc scan zones.

Base Command

tenable-sc-list-zones

Input

There are no input arguments for this command.

Context Output

Path Type Description
TenableSC.ScanZone.Name string Scan Zone name.
TenableSC.ScanZone.ID number Scan Zone ID.
TenableSC.ScanZone.Description string Scan Zone description.
TenableSC.ScanZone.IPList unknown Scan Zone IP list.
TenableSC.ScanZone.ActiveScanners number Scan Zone active scanners.
TenableSC.ScanZone.Scanner.Name string Scanner name.
TenableSC.ScanZone.Scanner.ID number Scanner ID.
TenableSC.ScanZone.Scanner.Description string Scanner description.
TenableSC.ScanZone.Scanner.Status number Scanner status.

Human Readable Output

Tenable.sc Scan Zones

ID Name IPList activeScanners
1 Default Scan Zone ip 1

Tenable.sc Scanners

ID Name Status
2 RHEL6 Scanner 1

tenable-sc-create-scan


Requires security manager role. Create a scan on Tenable.sc

Base Command

tenable-sc-create-scan

Input

Argument Name Description Required
name Scan name. Required
policy_id Policy ID, can be retrieved from the list-policies command. Required
plugin_id Plugin ID. Optional
description Scan description. Optional
repository_id Scan Repository ID. Can be retrieved from the list-repositories command. Required
zone_id Scan zone ID (default is all zones). Can be retrieved from the list-zones command. Optional
schedule Schedule for the scan. Possible values are: dependent, ical, never, rollover, now. Optional
asset_ids Either all assets or comma-separated asset IDs to scan. Can be retrieved from the list-assets command. Possible values are: All, AllManageable. Optional
scan_virtual_hosts Whether to include virtual hosts. Default is false. Possible values are: true, false. Optional
ip_list Comma-separated IPs to scan, e.g., 10.0.0.1,10.0.0.2 . Optional
report_ids Comma- separated list of report definition IDs to create post-scan. Can be retrieved from the list-report-definitions command. Optional
credentials Comma-separated credentials IDs to use. Can be retrieved from the list-credentials command. Optional
timeout_action Scan timeout action. Default is import. Possible values are: discard, import, rollover. Optional
max_scan_time Maximum scan run time in hours, Default is 1. Optional
dhcp_tracking Track hosts which have been issued new IP address, (e.g., DHCP). Possible values are: true, false. Optional
rollover_type Scan rollover type. Possible values are: nextDay. Optional
dependent_id Dependent scan ID in case of a dependent schedule. Can be retrieved from the list-scans command. Optional
time_zone The timezone for the given start_time, Possible values can be found here: https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html. Optional
start_time The scan start time in the format of YYYY-MM-DD:HH:MM:SS or relative timestamp (i.e., now, 3 days). Optional
repeat_rule_freq Specifies repeating events based on an interval of a repeat_rule_freq or more. Possible values are: HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY. Optional
repeat_rule_interval The number of repeat_rule_freq between each interval (for example: If repeat_rule_freq=DAILY and repeat_rule_interval=8 it means every eight days.). Optional
repeat_rule_by_day A comma-separated list of days of the week to run the schedule. Possible values are: SU,MO,TU,WE,TH,FR,SA. Optional
enabled The “enabled” field can only be set to “false” for schedules of type “ical”. For all other schedules types, “enabled” is set to “true”. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
TenableSC.Scan.ID string Scan ID.
TenableSC.Scan.CreatorID string Scan’s creator ID.
TenableSC.Scan.Name string Scan Name.
TenableSC.Scan.Type string Scan type.
TenableSC.Scan.CreatedTime date Scan creation time.
TenableSC.Scan.OwnerName string Scan owner Username.
TenableSC.Scan.Reports unknown Scan report definition IDs.

Human Readable Output

Scan created successfully

ID CreatorID Name Type CreationTime
70 39 my_name policy 2023-05-24T12:33:03Z

tenable-sc-delete-scan


Requires security manager role. Delete a scan in Tenable.sc.

Base Command

tenable-sc-delete-scan

Input

Argument Name Description Required
scan_id Scan ID. Can be retrieved from the the list-scans command. Required

Context Output

There is no context output for this command.

Human Readable Output

Scan {scan_id} was deleted successfully.

tenable-sc-list-assets


Requires security manager role. Get a list of Tenable.sc assets.

Base Command

tenable-sc-list-assets

Input

Argument Name Description Required
manageable Whether to return only manageable assets. Returns both usable and manageable by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.Asset.ID string Asset ID.
TenableSC.Asset.Name string Asset name.
TenableSC.Asset.HostCount number Asset host IPs count.
TenableSC.Asset.Type string Asset type.
TenableSC.Asset.Tag string Asset tag.
TenableSC.Asset.Owner string Asset owner username.
TenableSC.Asset.Group string Asset group.
TenableSC.Asset.LastModified date Asset last modified time.

Human Readable Output

Tenable.sc Assets

ID Name Tag Owner Type HostCount LastModified
0 All Defined Ranges     static 0 2023-01-09T13:13:52Z
1 asset_1_name   test dynamic 106 2023-05-21T09:12:52Z
2 Systems that have been Scanned   test dynamic 152 2023-01-09T13:14:43Z

tenable-sc-create-asset


Requires security manager role. Create an asset in Tenable.sc with provided IP addresses.

Base Command

tenable-sc-create-asset

Input

Argument Name Description Required
name Asset name. Required
description Asset description. Optional
owner_id Asset owner ID. Default is the Session User ID. Can be retrieved from the list-users command. Optional
tag Asset tag. Optional
ip_list Comma-separated list of IPs to include in the asset, e.g., 10.0.0.2,10.0.0.4. Required

Context Output

Path Type Description
TenableSC.Asset.Name string Asset name.
TenableSC.Asset.ID string Asset ID.
TenableSC.Asset.OwnerName string Asset owner name.
TenableSC.Asset.Tags string Asset tags.

Human Readable Output

Asset created successfully

ID Name OwnerName
42 example output yuv

tenable-sc-get-asset


Requires security manager role. Get details for a given asset in Tenable.sc.

Base Command

tenable-sc-get-asset

Input

Argument Name Description Required
asset_id Asset ID that can be retrieved from the list-assets command. Required

Context Output

Path Type Description
TenableSC.Asset.ID number Asset ID.
TenableSC.Asset.Name string Asset name.
TenableSC.Asset.Description string Asset description.
TenableSC.Asset.Tag string Asset tag.
TenableSC.Asset.Modified date Asset last modified time.
TenableSC.Asset.Owner string Asset owner user name.
TenableSC.Asset.Group string Asset owner group.
TenableSC.Asset.IPs unknown Asset viewable IPs.

Human Readable Output

Tenable.sc Asset

ID Name Description Created Modified Owner Group IPs
1 asset_1_name asset_1_description 2023-01-09T13:14:43Z 2023-05-21T09:12:52Z test Full Access {IPs_list}

tenable-sc-delete-asset


Requires security manager role. Delete the asset with the given ID from Tenable.sc.

Base Command

tenable-sc-delete-asset

Input

Argument Name Description Required
asset_id Asset ID. Required

Context Output

There is no context output for this command.

Human Readable Output

Asset {asset_id} was deleted successfully.

tenable-sc-list-alerts


Requires security manager role. List alerts from Tenable.sc.

Base Command

tenable-sc-list-alerts

Input

Argument Name Description Required
manageable Whether to return only manageable alerts. Returns both usable and manageable by default. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
TenableSC.Alert.ID string Alert ID.
TenableSC.Alert.Name string Alert name.
TenableSC.Alert.Description string Alert description.
TenableSC.Alert.State string Alert state.
TenableSC.Alert.Actions string Alert actions.
TenableSC.Alert.LastTriggered date Alert last triggered time.
TenableSC.Alert.LastEvaluated date Alert last evaluated time.
TenableSC.Alert.Group string Alert owner group name.
TenableSC.Alert.Owner string Alert owner user name.

Human Readable Output

Tenable.sc Alerts

ID Name Actions State LastTriggered LastEvaluated Group Owner
1 Test Alert 1 ticket Triggered 2023-02-16T07:13:08Z 2023-05-23T13:30:01Z Full Access lmanager
2 Test Alert 2 scan Triggered 2023-02-16T07:14:07Z 2023-05-24T12:14:08Z Full Access lmanager

tenable-sc-get-alert


Requires security manager role. Get information about a given alert in Tenable.sc.

Base Command

tenable-sc-get-alert

Input

Argument Name Description Required
alert_id Alert ID. Can be retrieved from the list-alerts command. Required

Context Output

Path Type Description
TenableSC.Alert.ID string Alert ID.
TenableSC.Alert.Name string Alert name.
TenableSC.Alert.Description string Alert description.
TenableSC.Alert.State string Alert state.
TenableSC.Alert.Condition.Trigger string Alert trigger.
TenableSC.Alert.LastTriggered date Alert last triggered time.
TenableSC.Alert.Condition.Query string Alert query name.
TenableSC.Alert.Condition.Filter.Name string Alert query filter name.
TenableSC.Alert.Condition.Filter.Values Unknown Alert query filter values.
TenableSC.Alert.Action.Type string Alert action type.
TenableSC.Alert.Action.Values Unknown Alert action values.

Human Readable Output

Tenable.sc Alert

ID Name LastTriggered State Behavior  
  1 Test Alert 1 2023-02-16T07:13:08Z Triggered Execute only on first trigger

Condition

Trigger Query
sumip \u003e= 10 Query for alert ‘Test Alert 1’ at 1676531587

Actions

Type Values
ticket lmanager

tenable-sc-get-device


Requires security manager role. Gets the specified device information.

Base Command

tenable-sc-get-device

Input

Argument Name Description Required
ip A valid IP address of a device. Optional
dns_name DNS name of a device. Optional
repository_id Repository ID to get the device from. Can be retrieved from the list-repositories command. Optional

Context Output

Path Type Description
TenableSC.Device.IP string Device IP address.
TenableSC.Device.UUID string Device UUID.
TenableSC.Device.RepositoryID string Device repository ID.
TenableSC.Device.MacAddress string Device Mac address.
TenableSC.Device.NetbiosName string Device Netbios name.
TenableSC.Device.DNSName string Device DNS name.
TenableSC.Device.OS string Device operating system.
TenableSC.Device.OsCPE string Device Common Platform Enumeration.
TenableSC.Device.LastScan date Device’s last scan time.
TenableSC.Device.RepositoryName string Device repository name.
TenableSC.Device.TotalScore number Device total threat score.
TenableSC.Device.LowSeverity number Device total threat scores with low severity.
TenableSC.Device.MediumSeverity number Device total threat scores with medium severity.
TenableSC.Device.HighSeverity number Device total threat scores with high severity.
TenableSC.Device.CriticalSeverity number Device total threat scores with critical severity.
Endpoint.IPAddress string Endpoint IP address.
Endpoint.Hostname string Endpoint DNS name.
Endpoint.MACAddress string Endpoint MAC address.
Endpoint.OS string Endpoint OS.

Human Readable Output

Tenable.sc Device

IP UUID MacAddress
{IP} {UUID} {MacAddress}

tenable-sc-list-users


List users in Tenable.sc. Results may vary based on the role type (admin or security manager).

Base Command

tenable-sc-list-users

Input

Argument Name Description Required
id Filter by user ID. Optional
username Filter by user username. Optional
email Filter by user email address. Optional

Context Output

Path Type Description
TenableSC.User.ID string User ID.
TenableSC.User.Username string Username.
TenableSC.User.FirstName string User first name.
TenableSC.User.LastName string User last name.
TenableSC.User.Title string User title.
TenableSC.User.Email string User email address.
TenableSC.User.Created date The creation time of the user.
TenableSC.User.Modified date Last modification time of the user.
TenableSC.User.Login date User last login.
TenableSC.User.Role string User role name.

Human Readable Output

Tenable.sc Users

ID Username Title Email Created Modified LastLogin Role
1 test     2023-01-09T13:13:53Z 2023-05-24T10:23:29Z   Security Manager
2 secman     2023-02-06T09:54:47Z 2023-05-01T10:05:46Z 2023-05-24T12:43:35Z Security Manager

tenable-sc-get-system-licensing


Retrieve licensing information from Tenable.sc. Requires admin role.

Base Command

tenable-sc-get-system-licensing

Input

There are no input arguments for this command.

Context Output

Path Type Description
TenableSC.Status.ActiveIPS number Number of active IP addresses.
TenableSC.Status.LicensedIPS Unknown Number of licensed IP addresses.
TenableSC.Status.License Unknown License status.

Human Readable Output

Tenable.sc Licensing information

License LicensedIPS ActiveIPS
Valid 512 152

tenable-sc-get-system-information


Get the system information and diagnostics from Tenable.sc. Requires admin role.

Base Command

tenable-sc-get-system-information

Input

There are no input arguments for this command.

Context Output

Path Type Description
TenableSC.System.Version string System version.
TenableSC.System.BuildID string System build ID.
TenableSC.System.ReleaseID string System release ID.
TenableSC.System.License string System license status.
TenableSC.System.JavaStatus boolean Server Java status.
TenableSC.System.RPMStatus boolean Server RPM status.
TenableSC.System.DiskStatus boolean Server disk status.
TenableSC.System.DiskThreshold number Disk threshold.
TenableSC.System.LastCheck date System last check time.

Human Readable Output

Tenable.sc System information

RPMStatus JavaStatus DiskStatus DiskThreshold LastCheck
true true true 5% 2023-05-24T04:10:02Z

tenable-sc-get-all-scan-results


Returns all scan results in Tenable.sc. Requires security manager role.

Base Command

tenable-sc-get-all-scan-results

Input

Argument Name Description Required
manageable Filter only manageable alerts. By default, returns both usable and manageable alerts. Possible values are: true, false. Default is false. Optional
page The page to return, starting from 0. Default is 0. Optional
limit The number of objects to return in one response (maximum limit is 200). Default is 50. Optional

Context Output

Path Type Description
TenableSC.ScanResults.ID Number Scan ID.
TenableSC.ScanResults.Name string Scan name.
TenableSC.ScanResults.Status string Scan status.
TenableSC.ScanResults.Description string Scan description.
TenableSC.ScanResults.Policy string Scan policy.
TenableSC.ScanResults.Group string Scan group name.
TenableSC.ScanResults.Checks number Scan completed number of checks.
TenableSC.ScanResults.StartTime date Scan results start time.
TenableSC.ScanResults.EndTime date Scan results end time.
TenableSC.ScanResults.Duration number Scan duration in minutes.
TenableSC.ScanResults.ImportTime date Scan import time.
TenableSC.ScanResults.ScannedIPs number Number of scanned IPs.
TenableSC.ScanResults.Owner string Scan owner name.
TenableSC.ScanResults.RepositoryName string Scan repository name.
TenableSC.ScanResults.ImportStatus string Scan import status.

Human Readable Output

Tenable.sc Scan results - 0-1

Total number of elements is 77

ID Name Status Description Policy Group Owner ScannedIPs StartTime EndTime Duration Checks ImportTime RepositoryName
92 test_scan_2023-mart-05-1950 Error Test scan 2023 Network Scan Full Access secman 0 2023-04-24T23:50:07Z 2023-04-25T01:10:13Z 80.1 22639720   Local
93 test_scan_2023-mart-05-1950 Error Test scan 2023 Network Scan Full Access secman 0 2023-04-25T23:50:07Z 2023-04-26T00:30:44Z 40.61666666666667 12624659   Local

tenable-sc-list-groups


List all groups. Requires security manager role.

Base Command

tenable-sc-list-groups

Input

Argument Name Description Required
show_users Whether to show group member. Possible values are: true, false. Default is true. Optional
limit The number of objects to return in one response. Default is 50. Optional

Context Output

Path Type Description
TenableSC.Group.Name string Group name.
TenableSC.Group.ID number Group ID.
TenableSC.Group.Description string Group description.
TenableSC.Group.Users.Firstname string Group’s user’s first name.
TenableSC.Group.Users.Lastname string Group’s user’s last name.
TenableSC.Group.Users.ID string Group’s user’s ID.
TenableSC.Group.Users.UUID string Group’s user’s UUID.
TenableSC.Group.Users.Username string Group’s user’s username.

Human Readable Output

Tenable.sc groups

ID
0

Group id:0

Username Firstname Lastname
test test  
secman    
testuser1 fname lname
testuser444 fname2 lname2
testuser3 fname3 lname3

tenable-sc-create-user


Creates a new user. This command can be executed with both roles (admin or security manager) based on the role_id you choose.

Base Command

tenable-sc-create-user

Input

Argument Name Description Required
first_name The user’s first name. Optional
last_name The user’s last name. Optional
user_name The user’s username. Required
email The user’s email address. Required if email_notice is given. Optional
address The user’s postal address. Optional
phone The user’s phone number. Optional
city The city the user is living in. Optional
state The state the user is living in. Optional
country The country the user is living in. Optional
locked Whether the user should be locked. Possible values are: true, false. Default is false. Optional
email_notice If different from None, a valid email address must be given. Possible values are: both, password, id, none. Default is none. Optional
auth_type The authentication type. Tenable (TNS). Lightweight Directory Access Protocol (LDAP). Security Assertion Markup Language (SAML). LDAP server or SAML authentication needs to be configured in order to select LDAP or SAML. Possible values are: ldap, legacy, linked, saml, tns. Default is tns. Required
password The user’s password. Must be at least 3 characters. Required
time_zone The user timezone, possible values can be found here: https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html. Optional
role_id The user’s role. Only an Administrator can create Administrator accounts. Possible values are: Administrator, Security Manager, Security Analyst, Vulnerability Analyst, Executive, Credential Manager, Auditor. Required
must_change_password Whether the password must be changed. When choosing LDAP or SAML auth types, ‘must_change_password’ must be set to False. For all other cases can be either True or False. Possible values are: false, true. Default is false. Optional
managed_users_groups Comma-separated list of session user’s role that can manage groups. Use tenable-sc-list-groups to get all available groups. Optional
managed_objects_groups Comma-separated list of the session user’s role that can manage groups. Use tenable-sc-list-groups to get all available groups. Optional
group_id Valid group ID whose users can be managed by the created user. Required
responsible_asset_id Default is 0. ID of a valid, usable, accessible asset. Use tenable-sc-list-assets to get all available assets. -1 is not set, 0 is all assets, and other numbers are asset ID. Required

Context Output

Path Type Description
TenableSC.User.Address String User address.
TenableSC.User.ApiKeys Unknown User API keys.
TenableSC.User.AuthType String User auth type.
TenableSC.User.CanManage Boolean Whether the user has manage permissions.
TenableSC.User.CanUse Boolean Whether the user has use permissions.
TenableSC.User.City String User city of residence.
TenableSC.User.Country String User country of residence.
TenableSC.User.CreatedTime Date User creation time.
TenableSC.User.Email String User email address.
TenableSC.User.FailedLogins String Number of failed user logins.
TenableSC.User.Fax String User fax.
TenableSC.User.Fingerprint Unknown User fingerprint.
TenableSC.User.Firstname String User first name.
TenableSC.User.group.Description String User group’s description.
TenableSC.User.Group.ID String User group’s ID.
TenableSC.User.Group.Name String User group’s name.
TenableSC.User.ID String User ID.
TenableSC.User.LastLogin String User last login time.
TenableSC.User.LastLoginIP String User last login IP.
TenableSC.User.Lastname String User last name.
TenableSC.User.Ldap.Description String User LDAP description.
TenableSC.User.Ldap.ID Number User LDAP ID.
TenableSC.User.Ldap.Name String User LDAP name.
TenableSC.User.LdapUsername String user LDAP username.
TenableSC.User.Locked String Whether user is locked.
TenableSC.User.ManagedObjectsGroups.Description String User managed object groups description.
TenableSC.User.ManagedObjectsGroups.ID String User managed object groups ID.
TenableSC.User.ManagedObjectsGroups.Name String User managed object groups name.
TenableSC.User.ManagedUsersGroups.Description String User managed users groups description.
TenableSC.User.ManagedUsersGroups.ID String User managed users groups ID.
TenableSC.User.ManagedUsersGroups.Name String User managed users groups name.
TenableSC.User.ModifiedTime Date User last modification time.
TenableSC.User.MustChangePassword String If user must change password.
TenableSC.User.Password String If user password is set.
TenableSC.User.Phone String User phone number.
TenableSC.User.Preferences.Name String User preferences name.
TenableSC.User.Preferences.Tag String User preferences tag.
TenableSC.User.Preferences.Value String User preferences value.
TenableSC.User.ResponsibleAsset.Description String User responsible asset description.
TenableSC.User.ResponsibleAsset.ID String User responsible asset ID.
TenableSC.User.ResponsibleAsset.Name String User responsible asset name.
TenableSC.User.ResponsibleAsset.UUID Unknown User responsible asset UUID.
TenableSC.User.Role.Description String User role description.
TenableSC.User.Role.ID String User role ID.
TenableSC.User.Role.Name String User role name.
TenableSC.User.State String User state.
TenableSC.User.Status String User status.
TenableSC.User.Title String User title.
TenableSC.User.Username String User username.
TenableSC.User.UUID String User UUID.

Human Readable Output

User example_output was created successfully

User type User Id User Status User Name User Role Name User Group Name
regular 57 0 example_output Security Analyst Full Access

tenable-sc-update-user


Update user details of the given user_id.

Base Command

tenable-sc-update-user

Input

Argument Name Description Required
first_name The user’s first name. Optional
last_name The user’s last name. Optional
user_name The user’s username. Optional
email The user’s email address. Required if email_notice is given. Optional
address The user’s postal address. Optional
phone The user’s phone number. Optional
city The city the user is living in. Optional
state The state the user is living in. Optional
country The country the user is living in. Optional
locked Whether the user should be locked. Possible values are: true, false. Default is false. Optional
time_zone The user timezone. Possible values can be found here: https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html. Optional
role_id The user’s role. Only an Administrator can create Administrator accounts. Possible values are: Administrator, Security Manager, Security Analyst, Vulnerability Analyst, Executive, Credential Manager, Auditor. Optional
must_change_password Whether the password must be changed. When choosing LDAP or SAML auth types, ‘must_change_password’ must be set to False. For all other cases can be either True or False. Possible values are: false, true. Default is false. Optional
managed_users_groups Comma-separated list of session user’s role that can manage groups. Use tenable-sc-list-groups to get all available groups. Optional
managed_objects_groups Comma-separated list of session user’s role that can manage groups. Use tenable-sc-list-groups to get all available groups. Optional
group_id Valid group ID whose users can be managed by the created user. Optional
responsible_asset_id ID of a valid, usable, accessible asset. Use tenable-sc-list-assets to get all available assets. -1 is not set, 0 is all assets, and other numbers are asset ID. Optional
password The new password to set. Must be given with current_password. Must be at least 3 characters. Optional
current_password This is the admin/Security Manager password from the instance parameters. Required when attempting to change a user’s password. Optional
user_id The ID of the user whose details you want to update. Required

Context Output

Path Type Description
TenableSC.User.Address String User address.
TenableSC.User.ApiKeys Unknown User API keys.
TenableSC.User.AuthType String User auth type.
TenableSC.User.CanManage Boolean Whether the user has manage permissions.
TenableSC.User.CanUse Boolean Whether the user has use permissions.
TenableSC.User.City String User city of residence.
TenableSC.User.Country String User country of residence.
TenableSC.User.CreatedTime Date User creation time.
TenableSC.User.Email String User email address.
TenableSC.User.FailedLogins String Number of failed user logins.
TenableSC.User.Fax String User fax.
TenableSC.User.Fingerprint Unknown User fingerprint.
TenableSC.User.Firstname String User first name.
TenableSC.User.group.Description String User group’s description.
TenableSC.User.Group.ID String User group’s ID.
TenableSC.User.Group.Name String User group’s name.
TenableSC.User.ID String User ID.
TenableSC.User.LastLogin String User last login time.
TenableSC.User.LastLoginIP String User last login IP.
TenableSC.User.Lastname String User last name.
TenableSC.User.Ldap.Description String User LDAP description.
TenableSC.User.Ldap.ID Number User LDAP ID.
TenableSC.User.Ldap.Name String User LDAP name.
TenableSC.User.LdapUsername String User LDAP username.
TenableSC.User.Locked String Whether user is locked.
TenableSC.User.ManagedObjectsGroups.Description String User managed object groups description.
TenableSC.User.ManagedObjectsGroups.ID String User managed object groups ID.
TenableSC.User.ManagedObjectsGroups.Name String User managed object groups name.
TenableSC.User.ManagedUsersGroups.Description String User managed users groups description.
TenableSC.User.ManagedUsersGroups.ID String User managed users groups ID.
TenableSC.User.ManagedUsersGroups.Name String User managed users groups name.
TenableSC.User.ModifiedTime Date User last modification time.
TenableSC.User.MustChangePassword String If user must change password.
TenableSC.User.Password String If user password is set.
TenableSC.User.Phone String User phone number.
TenableSC.User.Preferences.Name String User preferences name.
TenableSC.User.Preferences.Tag String User preferences tag.
TenableSC.User.Preferences.Value String User preferences value.
TenableSC.User.ResponsibleAsset.Description String User responsible asset description.
TenableSC.User.ResponsibleAsset.ID String User responsible asset ID.
TenableSC.User.ResponsibleAsset.Name String User responsible asset name.
TenableSC.User.ResponsibleAsset.UUID Unknown User responsible asset UUID.
TenableSC.User.Role.Description String User role description.
TenableSC.User.Role.ID String User role ID.
TenableSC.User.Role.Name String User role name.
TenableSC.User.State String User state.
TenableSC.User.Status String User status.
TenableSC.User.Title String User title.
TenableSC.User.Username String User username.
TenableSC.User.UUID String User UUID.

Human Readable Output

user 23 was updated successfully

User type User Id User Status User Name First Name Lat Name Email User Role Name User Group Name
regular 23 0 testuser30 testuser30 testuser30 testuser30@mymail.com Credential Manager Full Access

tenable-sc-delete-user


Delete a user by given user_id. This command can be executed with both roles (admin or security manager).

Base Command

tenable-sc-delete-user

Input

Argument Name Description Required
user_id The ID of the user we want to delete. Required

Context Output

There is no context output for this command.

Human Readable Output

User {user_id} was deleted successfully.

tenable-sc-list-plugin-family


List plugin families / return information about a plugin family given ID. Requires security manager role.

Base Command

tenable-sc-list-plugin-family

Input

Argument Name Description Required
plugin_id The ID of the plugin to search. If given, other arguments will be ignored. Optional
limit The number of objects to return in one response (maximum limit is 200). Ignored when plugin_id is given. Default is 50. Optional
is_active Default is none. none - both active and passive Plugin Families are returned. true - Only active Plugin Families will be returned. false - Only passive Plugin Families will be returned. Ignored when plugin_id is given. Possible values are: true, false. Optional

Context Output

Path Type Description
TenableSC.PluginFamily.ID String Plugin family ID.
TenableSC.PluginFamily.Name String Plugin family name.
TenableSC.PluginFamily.Count String Number of plugins in a family.
TenableSC.PluginFamily.Plugins String The plugins list.
TenableSC.PluginFamily.Type String Plugin family type.

Human Readable Output

When plugin_id isn’t given:

Plugin families

Plugin ID Plugin Name
0 N/A
1 Red Hat Local Security Checks

When plugin_id is given:

Plugin families

Plugin ID Plugin Name Is Active
2 HP-UX Local Security Checks true

tenable-sc-create-policy


Creates a policy. Requires security manager role. This command is prerequisite for creating remediation scan.

Base Command

tenable-sc-create-policy

Input

Argument Name Description Required
policy_name The name of the policy to create. Optional
policy_description The description of the policy to create. Optional
policy_template_id Policy template id. Default is 1. Default is 1. Required
port_scan_range Possible values: default, all or a comma-separated list of values - 21,23,25,80,110. Optional
tcp_scanner Only possible if you are using Linux or FreeBSD. On Windows or macOS, the scanner does not do a TCP scan and instead uses the SYN scanner. If you enable this option, you can also set the syn_firewall_detection option. Possible values are: no, yes. Default is no. Optional
syn_scanner Identifies open TCP ports on the target hosts. If you enable this option, you can also set the syn_firewall_detection option. Possible values are: no, yes. Default is yes. Optional
udp_scanner Enabling the UDP port scanner may dramatically increase the scan time and produce unreliable results. Consider using the netstat or SNMP port enumeration options instead if possible. Possible values are: no, yes. Default is no. Optional
family_id Family ID. Can be retrieved from the result of the tenable-sc-list-plugin-family command. Required
plugins_id Comma-separated list of plugin_ids, Can be retrieved from the result of the tenable-sc-list-plugin-family command with family_id as the argument. Required
syn_firewall_detection Rely on local port enumeration first before relying on network port scans. Possible values are: Automatic (normal), Do not detect RST rate limitation(soft), Ignore closed ports(aggressive), Disabled(softer). Default is Automatic (normal). Optional

Context Output

Path Type Description
TenableSC.ScanPolicy.AuditFiles Unknown Policy audit files.
TenableSC.ScanPolicy.CanManage String Policy permissions.
TenableSC.ScanPolicy.CanUse String Policy permissions.
TenableSC.ScanPolicy.Context String Policy context.
TenableSC.ScanPolicy.CreatedTime Date Policy creation time.
TenableSC.ScanPolicy.Creator.Firstname String Policy creator first name.
TenableSC.ScanPolicy.Creator.ID String Policy creator ID.
TenableSC.ScanPolicy.Creator.Lastname String Policy creator last name.
TenableSC.ScanPolicy.Creator.Username String Policy creator user name.
TenableSC.ScanPolicy.Creator.UUID String Policy creator UUID.
TenableSC.ScanPolicy.Description String Policy description.
TenableSC.ScanPolicy.Families.Count String Policy number of families.
TenableSC.ScanPolicy.Families.ID String Policy family ID.
TenableSC.ScanPolicy.Families.Name String Policy family name.
TenableSC.ScanPolicy.Families.Plugins Unknown Policy family plugins.
TenableSC.ScanPolicy.GenerateXCCDFResults String Policy generated XCCDF results.
TenableSC.ScanPolicy.Groups Unknown Policy groups.
TenableSC.ScanPolicy.ID String Policy ID.
TenableSC.ScanPolicy.ModifiedTime Date Policy last modification time.
TenableSC.ScanPolicy.Name String Policy name.
TenableSC.ScanPolicy.Owner.Firstname String Policy owner first name.
TenableSC.ScanPolicy.Owner.ID String Policy owner ID.
TenableSC.ScanPolicy.Owner.Lastname String Policy owner last name.
TenableSC.ScanPolicy.Owner.Username String Policy owner username.
TenableSC.ScanPolicy.Owner.UUID String Policy owner UUID.
TenableSC.ScanPolicy.OwnerGroup.Description String Policy owner group description.
TenableSC.ScanPolicy.OwnerGroup.ID String Policy owner group ID.
TenableSC.ScanPolicy.OwnerGroup.Name String Policy owner group name.
TenableSC.ScanPolicy.PolicyTemplate.Agent String Policy template agent.
TenableSC.ScanPolicy.PolicyTemplate.Description String Policy template description.
TenableSC.ScanPolicy.PolicyTemplate.ID String Policy template ID.
TenableSC.ScanPolicy.PolicyTemplate.Name String Policy template name.
TenableSC.ScanPolicy.Preferences.PortscanRange String Policy port scan range.
TenableSC.ScanPolicy.Preferences.SynFirewallDetection String Policy SYN firewall detection.
TenableSC.ScanPolicy.Preferences.SynScanner String Policy SYN scanner.
TenableSC.ScanPolicy.Preferences.TcpScanner String Policy TCP scanner.
TenableSC.ScanPolicy.Preferences.UdpScanner String Policy UDP scanner.
TenableSC.ScanPolicy.Status String Policy status.
TenableSC.ScanPolicy.tags String Policy tags.
TenableSC.ScanPolicy.TargetGroup.Description String Policy target group description.
TenableSC.ScanPolicy.TargetGroup.ID Number Policy target group ID.
TenableSC.ScanPolicy.TargetGroup.Name String Policy target group name.
TenableSC.ScanPolicy.UUID String Policy UUID.

Human Readable Output

Policy was created successfully

Policy type name Created Time Plugin Families Policy Status Policy UUID Policy can Manage Creator Username policyTemplate Name
regular scan_name 1684923394 {‘id’: ‘1’, ‘name’: ‘Red Hat Local Security Checks’, ‘count’: ‘9297’, ‘plugins’: []} 0 {policy UUID} true yuv Advanced Scan

tenable-sc-list-query


Lists the queries. Requires security manager role.

Base Command

tenable-sc-list-query

Input

Argument Name Description Required
query_id The ID of the query to search. Optional
type The query type to retrieve. When no type is set all queries are returned. Possible values are: alert, lce, mobile, ticket, user. Optional

Context Output

Path Type Description
TenableSC.Query.Manageable.BrowseColumns String Relevant only when query_id is not given. Manageable Query browse columns.
TenableSC.Query.Manageable.BrowseSortColumn String Relevant only when query_id is not given. Manageable Query browse sort column.
TenableSC.Query.Manageable.BrowseSortDirection String Relevant only when query_id is not given. Manageable Query browse sort direction.
TenableSC.Query.Manageable.CanManage String Relevant only when query_id is not given. Manageable Query permissions.
TenableSC.Query.Manageable.CanUse String Relevant only when query_id is not given. Manageable Query permissions.
TenableSC.Query.Manageable.Context String Relevant only when query_id is not given. Manageable Query context.
TenableSC.Query.Manageable.CreatedTime Date Relevant only when query_id is not given. Manageable Query creation time.
TenableSC.Query.Manageable.Creator.Firstname String Relevant only when query_id is not given. Manageable Query Creator first name.
TenableSC.Query.Manageable.Creator.ID String Relevant only when query_id is not given. Manageable Query Creator ID.
TenableSC.Query.Manageable.Creator.Lastname String Relevant only when query_id is not given. Manageable Query Creator last name.
TenableSC.Query.Manageable.Creator.Username String Relevant only when query_id is not given. Manageable Query Creator user name.
TenableSC.Query.Manageable.Creator.UUID String Relevant only when query_id is not given. Manageable Query Creator UUID.
TenableSC.Query.Manageable.Description String Relevant only when query_id is not given. Manageable Query description.
TenableSC.Query.Manageable.Filters.FilterName String Relevant only when query_id is not given. Manageable Query filter name.
TenableSC.Query.Manageable.Filters.Operator String Relevant only when query_id is not given. Manageable Query filter operator.
TenableSC.Query.Manageable.Filters.Value String Relevant only when query_id is not given. Manageable Query filter value
TenableSC.Query.Manageable.Groups Unknown Relevant only when query_id is not given. Manageable Query groups.
TenableSC.Query.Manageable.ID String Relevant only when query_id is not given. Manageable Query ID.
TenableSC.Query.Manageable.ModifiedTime Date Relevant only when query_id is not given. Manageable Query modification time.
TenableSC.Query.Manageable.Name String Relevant only when query_id is not given. Manageable Query name.
TenableSC.Query.Manageable.Owner.Firstname String Relevant only when query_id is not given. Manageable Query owner first name.
TenableSC.Query.Manageable.Owner.ID String Relevant only when query_id is not given. Manageable Query owner ID.
TenableSC.Query.Manageable.Owner.Lastname String Relevant only when query_id is not given. Manageable Query owner last name.
TenableSC.Query.Manageable.Owner.Username String Relevant only when query_id is not given. Manageable Query owner user name.
TenableSC.Query.Manageable.Owner.UUID String Relevant only when query_id is not given. Manageable Query owner UUID.
TenableSC.Query.Manageable.OwnerGroup.Description String Relevant only when query_id is not given. Manageable Query owner group description.
TenableSC.Query.Manageable.OwnerGroup.ID String Relevant only when query_id is not given. Manageable Query owner group ID.
TenableSC.Query.Manageable.OwnerGroup.Name String Relevant only when query_id is not given. Manageable Query owner group name.
TenableSC.Query.Manageable.Status String Relevant only when query_id is not given. Manageable Query status.
TenableSC.Query.Manageable.Tags String Relevant only when query_id is not given. Manageable Query tags.
TenableSC.Query.Manageable.TargetGroup.Description String Relevant only when query_id is not given. Manageable Query target group description.
TenableSC.Query.Manageable.TargetGroup.ID Number Relevant only when query_id is not given. Manageable Query target group ID.
TenableSC.Query.Manageable.TargetGroup.Name String Relevant only when query_id is not given. Manageable Query target group name.
TenableSC.Query.Manageable.Tool String Relevant only when query_id is not given. Manageable Query tool.
TenableSC.Query.Manageable.Type String Relevant only when query_id is not given. Manageable Query type.
TenableSC.Query.Manageable.Filters.Value.Description String Relevant only when query_id is not given. Manageable Query filter value description.
TenableSC.Query.Manageable.Filters.Value.ID String Relevant only when query_id is not given. Manageable Query filter value ID.
TenableSC.Query.Manageable.Filters.Value.Name String Relevant only when query_id is not given. Manageable Query filter value name.
TenableSC.Query.Manageable.Filters.Value.Type String Relevant only when query_id is not given. Manageable Query filter value type.
TenableSC.Query.Manageable.Filters.Value.UUID String Relevant only when query_id is not given. Manageable Query filter value UUID
TenableSC.Query.Manageable.Filters Unknown Relevant only when query_id is not given. Manageable Query filters.
TenableSC.Query.Usable.BrowseColumns String Relevant only when query_id is not given. Usable Query browse columns.
TenableSC.Query.Usable.BrowseSortColumn String Relevant only when query_id is not given. Usable Query browse sort column.
TenableSC.Query.Usable.BrowseSortDirection String Relevant only when query_id is not given. Usable Query browse sort direction.
TenableSC.Query.Usable.CanManage String Relevant only when query_id is not given. Usable Query permissions.
TenableSC.Query.Usable.CanUse String Relevant only when query_id is not given. Usable Query permissions.
TenableSC.Query.Usable.Context String Relevant only when query_id is not given. Usable Query context.
TenableSC.Query.Usable.CreatedTime Date Relevant only when query_id is not given. Usable Query creation time.
TenableSC.Query.Usable.Creator.Firstname String Relevant only when query_id is not given. Usable Query Creator first name.
TenableSC.Query.Usable.Creator.ID String Relevant only when query_id is not given. Usable Query Creator ID.
TenableSC.Query.Usable.Creator.Lastname String Relevant only when query_id is not given. Usable Query Creator last name.
TenableSC.Query.Usable.Creator.Username String Relevant only when query_id is not given. Usable Query Creator user name.
TenableSC.Query.Usable.Creator.UUID String Relevant only when query_id is not given. Usable Query Creator UUID.
TenableSC.Query.Usable.Description String Relevant only when query_id is not given. Usable Query description.
TenableSC.Query.Usable.Filters.FilterName String Relevant only when query_id is not given. Usable Query filter name.
TenableSC.Query.Usable.Filters.Operator String Relevant only when query_id is not given. Usable Query filter operator.
TenableSC.Query.Usable.Filters.Value String Relevant only when query_id is not given. Usable Query filter value.
TenableSC.Query.Usable.Groups Unknown Relevant only when query_id is not given. Usable Query groups.
TenableSC.Query.Usable.ID String Relevant only when query_id is not given. Usable Query ID.
TenableSC.Query.Usable.ModifiedTime Date Relevant only when query_id is not given. Usable Query modification time.
TenableSC.Query.Usable.Name String Relevant only when query_id is not given. Usable Query name.
TenableSC.Query.Usable.Owner.Firstname String Relevant only when query_id is not given. Usable Query owner first name.
TenableSC.Query.Usable.Owner.ID String Relevant only when query_id is not given. Usable Query owner ID.
TenableSC.Query.Usable.Owner.Lastname String Relevant only when query_id is not given. Usable Query owner last name.
TenableSC.Query.Usable.Owner.Username String Relevant only when query_id is not given. Usable Query owner user name.
TenableSC.Query.Usable.Owner.UUID String Relevant only when query_id is not given. Usable Query owner UUID.
TenableSC.Query.Usable.OwnerGroup.Description String Relevant only when query_id is not given. Usable Query owner group description.
TenableSC.Query.Usable.OwnerGroup.ID String Relevant only when query_id is not given. Usable Query owner group ID.
TenableSC.Query.Usable.OwnerGroup.Name String Relevant only when query_id is not given. Usable Query owner group name.
TenableSC.Query.Usable.Status String Relevant only when query_id is not given. Usable Query status.
TenableSC.Query.Usable.Tags String Relevant only when query_id is not given. Usable Query tags.
TenableSC.Query.Usable.TargetGroup.Description String Relevant only when query_id is not given. Usable Query target group description.
TenableSC.Query.Usable.TargetGroup.ID Number Relevant only when query_id is not given. Usable Query target group ID.
TenableSC.Query.Usable.TargetGroup.Name String Relevant only when query_id is not given. Usable Query target group name.
TenableSC.Query.Usable.Tool String Relevant only when query_id is not given. Usable Query tool.
TenableSC.Query.Usable.Type String Relevant only when query_id is not given. Usable Query type.
TenableSC.Query.Usable.Filters.Value.Description String Relevant only when query_id is not given. Usable Query filter value description.
TenableSC.Query.Usable.Filters.Value.ID String Relevant only when query_id is not given. Usable Query filter value ID.
TenableSC.Query.Usable.Filters.Value.Name String Relevant only when query_id is not given. Usable Query filter value name.
TenableSC.Query.Usable.Filters.Value.Type String Relevant only when query_id is not given. Usable Query filter value type.
TenableSC.Query.Usable.Filters.Value.UUID String Relevant only when query_id is not given. Usable Query filter value UUID.
TenableSC.Query.Usable.Filters Unknown Relevant only when query_id is not given. Usable Query filters.
TenableSC.Query.BrowseColumns String Relevant only when query_id is given. Query browse columns.
TenableSC.Query.BrowseSortColumn String Relevant only when query_id is given. Query browse sort columns.
TenableSC.Query.BrowseSortDirection String Relevant only when query_id is given. Query browse sort direction
TenableSC.Query.CanManage String Relevant only when query_id is given. Query permissions.
TenableSC.Query.CanUse String Relevant only when query_id is given. Query permissions.
TenableSC.Query.Context String Relevant only when query_id is given. Query context.
TenableSC.Query.CreatedTime Date Relevant only when query_id is given. Query creation time.
TenableSC.Query.Creator.Firstname String Relevant only when query_id is given. Query creator first name.
TenableSC.Query.Creator.ID String Relevant only when query_id is given. Query creator ID.
TenableSC.Query.Creator.Lastname String Relevant only when query_id is given. Query creator last name.
TenableSC.Query.Creator.Username String Relevant only when query_id is given. Query creator user name.
TenableSC.Query.Creator.UUID String Relevant only when query_id is given. Query creator UUID.
TenableSC.Query.Description String Relevant only when query_id is given. Query description.
TenableSC.Query.Filters Unknown Relevant only when query_id is given. Query filters.
TenableSC.Query.Groups Unknown Relevant only when query_id is given. Query groups.
TenableSC.Query.ID String Relevant only when query_id is given. Query ID.
TenableSC.Query.ModifiedTime Date Relevant only when query_id is given. Query modification time.
TenableSC.Query.Name String Relevant only when query_id is given. Query name.
TenableSC.Query.Owner.Firstname String Relevant only when query_id is given. Query owner first name.
TenableSC.Query.Owner.ID String Relevant only when query_id is given. Query owner ID.
TenableSC.Query.Owner.Lastname String Relevant only when query_id is given. Query owner last name.
TenableSC.Query.Owner.Username String Relevant only when query_id is given. Query owner user name.
TenableSC.Query.Owner.UUID String Relevant only when query_id is given. Query owner UUID.
TenableSC.Query.OwnerGroup.Description String Relevant only when query_id is given. Query owner group description.
TenableSC.Query.OwnerGroup.ID String Relevant only when query_id is given. Query owner group ID.
TenableSC.Query.OwnerGroup.Name String Relevant only when query_id is given. Query owner group name.
TenableSC.Query.Status String Relevant only when query_id is given. Query status.
TenableSC.Query.Tags String Relevant only when query_id is given. Query tags.
TenableSC.Query.TargetGroup.Description String Relevant only when query_id is given. Query target group description.
TenableSC.Query.TargetGroup.ID Number Relevant only when query_id is given. Query target group ID.
TenableSC.Query.TargetGroup.Name String Relevant only when query_id is given. Query target group name.
TenableSC.Query.Tool String Relevant only when query_id is given. Query tool
TenableSC.Query.Type String Relevant only when query_id is given. Query type.

Human Readable Output

If query_id isn’t given:

Queries

Query Id Query Name Query Description Query Filters Query Manageable Query Usable
1 test_name test_description filter True True
2 test_name test_description   True False

If query_id is given:

Query {query_id}

Query Id Query Name Query Description
test_id test_name test_description

tenable-sc-update-asset


Requires security manager role. Update an asset.

Base Command

tenable-sc-update-asset

Input

Argument Name Description Required
name Asset name. Optional
asset_id The ID of the asset to update. Required
description The asset description. Optional
owner_id The asset owner ID. Optional
tag The asset tag. Optional
ip_list Comma-separated list of the asset IPs list. Optional

Context Output

There is no context output for this command.

Human Readable Output

asset {asset_id} was updated successfully.

tenable-sc-create-remediation-scan


Creates a remediation scan. Requires security manager role. This command is a prerequisite for creating remediation scan.

Base Command

tenable-sc-create-remediation-scan

Input

Argument Name Description Required
policy_name The name of the policy to create. Optional
policy_description The description of the policy to create. Optional
port_scan_range Possible values: default, all or a comma-separated list of values - 21,23,25,80,110. Optional
tcp_scanner Only possible if you are using Linux or FreeBSD. On Windows or macOS, the scanner does not do a TCP scan and instead uses the SYN scanner..If you enable this option, you can also set the syn_firewall_detection. Possible values are: no, yes. Default is no. Optional
syn_scanner Identifies open TCP ports on the target hosts. If you enable this option, you can also set the syn_firewall_detection option. Possible values are: no, yes. Default is yes. Optional
udp_scanner Enabling the UDP port scanner may dramatically increase the scan time and produce unreliable results. Consider using the netstat or SNMP port enumeration options instead if possible. Possible values are: no, yes. Default is no. Optional
syn_firewall_detection Default is Automatic (normal). Rely on local port enumeration first before relying on network port scans. Possible values are: Automatic (normal), Do not detect RST rate limitation(soft), Ignore closed ports(aggressive), Disabled(softer). Default is Automatic (normal). Optional
family_id Can be retrieved from the result of the tenable-sc-list-plugin-family command. Required
plugins_id Comma-separated list of plugin_ids, Can be retrieved from the result of the tenable-sc-list-plugin-family command with family_id as the argument. Required
scan_name Scan name. Required
description Scan description. Optional
repository_id Scan Repository ID, can be retrieved from the list-repositories command. Default is 1. Required
time_zone The timezone for the given start_time. Possible values can be found here: https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html. Optional
start_time The scan start time, in the format of YYYY-MM-DD:HH:MM:SS or relative timestamp (i.e., now, 3 days). Optional
repeat_rule_freq Specifies repeating events based on an interval of a repeat_rule_freq or more. Possible values are: HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY. Optional
repeat_rule_interval The number of repeat_rule_freq between each interval (for example: If repeat_rule_freq=DAILY and repeat_rule_interval=8 it means every eight days.). Optional
repeat_rule_by_day A comma-separated list of days of the week to run the schedule. Possible values are: SU, MO, TU, WE, TH, FR, SA. Optional
asset_ids Either no assets or comma-separated list of asset IDs to scan. Can be retrieved from the list-assets command. Optional
scan_virtual_hosts Default is false. Whether to include virtual hosts. Possible values are: true, false. Optional
ip_list Comma-separated IPs to scan, e.g., 10.0.0.1,10.0.0.2 . Optional
report_ids Comma-separated list of report definition IDs to create post-scan. Can be retrieved from the list-report-definitions command. Optional
credentials Comma-separated credentials IDs to use. Can be retrieved from the list-credentials command. Optional
timeout_action discard - do not import any of the results obtained by the scan to the database. import - Import the results of the current scan and discard the information for any unscanned targets. rollover-Import the results from the scan into the database and create a rollover scan that may be launched at a later time to complete the scan. Possible values are: discard, import, rollover. Default is import. Optional
max_scan_time Maximum scan run time in hours. Default is 1. Optional
dhcp_tracking Track hosts which have been issued new IP address, (e.g., DHCP). Possible values are: true, false. Optional
enabled Whether the schedule is enabled. The “enabled” field can only be set to “false” for schedules of type “ical”. For all other schedules types, “enabled” is set to “true”. Possible values are: true, false. Default is true. Optional
rollover_type Create a rollover scan scheduled to launch the next day at the same start time as the just completed scan. template-Create a rollover scan as a template for users to launch manually This field is required if the timeout_action is set to rollover. Default is nextDay. Optional

Context Output

Path Type Description
TenableSC.Scan.Assets Unknown Scan assets.
TenableSC.Scan.CanManage String Scan permissions.
TenableSC.Scan.CanUse String Scan permissions.
TenableSC.Scan.ClassifyMitigatedAge String Scan classify mitigated age.
TenableSC.Scan.CreatedTime Date Scan creation time.
TenableSC.Scan.Creator.Firstname String Scan creator first name.
TenableSC.Scan.Creator.ID String Scan creator ID.
TenableSC.Scan.Creator.Lastname String Scan creator last name.
TenableSC.Scan.Creator.Username String Scan creator username.
TenableSC.Scan.Creator.UUID String Scan creator UUID.
TenableSC.Scan.Credentials Unknown Scan credentials.
TenableSC.Scan.Description String Scan description.
TenableSC.Scan.DhcpTracking String Scan DHCP tracking.
TenableSC.Scan.EmailOnFinish String Scan email on finish.
TenableSC.Scan.EmailOnLaunch String Scan email on launch.
TenableSC.Scan.ID String Scan ID.
TenableSC.Scan.IpList String Scan IP list.
TenableSC.Scan.MaxScanTime String Scan max scan time.
TenableSC.Scan.ModifiedTime Date Scan last modification time.
TenableSC.Scan.Name String Scan name.
TenableSC.Scan.NumDependents Number Scan number of dependents.
TenableSC.Scan.Owner.Firstname String Scan owner first name.
TenableSC.Scan.Owner.ID String Scan owner ID.
TenableSC.Scan.Owner.Lastname String Scan owner last name.
TenableSC.Scan.Owner.Username String Scan owner username.
TenableSC.Scan.Owner.UUID String Scan owner UUID.
TenableSC.Scan.OwnerGroup.Description String Scan owner group description.
TenableSC.Scan.OwnerGroup.ID String Scan owner group ID.
TenableSC.Scan.OwnerGroup.Name String Scan owner group name.
TenableSC.Scan.Plugin.Description String Scan plugin description.
TenableSC.Scan.Plugin.ID String Scan plugin ID.
TenableSC.Scan.Plugin.Name String Scan plugin name.
TenableSC.Scan.Plugin.Type String Scan plugin type.
TenableSC.Scan.Policy.Context String Scan policy context.
TenableSC.Scan.Policy.Description String Scan policy description.
TenableSC.Scan.Policy.ID String Scan policy ID.
TenableSC.Scan.Policy.Name String Scan policy name.
TenableSC.Scan.Policy.Owner.Firstname String Scan policy owner first name.
TenableSC.Scan.Policy.Owner.ID String Scan policy owner ID.
TenableSC.Scan.Policy.Owner.Lastname String Scan policy owner last name.
TenableSC.Scan.Policy.Owner.Username String Scan policy owner username.
TenableSC.Scan.Policy.Owner.UUID String Scan policy owner UUID.
TenableSC.Scan.Policy.OwnerGroup.Description String Scan policy owner group description.
TenableSC.Scan.Policy.OwnerGroup.ID String Scan policy owner group ID.
TenableSC.Scan.Policy.OwnerGroup.Name String Scan policy owner group name.
TenableSC.Scan.Policy.Tags String Scan policy tags.
TenableSC.Scan.Policy.UUID String Scan policy UUID.
TenableSC.Scan.PolicyPrefs.Name String Scan policy preferation name.
TenableSC.Scan.PolicyPrefs.Value String Scan policy preferation value.
TenableSC.Scan.Reports Unknown Scan reports.
TenableSC.Scan.Repository.Description String Scan repository description.
TenableSC.Scan.Repository.ID String Scan repository ID.
TenableSC.Scan.Repository.Name String Scan repository name.
TenableSC.Scan.Repository.Type String Scan repository type.
TenableSC.Scan.Repository.UUID String Scan repository UUID.
TenableSC.Scan.RolloverType String Scan rollover type.
TenableSC.Scan.ScanResultID String Scan results ID.
TenableSC.Scan.ScanningVirtualHosts String Scan virtual hosts.
TenableSC.Scan.Schedule.Dependent.Description String Scan schedule dependent description.
TenableSC.Scan.Schedule.Dependent.ID Number Scan schedule dependent ID.
TenableSC.Scan.Schedule.Dependent.Name String Scan schedule dependent name.
TenableSC.Scan.Schedule.Enabled String Scan schedule enabled.
TenableSC.Scan.Schedule.ID Number Scan schedule ID.
TenableSC.Scan.Schedule.NextRun Number Scan schedule next run.
TenableSC.Scan.Schedule.ObjectType Number Scan schedule object type.
TenableSC.Scan.Schedule.RepeatRule String Scan schedule repeat rule.
TenableSC.Scan.Schedule.Start String Scan schedule start time.
TenableSC.Scan.Schedule.Type String Scan schedule type.
TenableSC.Scan.Status String Scan status.
TenableSC.Scan.TimeoutAction String Scan timeout action.
TenableSC.Scan.Type String Scan type.
TenableSC.Scan.UUID String Scan UUID.
TenableSC.Scan.Zone.Description String Scan zone description.
TenableSC.Scan.Zone.ID Number Scan zone ID.
TenableSC.Scan.Zone.Name String Scan zone name.

Human Readable Output

Remediation scan created successfully

Scan ID Scan Name Scan Type Dhcp Tracking status Created Time Modified Time Max Scan Time Policy id Policy context Schedule type Group Owner
69 my_Test_scan policy false 2023-05-24T10:12:27Z 1684923147 3600 1000044 scan now Full Access yuv

Vulnerabilities

ID Name Family Severity Total
10092 FTP Server Detection Service detection Info 6
10107 HTTP Server Type and Version Web Servers Info 61

Troubleshooting

For errors within Tenable.sc, the cause is generally specified, e.g., The currently logged in used is not an administrator, Unable to retrieve Asset #2412. Asset #2412 does not exist or Invalid login credentials. However there might be connection errors, for example when the server URL provided is incorrect.

tenable-sc-get-organization


Requires administrator role. Command to get a list of organizations’ information, depending on the comma-separated list of fields provided.

Base Command

tenable-sc-get-organization

Input

Argument Name Description Required
fields Optional fields to return specific values, example: restrictedIPs. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • server — Server URL (e.g. https://192.168.0.1) (required)
  • creds_keys — Access key
  • credentials — Username
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • fetch_time — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetchAssets — Fetch assets and vulnerabilities
  • assetsFetchInterval — Assets fetch interval in minutes.

Commands (33)

  • tenable-sc-create-asset

    Requires security manager role. Create an asset in Tenable.sc with provided IP addresses.

  • tenable-sc-create-policy

    Creates a policy. Requires security manager role. This command is prerequisite for creating remediation scan.

  • tenable-sc-create-remediation-scan

    Creates a remediation scan. Requires security manager role. This command is a prerequisite for creating remediation scan.

  • tenable-sc-create-scan

    Requires security manager role. Create a scan on Tenable.sc

  • tenable-sc-create-user

    Creates a new user. This command can be executed with both roles (admin or security manager) based on the role_id you choose.

  • tenable-sc-delete-asset

    Requires security manager role. Delete the asset with the given ID from Tenable.sc.

  • tenable-sc-delete-scan

    Requires security manager role. Delete a scan in Tenable.sc.

  • tenable-sc-delete-user

    Delete a user by given user_id. This command can be executed with both roles (admin or security manager).

  • tenable-sc-get-alert

    Requires security manager role. Get information about a given alert in Tenable.sc.

  • tenable-sc-get-all-scan-results

    Returns all scan results in Tenable.sc. Requires security manager role.

  • tenable-sc-get-asset

    Requires security manager role. Get details for a given asset in Tenable.sc.

  • tenable-sc-get-device

    Requires security manager role. Gets the specified device information.

  • tenable-sc-get-organization

    Requires administrator role. Command to get a list of organizations' information, depending on the comma-separated list of fields provided.

  • tenable-sc-get-scan-report

    Requires security manager role. Get a single report with Tenable.sc scan results. In case of `Importstatus = Error` (The results import wasn't finished), the vulnerabilities section will not be added to the results.

  • tenable-sc-get-scan-status

    Requires security manager role. Get the status of a specific scan in Tenable.sc.

  • tenable-sc-get-system-information

    Get the system information and diagnostics from Tenable.sc. Requires admin role.

  • tenable-sc-get-system-licensing

    Retrieve licensing information from Tenable.sc. Requires admin role.

  • tenable-sc-get-vulnerability

    Requires security manager role. Get details about a given vulnerability from a given Tenable.sc scan.

  • tenable-sc-launch-scan

    Requires security manager role. Launch an existing scan from Tenable.sc. Set polling to true to follow the scan and receive results when scan is over.

  • tenable-sc-list-alerts

    Requires security manager role. List alerts from Tenable.sc.

  • tenable-sc-list-assets

    Requires security manager role. Get a list of Tenable.sc assets.

  • tenable-sc-list-credentials

    Requires security manager role. Get a list of Tenable.sc credentials.

  • tenable-sc-list-groups

    List all groups. Requires security manager role.

  • tenable-sc-list-plugin-family

    List plugin families / return information about a plugin family given ID. Requires security manager role.

  • tenable-sc-list-policies

    Requires security manager role. Get a list of Tenable.sc scan policies.

  • tenable-sc-list-query

    Lists the queries. Requires security manager role.

  • tenable-sc-list-report-definitions

    Requires security manager role. Get a list of Tenable.sc report definitions.

  • tenable-sc-list-repositories

    Requires security manager role. Get a list of Tenable.sc scan repositories.

  • tenable-sc-list-scans

    Requires security manager role. Get a list of Tenable.sc existing scans.

  • tenable-sc-list-users

    List users in Tenable.sc. Results may vary based on the role type (admin or security manager).

  • tenable-sc-list-zones

    Requires admin role. Get a list of Tenable.sc scan zones.

  • tenable-sc-update-asset

    Requires security manager role. Update an asset.

  • tenable-sc-update-user

    Update user details of the given user_id.

import functools
import json
import re
import sys
import time
from datetime import datetime
from typing import Any

import demistomock as demisto  # noqa: F401
import pytz
import urllib3
from CommonServerPython import *  # noqa: F401
from requests import Session, cookies

# disable insecure warnings
urllib3.disable_warnings()


""" GLOBAL VARIABLES"""
ACTION_TYPE_TO_VALUE = {
    "notification": "users.username",
    "email": "users.username",
    "syslog": "host",
    "scan": "scan.name",
    "report": "report.name",
    "ticket": "assignee.username",
}

FIELDS_TO_INCLUDE = "id,name,description,type,ownerGroup,owner,tags,modifiedTime,restrictedIPs"
API_KEY = "API_KEY"
USERNAME_AND_PASSWORD = "USERNAME_AND_PASSWORD"
ROLE_ID_DICT = {
    "Administrator": "1",
    "Security Manager": "2",
    "Security Analyst": "3",
    "Vulnerability Analyst": "4",
    "Executive": "5",
    "Credential Manager": "6",
    "Auditor": "7",
}

# Asset and vulnerability fetch constants
VENDOR = "tenable"
PRODUCT = "sc"
ASSETS_PAGE_SIZE = 500
VULNS_PAGE_SIZE = 500
MIN_ASSETS_INTERVAL = 60  # Minimum minutes between full fetch cycles
XSIAM_EVENT_CHUNK_SIZE_LIMIT = 4 * (10**6)  # 4 MB
HOST_FIELDS = (
    "id,uuid,tenableUUID,name,ipAddress,os,firstSeen,lastSeen,"
    "macAddress,source,netBios,dns,acr,aes,repository,systemType,"
    "createdTime,modifiedTime"
)


class Client(BaseClient):
    def __init__(
        self,
        verify_ssl: bool = True,
        proxy: bool = False,
        user_name: str = "",
        password: str = "",
        access_key: str = "",
        secret_key: str = "",
        url: str = "",
    ):
        if not proxy:
            try:
                del os.environ["HTTP_PROXY"]
                del os.environ["HTTPS_PROXY"]
                del os.environ["http_proxy"]
                del os.environ["https_proxy"]
            except Exception as e:
                demisto.debug(f"encountered the following issue: {e}")

        self.url = f"{get_server_url(url)}/rest"
        self.verify_ssl = verify_ssl
        self.max_retries = 3
        self.headers: dict[str, Any] = {"Accept": "application/json", "Content-Type": "application/json"}
        self.auth_method = API_KEY
        if not (user_name and password) and not (secret_key and access_key):
            raise DemistoException("Please provide either user_name and password or secret_key and access_key")
        if secret_key and access_key:
            self.headers["x-apikey"] = f"accesskey={access_key}; secretkey={secret_key}"
            BaseClient.__init__(self, base_url=self.url, headers=self.headers, verify=verify_ssl, proxy=proxy)
            self.send_request = self.send_request_api_key_auth
        else:
            self.session = Session()
            integration_context = demisto.getIntegrationContext()
            self.token = integration_context.get("token")
            self.cookie = integration_context.get("cookie")
            self.user_name = user_name
            self.password = password
            self.send_request = self.send_request_username_and_password_auth
            self.auth_method = USERNAME_AND_PASSWORD
            if not self.token or not self.cookie:
                self.login()

    def __enter__(self):
        return self

    def send_request_api_key_auth(self, path, method="GET", body={}, params={}, headers=None):
        """
        Send the requests for access & secret keys authentication method.
        Args:
            path (str): The url suffix.
            method (str): The request method.
            body (dict): The request body.
            params (dict): The request params.
            headers (dict): The request headers.
        Returns:
            Dict: The response.
        """
        headers = headers or self.headers
        return self._http_request(method, url_suffix=path, params=params, data=json.dumps(body), headers=headers)

    def send_request_username_and_password_auth(self, path, method="GET", body=None, params=None, headers=None, try_number=1):
        """
        Send the requests for username & password authentication method.
        Args:
            path (str): The url suffix.
            method (str): The request method.
            body (dict): The request body.
            params (dict): The request params.
            headers (dict): The request headers.
            try_number (int): The request retries counter.
        Returns:
            Dict: The response.
        """
        body = body if body is not None else {}
        params = params if params is not None else {}
        headers = headers if headers is not None else self.headers

        headers["X-SecurityCenter"] = self.token
        url = f"{self.url}/{path}"

        session_cookie = cookies.create_cookie("TNS_SESSIONID", self.cookie)
        self.session.cookies.set_cookie(session_cookie)  # type: ignore

        res = self.session.request(method, url, data=json.dumps(body), params=params, headers=headers, verify=self.verify_ssl)

        if res.status_code == 403 and try_number <= self.max_retries:
            self.login()
            headers["X-SecurityCenter"] = self.token  # The Token is being updated in the login
            return self.send_request_username_and_password_auth(path, method, body, params, headers, try_number + 1)

        elif res.status_code < 200 or res.status_code >= 300:
            try:
                error = res.json()
            except Exception:
                raise DemistoException(
                    f"Error: Got status code {res.status_code!s} with {url=} with"
                    f" body {res.content} with headers {res.headers!s}"  # type: ignore[str-bytes-safe]
                )

            raise DemistoException(f"Error: Got an error from TenableSC, code: {error['error_code']}, \
                        details: {error['error_msg']}")  # type: ignore
        return res.json()

    def login(self):
        """
        Set the token for username & password authentication method.
        """
        login_body = {"username": self.user_name, "password": self.password}
        login_response = self.send_login_request(login_body)

        if "response" not in login_response:
            raise DemistoException("Error: Could not retrieve login token")

        token = login_response["response"].get("token")
        # There might be a case where the API does not return a token because there are too many sessions with the same user
        # In that case we need to add 'releaseSession = true'
        if not token:
            login_body["releaseSession"] = "true"
            login_response = self.send_login_request(login_body)
            if "response" not in login_response or "token" not in login_response["response"]:
                raise DemistoException("Error: Could not retrieve login token")
            token = login_response["response"]["token"]

        self.token = str(token)
        demisto.setIntegrationContext({"token": self.token})

    def send_login_request(self, login_body):
        """
        Send the request to login for username & password authentication method.
        Args:
            login_body (dict): The request body.
        Returns:
            Dict: The response.
        """
        url = f"{self.url}/token"

        headers = {"Accept": "application/json", "Content-Type": "application/json"}
        res = self.session.request("post", url, headers=headers, data=json.dumps(login_body), verify=self.verify_ssl)

        if res.status_code < 200 or res.status_code >= 300:
            raise DemistoException(f"Error: Got status code {res.status_code!s} with {url=} \
                        with body {res.content} with headers {res.headers!s}")  # type: ignore[str-bytes-safe]

        self.cookie = res.cookies.get("TNS_SESSIONID", self.cookie)
        demisto.setIntegrationContext({"cookie": self.cookie})

        return res.json()

    def __exit__(self, *args):
        """
        Send the request to logout for username & password authentication method.
        """
        if self.auth_method == USERNAME_AND_PASSWORD:
            self.send_request(path="token", method="DELETE")

    def create_scan(self, args: dict[str, Any]):
        """
        Send the request for create_scan_command and create_remediation_scan_command.
        Args:
            args (dict): The demisto.args() object.
        Returns:
            Dict: The response.
        """
        body = self.create_scan_body(args)

        return self.send_request(path="scan", method="POST", body=body)

    def create_scan_body(self, args):
        """
        Construct the body for the create_scan request.
        Args:
            args (dict): The demisto.args() object.
        Returns:
            Dict: The request body.
        """
        create_scan_mapping_dict = {
            "description": "description",
            "dhcpTracking": "dhcp_tracking",
            "timeoutAction": "timeout_action",
            "scanningVirtualHosts": "scan_virtual_hosts",
            "rolloverType": "rollover_type",
            "ipList": "ip_list",
        }
        body = {key: args.get(value) for key, value in create_scan_mapping_dict.items() if args.get(value)}

        scan_type = args.get("scan_type")
        body["type"] = scan_type if scan_type else ("policy" if args.get("policy_id") else "plugin")

        body["name"] = args.get("name") or args.get("scan_name")

        body["pluginID"] = args.get("plugin_id") or args.get("plugins_id")

        if repo_id := args.get("repository_id"):
            body["repository"] = {"id": repo_id}

        if policy_id := args.get("policy_id"):
            body["policy"] = {"id": policy_id}

        if zone_id := args.get("zone_id"):
            body["zone"] = {"id": zone_id}

        if report_ids := args.get("report_ids"):
            body["reports"] = [{"id": r_id, "reportSource": "individual"} for r_id in argToList(report_ids)]

        if asset_ids := args.get("asset_ids"):
            if str(asset_ids).startswith("All"):
                manageable = asset_ids == "AllManageable"
                res = self.get_assets(None)
                assets = get_elements(res["response"], manageable)
                asset_ids = [a["id"] for a in assets]
            body["assets"] = [{"id": a_id} for a_id in argToList(asset_ids)]

        if credentials := args.get("credentials"):
            body["credentials"] = [{"id": c_id} for c_id in argToList(credentials)]

        if max_scan_time := int(args.get("max_scan_time", "1")):
            body["maxScanTime"] = max_scan_time * 3600

        if schedule := args.get("schedule"):
            schedule_body = {"type": schedule}

            if dependent := args.get("dependent_id"):
                schedule_body["dependentID"] = dependent

            if schedule == "ical":
                start_time = args.get("start_time")
                repeat_rule_freq = args.get("repeat_rule_freq", "")
                repeat_rule_interval = int(args.get("repeat_rule_interval", 0))
                repeat_rule_by_day = argToList(args.get("repeat_rule_by_day", ""))
                timestamp_format = "%Y%m%dT%H%M%S"
                expected_format = "%Y-%m-%d:%H:%M:%S"
                try:
                    start_time = datetime.strptime(start_time, expected_format)
                    start_time = datetime.strftime(start_time, timestamp_format)
                except Exception:
                    start_time = parse_date_range(start_time, date_format=timestamp_format)[0]
                if time_zone := args.get("time_zone") and start_time:
                    schedule_body["start"] = f"TZID={time_zone}:{start_time}"
                else:
                    raise DemistoException("Please make sure to provide both time_zone and start_time.")
                if all([repeat_rule_freq, repeat_rule_interval, repeat_rule_by_day]):
                    schedule_body["repeatRule"] = f"FREQ={repeat_rule_freq};INTERVAL={repeat_rule_interval};"
                    f"BYDAY={repeat_rule_by_day}"
                elif repeat_rule_freq and repeat_rule_interval:
                    schedule_body["repeatRule"] = f"FREQ={repeat_rule_freq};INTERVAL={repeat_rule_interval}"
                elif any([repeat_rule_freq, repeat_rule_interval, repeat_rule_by_day]):
                    raise DemistoException(
                        "Please make sure to provide repeat_rule_freq, repeat_rule_interval with or without "
                        "repeat_rule_by_day, or don't provide any of them."
                    )
                schedule_body["enabled"] = argToBoolean(args.get("enabled", True))
            body["schedule"] = schedule_body

        remove_nulls_from_dictionary(body)
        return body

    def get_scan_results(self, scan_results_id):
        """
        Send the request for get_scan_status.
        Args:
            scan_results_id (str): The ID of the scan results to search.
        Returns:
            Dict: The response.
        """
        path = "scanResult/" + scan_results_id

        return self.send_request(path)

    def launch_scan(self, scan_id, scan_target):
        """
        Send the request for launch_scan_command and launch_scan_report_command.
        Args:
            scan_id (str): The ID of the scan to launch.
            scan_target (str): Optional body parameters.
        Returns:
            Dict: The response.
        """
        path = "scan/" + scan_id + "/launch"
        body = None
        if scan_target:
            body = {"diagnosticTarget": scan_target["address"], "diagnosticPassword": scan_target["password"]}

        return self.send_request(path, "post", body=body)

    def get_query(self, query_id):
        """
        Send the request for get_alert_command and list_query_command.
        Args:
            query_id (str): The ID of the query to retrieve.
        Returns:
            Dict: The response.
        """
        path = f"query/{query_id}"

        return self.send_request(path)

    def list_queries(self, type):
        """
        Send the request for list_query_command and list_queries.
        Args:
            type (str): The query type to retrieve.
        Returns:
            Dict: The response.
        """
        path = "query"
        params = {}
        if type:
            params["type"] = type

        return self.send_request(path=path, method="GET", params=params)

    def get_all_scan_results(self):
        """
        Send the request for get_all_scan_results_command.
        Returns:
            Dict: The response.
        """
        params = {
            "fields": "name,description,details,status,scannedIPs,startTime,scanDuration,importStart,"
            "finishTime,completedChecks,owner,ownerGroup,repository,importStatus"
        }
        return self.send_request(path="scanResult", params=params)

    def get_alerts(self, fields=None, alert_id=None):
        """
        Send the request for list_alerts_command and get_alert_command.
        Args:
            fields (str): The fields to include in the response.
            alert_id (str): The ID of the alert to search.
        Returns:
            Dict: The response.
        """
        path = "alert"
        params = {}  # type: Dict[str, Any]

        if alert_id:
            path += "/" + alert_id

        if fields:
            params = {"fields": fields}

        return self.send_request(path, params=params)

    def get_organization(self, fields=None):
        """
        Send the request for get_organization_command.
        Args:
            fields (str): The fields to include in the response.
        Returns:
            Dict: The response.
        """
        params = {}  # type: Dict[str, Any]

        if fields:
            params = {"fields": ",".join(fields)}

        return self.send_request(path="organization", params=params)

    def get_system_licensing(self):
        """
        Send the request for get_system_licensing_command.
        Returns:
            Dict: The response.
        """
        return self.send_request(path="status")

    def get_scans(self, fields):
        """
        Send the request for list_scans_command.
        Args:
            fields (str): The fields to include in the response.
        Returns:
            Dict: The response.
        """
        params = None

        if fields:
            params = {"fields": fields}

        return self.send_request(path="scan", params=params)

    def get_policies(self, fields):
        """
        Send the request for list_policies_command.
        Args:
            fields (str): The fields to include in the response.
        Returns:
            Dict: The response.
        """
        params = None

        if fields:
            params = {"fields": fields}

        return self.send_request(path="policy", params=params)

    def get_repositories(self):
        """
        Send the request for list_repositories_command.
        Returns:
            Dict: The response.
        """
        return self.send_request(path="repository")

    def get_assets(self, fields):
        """
        Send the request for list_assets_command and create_scan.
        Args:
            fields (str): The fields to include in the response.
        Returns:
            Dict: The response.
        """
        params = None

        if fields:
            params = {"fields": fields}

        return self.send_request(path="asset", params=params)

    def get_credentials(self):
        """
        Send the request for list_credentials_command.
        Returns:
            Dict: The response.
        """
        params = {"fields": FIELDS_TO_INCLUDE}

        return self.send_request(path="credential", params=params)

    def get_asset(self, asset_id):
        """
        Send the request for list_assets_command.
        Args:
            asset_id (str): The ID of the asset to search.
        Returns:
            Dict: The response.
        """
        params = {"fields": "id,name,description,status,createdTime,modifiedTime,viewableIPs,ownerGroup,tags,owner"}

        return self.send_request(path=f"asset/{asset_id}", params=params)

    def create_asset(self, name, description, owner_id, tags, ips):
        """
        Send the request for create_asset_command.
        Args:
            name (str): The name for the asset.
            description (str): The description for the asset.
            owner_id (str): The ID of the owner of the asset.
            tags (str): The tags for the asset.
            ips (str): The IP list for the asset.
        Returns:
            Dict: The response.
        """
        body = {"name": name, "definedIPs": ips, "type": "static"}

        if description:
            body["description"] = description

        if owner_id:
            body["ownerID"] = owner_id

        if tags:
            body["tags"] = tags

        return self.send_request(path="asset", method="POST", body=body)

    def delete_asset(self, asset_id):
        """
        Send the request for delete_asset_command.
        Args:
            asset_id (str): The ID of the asset to delete.
        Returns:
            Dict: The response.
        """
        return self.send_request(path=f"asset/{asset_id}", method="DELETE")

    def get_report_definitions(self, fields):
        """
        Send the request for list_report_definitions_command.
        Args:
            fields (str): The fields to include in the response.
        Returns:
            Dict: The response.
        """
        params = None

        if fields:
            params = {"fields": fields}

        return self.send_request(path="reportDefinition", params=params)

    def get_zones(self):
        """
        Send the request for list_zones_command.
        Returns:
            Dict: The response.
        """
        return self.send_request(path="zone")

    def get_scan_report(self, scan_results_id):
        """
        Send the request for get_scan_report_command and launch_scan_report_command.
        Args:
            scan_results_id (str): The ID of the scan_results to search for.
        Returns:
            Dict: The response.
        """
        path = "scanResult/" + scan_results_id

        params = {
            "fields": "name,description,details,status,scannedIPs,progress,startTime,scanDuration,importStart,"
            "finishTime,completedChecks,owner,ownerGroup,repository,policy,importStatus,running"
        }

        return self.send_request(path, params=params)

    def create_query(self, scan_id, tool):
        """
        Send the request for get_vulnerabilities.
        Args:
            scan_id (str): The ID of the scan_results to create the query for.
            tool (str): the tool to use.
        Returns:
            Dict: The response.
        """
        path = "query"

        body = {"name": f"scan {scan_id} query", "type": "vuln", "tool": tool, "scanID": scan_id}

        return self.send_request(path, method="POST", body=body)

    def delete_query(self, query_id):
        """
        Send the request for get_vulnerabilities.
        Args:
            query_id (str): The ID of the query to delete.
        Returns:
            Dict: The response.
        """
        if not query_id:
            raise DemistoException("query id returned None")
        path = "query/" + str(query_id)
        self.send_request(path, method="DELETE")

    def get_analysis(self, body=None, args={}):
        """
        Send the request for get_vulnerability_command and get_vulnerabilities.
        Args:
            args (dict): Either an empty dict if passed from get_vulnerabilities, otherwise, the demisto.results() object.
            body (str): The request body (if function is called from get_vulnerabilities).
        Returns:
            Dict: The response.
        """
        body = body or self.create_get_vulnerability_request_body(args)

        return self.send_request(path="analysis", method="POST", body=body)

    def create_get_vulnerability_request_body(self, args={}):
        """
        Create the body for the request made in get_analysis.
        Args:
            args (dict): Either an empty dict if passed from get_vulnerabilities, otherwise, the demisto.results() object.
        Returns:
            Dict: The prepared request body.
        """
        vuln_id = args.get("vulnerability_id")
        scan_results_id = args.get("scan_results_id")
        sort_field = args.get("sort_field", "severity")
        query_id = args.get("query_id")
        query = {"id": query_id}
        sort_direction = args.get("sort_direction", "ASC")
        source_type = args.get("source_type", "individual")
        page = int(args.get("page", "0"))
        limit = int(args.get("limit", "50"))
        if limit > 200:
            limit = 200
        body = {
            "type": "vuln",
            "view": "all",
            "sourceType": source_type,
            "startOffset": page,  # Lower bound for the results list (must be specified)
            "endOffset": page + limit,  # Upper bound for the results list (must be specified)
            "sortField": sort_field,
            "sortDir": sort_direction,
            "tool": "vulndetails",
        }
        if source_type == "individual":
            if scan_results_id:
                body["scanID"] = scan_results_id
            else:
                raise DemistoException("When choosing source_type = individual - scan_results_id must be provided.")
            vuln_filter = [{"filterName": "pluginID", "operator": "=", "value": vuln_id}]
            query["filters"] = vuln_filter
            query["tool"] = "vulndetails"
            query["type"] = "vuln"
        else:
            body["sourceType"] = source_type
            if not query_id:
                raise DemistoException(f"When choosing source_type = {source_type} - query_id must be provided.")
        body["query"] = query

        return body

    def get_system_diagnostics(self):
        """
        Send the request for get_system_information_command.
        Returns:
            Dict: The response.
        """
        return self.send_request(path="system/diagnostics")

    def get_system(self):
        """
        Send the request for get_system_information_command.
        Returns:
            Dict: The response.
        """
        return self.send_request(path="system")

    def list_groups(self, show_users):
        """
        Send the request for list_groups_command.
        Args:
            show_users (str): Optional filtering argument.
        Returns:
            Dict: The response.
        """
        params = {}
        if show_users:
            params["fields"] = "users"

        return self.send_request(path="group", method="GET", params=params)

    def get_vulnerability(self, vuln_id):
        """
        Send the request for get_vulnerability_command.
        Args:
            vuln_id (str): The ID of the vulnerability to search.
        Returns:
            Dict: The response.
        """
        path = f"plugin/{vuln_id}"

        params = {
            "fields": "name,description,family,type,cpe,riskFactor,solution,synopsis,exploitEase,exploitAvailable,"
            "cvssVector,baseScore,pluginPubDate,pluginModDate,vulnPubDate,temporalScore,xrefs,checkType"
        }

        return self.send_request(path, params=params)

    def delete_scan(self, scan_id):
        """
        Send the request for delete_scan_command.
        Args:
            scan_id (str): The ID of the scan to delete.
        Returns:
            Dict: The response.
        """
        return self.send_request(path=f"scan/{scan_id}", method="DELETE")

    def get_device(self, uuid, ip, dns_name, repo):
        """
        Send the request for get_device_command.
        Args:
            uuid (str): The UUID of the device to search.
            ip (str): Optional filtering argument.
            dns_name (str): Optional filtering argument.
            repo (str): Optional filtering argument.
        Returns:
            Dict: The response.
        """
        path, params = create_get_device_request_params_and_path(uuid, ip, dns_name, repo)

        return self.send_request(path, params=params)

    def get_users(
        self, fields="id,username,firstname,lastname,title,email,createdTime,modifiedTime,lastLogin,role", user_id=None
    ):
        """
        Send the request for list_users_command.
        Args:
            fields (str): The fields to include in the response.
            user_id (str): The ID of the user to search.
        Returns:
            Dict: The response.
        """
        path = "user"

        if user_id:
            path += "/" + user_id

        params = None

        if fields:
            params = {"fields": fields}

        return self.send_request(path, params=params)

    def create_user(self, args):
        """
        Send the request for create_user_command.
        Args:
            args (Dict): The demisto.args() object.
        Returns:
            Dict: The response.
        """
        body = create_user_request_body(args)

        return self.send_request(path="user", body=body, method="POST")

    def update_user(self, args, user_id):
        """
        Send the request for update_user_command.
        Args:
            args (Dict): The demisto.args() object.
            user_id (str): The ID of the user to update.
        Returns:
            Dict: The response.
        """
        body = create_user_request_body(args)

        return self.send_request(path=f"user/{user_id}", body=body, method="PATCH")

    def update_asset(self, args, asset_id):
        """
        Send the request for update_asset_command.
        Args:
            args (Dict): The demisto.args() object.
            asset_id (str): The ID of the asset to update.
        Returns:
            Dict: The response.
        """
        body = {
            "name": args.get("name"),
            "description": args.get("description"),
            "tags": args.get("tags"),
            "ownerID": args.get("owner_id"),
            "definedIPs": args.get("ip_list"),
        }
        remove_nulls_from_dictionary(body)
        return self.send_request(path=f"asset/{asset_id}", body=body, method="PATCH")

    def delete_user(self, user_id):
        """
        Send the request for delete_user_command.
        Args:
            user_id (str): The ID of the user to delete.
        Returns:
            Dict: The response.
        """
        return self.send_request(path=f"user/{user_id}", method="DELETE")

    def list_plugin_family(self, plugin_id, is_active):
        """
        Send the request for list_plugin_family_command.
        Args:
            plugin_id (str): The id of the plugin to get.
            is_active (str): Wether to filter by active / passive plugins.
        Returns:
            Dict: The response.
        """
        path = "pluginFamily"
        if plugin_id:
            path += f"/{plugin_id}"
        else:
            if is_active == "true":
                path += "?fields=active"
            elif is_active == "false":
                path += "?fields=passive"
        return self.send_request(path=path, method="GET")

    def create_policy(self, args):
        """
        Send the request for create_policy_command.
        Args:
            args (Dict): the demisto.args() object.
        Returns:
            Dict: The response.
        """
        body = create_policy_request_body(args)

        return self.send_request(path="policy", method="POST", body=body)

    def fetch_vulnerabilities_analysis(
        self,
        start_offset: int = 0,
        end_offset: int = 500,
    ) -> dict:
        """
        Fetch cumulative vulnerabilities via the analysis API for XSIAM ingestion.

        Uses sourceType=cumulative to get all known vulnerabilities across all scans.

        Args:
            start_offset: Starting offset for pagination.
            end_offset: Ending offset for pagination.

        Returns:
            Dict: The API response containing vulnerability analysis results.
        """
        body = {
            "type": "vuln",
            "sourceType": "cumulative",
            "view": "all",
            "wasVuln": "excludeWas",
            "startOffset": start_offset,
            "endOffset": end_offset,
            "sortField": "severity",
            "sortDir": "DESC",
            "tool": "vulndetails",
            "query": {
                "type": "vuln",
                "tool": "vulndetails",
                "filters": [],
            },
        }
        return self.send_request(path="analysis", method="POST", body=body)

    def search_hosts(
        self,
        fields: str,
        start_offset: int = 0,
        end_offset: int = 500,
        filters: dict | None = None,
    ) -> dict:
        """
        Search hosts using the /hosts/search endpoint with pagination.
        Args:
            fields: Comma-separated list of fields to return.
            start_offset: Starting offset for pagination.
            end_offset: Ending offset for pagination.
            filters: Optional filter payload for the request body.
        Returns:
            Dict: The API response containing totalRecords, returnedRecords, and results.
        """
        params = {
            "fields": fields,
            "startOffset": str(start_offset),
            "endOffset": str(end_offset),
            "pagination": "true",
        }
        body = filters or {}
        return self.send_request(path="hosts/search", method="POST", body=body, params=params)


""" HELPER FUNCTIONS """


def create_get_device_request_params_and_path(uuid: str, ip: str, dns_name: str, repo: str):
    """
    Construct the url suffix and params dict for get_device request.
    Args:
        uuid (str): UUID extracted from args.
        ip (str): IP extracted from args.
        dns_name (str): Dns extracted from args.
        repo (str): Repo name extracted from args.
    Returns:
        str: The url suffix for the request.
        Dict: The params for the request.
    """
    path = f"repository/{repo}/" if repo else ""
    path += "deviceInfo"
    params = {
        "fields": "ip,uuid,macAddress,netbiosName,dnsName,os,osCPE,lastScan,repository,total,severityLow,"
        "severityMedium,severityHigh,severityCritical"
    }
    if uuid:
        params["uuid"] = uuid
    else:
        params["ip"] = ip
        if dns_name:
            params["dnsName"] = dns_name
    return path, params


def create_policy_request_body(args: dict[str, Any]):
    """
    Construct the body for create_policy request.
    Args:
        args (Dict): The demisto.args() object.
    Returns:
        Dict: The body for the request.
    """
    body = {
        "name": args.get("policy_name"),
        "description": args.get("policy_description"),
        "context": "scan",
        "preferences": {
            "portscan_range": args.get("port_scan_range", "default"),
            "tcp_scanner": args.get("tcp_scanner", "no"),
            "syn_scanner": args.get("syn_scanner", "yes"),
            "udp_scanner": args.get("udp_scanner", "no"),
            "syn_firewall_detection": args.get("syn_firewall_detection", "Automatic (normal)"),
        },
        "policyTemplate": {"id": args.get("policy_template_id", "1")},
    }
    family = {"id": args.get("family_id", "")}
    if plugins_id := args.get("plugins_id"):
        family["plugins"] = [{"id": id for id in plugins_id.split(",")}]
    body["families"] = [family]
    remove_nulls_from_dictionary(body)
    return body


def create_user_request_body(args: dict[str, Any]):
    """
    Create user request body for update or create user commands.
    Args:
        args (Dict): the demisto.args() object.
    Returns:
        Dict: The request body.
    """
    user_query_mapping_dict: dict[str, str] = {
        "firstname": "first_name",
        "lastname": "last_name",
        "username": "user_name",
        "email": "email",
        "city": "city",
        "state": "state",
        "address": "address",
        "country": "country",
        "authType": "auth_type",
        "emailNotice": "email_notice",
        "phone": "phone",
        "locked": "locked",
        "mustChangePassword": "must_change_password",
        "currentPassword": "current_password",
        "password": "password",
        "groupID": "group_id",
        "responsibleAssetID": "responsible_asset_id",
    }
    body = {key: args.get(value) for key, value in user_query_mapping_dict.items() if args.get(value)}

    if role_id := args.get("role_id", ""):
        body["roleID"] = ROLE_ID_DICT.get(role_id, "")

    if args.get("managed_users_groups"):
        body["managedUsersGroups"] = [
            {"id": managed_users_group} for managed_users_group in args.get("managed_users_groups", "").split(",")
        ]
    if args.get("managed_objects_groups"):
        body["managedObjectsGroups"] = [
            {"id": int(managed_objects_group)} for managed_objects_group in args.get("managed_objects_groups", "").split(",")
        ]
    if time_zone := args.get("time_zone"):
        body["preferences"] = [{"name": "timezone", "value": time_zone, "tag": ""}]

    return body


def get_server_url(url):
    """
    Remove redundant '/' from the url the server url.
    For example: www.example.com/ - > www.example.com.
    Args:
        url (str): The server url.
    Returns:
        str: The server url.
    """
    url = re.sub(r"/[\/]+$/", "", url)
    url = re.sub(r"\/$", "", url)
    return url


def validate_user_body_params(args: dict[str, Any], command_type: str):
    """
    Validate all given arguments are valid according to the command type (update or create).
    Args:
        args (Dict): the demisto.args() object.
        command_type (Dict): the command type the function is called from (update or create)
    Returns:
        None: return error if arguments are invalid.
    """
    numbers_args_ls = ["group_id", "user_id", "responsible_asset_id"]

    time_zone = args.get("time_zone")
    password = args.get("password")
    email_notice = args.get("email_notice")
    email = args.get("email")
    auth_type = args.get("auth_type")

    for number_arg in numbers_args_ls:
        try:
            int(args.get(number_arg, "0"))
        except Exception:
            raise DemistoException(f"{number_arg} must be a valid number.")

    if time_zone and time_zone not in pytz.all_timezones:
        raise DemistoException(
            "Invalid time zone ID. Please choose one of the following: "
            "https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html"
        )

    if command_type == "create" and (auth_type == "Ldap" or auth_type == "saml"):
        args["must_change_password"] = "false"

    if password:
        if command_type == "update" and not args.get("current_password"):
            raise DemistoException("current_password must be provided when attempting to update password.")
        if len(password) < 3:
            raise DemistoException("Password length must be at least 3 characters.")

    if email and not re.compile(emailRegex).match(email):
        raise DemistoException(f"Error: The given email address: {email} is not in the correct format.")

    if command_type == "create" and not email_notice == "none" and not email:
        raise DemistoException("When email_notice is different from none, an email must be given as well.")


def timestamp_to_utc(timestamp_str, default_returned_value=""):
    """
    Convert timestamp string to UTC date time.
    Args:
        timestamp_str (str): timestamp string.
        default_returned_value (str): the default return value
    Returns:
        str: UTC date time string.
    """
    if timestamp_str and (int(timestamp_str) > 0):  # no value is when timestamp_str == '-1'
        return datetime.utcfromtimestamp(int(timestamp_str)).strftime("%Y-%m-%dT%H:%M:%SZ")
    return default_returned_value


def scan_duration_to_demisto_format(duration, default_returned_value=""):
    """
    Convert duration to demisto format time.
    Args:
        duration (str): Scan duration in tenable sc format.
        default_returned_value (str): the default return value
    Returns:
        Int / str: the scan duration in demisto format.
    """
    if duration:
        return float(duration) / 60
    return default_returned_value


""" FUNCTIONS """


def list_scans_command(client: Client, args: dict[str, Any]):
    """
    List scans.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_scans("id,name,description,policy,ownerGroup,owner")
    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No scans found")

    scans_dicts = get_elements(res["response"], manageable)

    if len(scans_dicts) == 0:
        raise DemistoException("No scans found")

    headers = ["ID", "Name", "Description", "Policy", "Group", "Owner"]

    mapped_scans = [
        {
            "Name": s["name"],
            "ID": s["id"],
            "Description": s["description"],
            "Policy": s["policy"].get("name"),
            "Group": s["ownerGroup"].get("name"),
            "Owner": s["owner"].get("username"),
        }
        for s in scans_dicts
    ]

    return CommandResults(
        outputs=createContext(mapped_scans, removeNull=True),
        outputs_prefix="TenableSC.Scan",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Scans", mapped_scans, headers, removeNull=True),
    )


def list_policies_command(client: Client, args: dict[str, Any]):
    """
    List policies.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_policies("id,name,description,tags,modifiedTime,owner,ownerGroup,policyTemplate")

    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No policies found")

    policies = get_elements(res["response"], manageable)

    if len(policies) == 0:
        raise DemistoException("No policies found")

    headers = ["ID", "Name", "Description", "Tag", "Type", "Group", "Owner", "LastModified"]

    mapped_policies = [
        {
            "ID": p["id"],
            "Name": p["name"],
            "Description": p["description"],
            "Tag": p["tags"],
            "Type": p.get("policyTemplate", {}).get("name"),
            "Group": p.get("ownerGroup", {}).get("name"),
            "Owner": p.get("owner", {}).get("username"),
            "LastModified": timestamp_to_utc(p["modifiedTime"]),
        }
        for p in policies
    ]

    return CommandResults(
        outputs=createContext(mapped_policies, removeNull=True),
        outputs_prefix="TenableSC.ScanPolicy",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Scan Policies", mapped_policies, headers, removeNull=True),
    )


def list_repositories_command(client: Client, args: dict[str, Any]):
    """
    List repositories.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_repositories()

    if not (res and res.get("response")):
        raise DemistoException("No repositories found")

    repositories = res["response"]

    if len(repositories) == 0:
        raise DemistoException("No repositories found")

    headers = ["ID", "Name", "Description"]

    mapped_repositories = [{"ID": r["id"], "Name": r["name"], "Description": r["description"]} for r in repositories]

    return CommandResults(
        outputs=createContext(mapped_repositories, removeNull=True),
        outputs_prefix="TenableSC.ScanRepository",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Scan Repositories", mapped_repositories, headers, removeNull=True),
    )


def list_credentials_command(client: Client, args: dict[str, Any]):
    """
    List credentials.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_credentials()

    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No credentials found")

    credentials = get_elements(res["response"], manageable)

    if len(credentials) == 0:
        raise DemistoException("No credentials found")

    headers = ["ID", "Name", "Description", "Type", "Tag", "Group", "Owner", "LastModified"]

    mapped_credentials = [
        {
            "ID": c["id"],
            "Name": c["name"],
            "Description": c["description"],
            "Type": c["type"],
            "Tag": c["tags"],
            "Group": c.get("ownerGroup", {}).get("name"),
            "Owner": c.get("owner", {}).get("name"),
            "LastModified": timestamp_to_utc(c["modifiedTime"]),
        }
        for c in credentials
    ]

    return CommandResults(
        outputs=createContext(mapped_credentials, removeNull=True),
        outputs_prefix="TenableSC.Credential",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Credentials", mapped_credentials, headers, removeNull=True),
    )


def list_assets_command(client: Client, args: dict[str, Any]):
    """
    List assets.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_assets("id,name,description,ipCount,type,tags,modifiedTime,groups,owner")

    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No assets found")

    assets = get_elements(res["response"], manageable)

    if len(assets) == 0:
        raise DemistoException("No assets found")

    headers = ["ID", "Name", "Tag", "Owner", "Group", "Type", "HostCount", "LastModified"]

    mapped_assets = [
        {
            "ID": a["id"],
            "Name": a["name"],
            "Tag": a["tags"],
            "Owner": a.get("owner", {}).get("username"),
            "Type": a["type"],
            "Group": a.get("ownerGroup", {}).get("name"),
            "HostCount": a["ipCount"],
            "LastModified": timestamp_to_utc(a["modifiedTime"]),
        }
        for a in assets
    ]

    return CommandResults(
        outputs=createContext(mapped_assets, removeNull=True),
        outputs_prefix="TenableSC.Asset",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Assets", mapped_assets, headers, removeNull=True),
    )


def get_asset_command(client: Client, args: dict[str, Any]):
    """
    Retrieve an asset by a given asset ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    asset_id = args.get("asset_id")

    res = client.get_asset(asset_id)

    if not res or "response" not in res:
        raise DemistoException("Asset not found")

    asset = res["response"]

    ips = []  # type: List[str]
    ip_lists = [v["ipList"] for v in asset.get("viewableIPs", "")]

    for ip_list in ip_lists:
        # Extract IPs
        ips += re.findall(r"[0-9]+(?:\.[0-9]+){3}", ip_list)

    headers = ["ID", "Name", "Description", "Tag", "Created", "Modified", "Owner", "Group", "IPs"]

    mapped_asset = {
        "ID": asset["id"],
        "Name": asset["name"],
        "Description": asset["description"],
        "Tag": asset["tags"],
        "Created": timestamp_to_utc(asset["createdTime"]),
        "Modified": timestamp_to_utc(asset["modifiedTime"]),
        "Owner": asset.get("owner", {}).get("username"),
        "Group": asset.get("ownerGroup", {}).get("name"),
        "IPs": ips,
    }

    return CommandResults(
        outputs=createContext(mapped_asset, removeNull=True),
        outputs_prefix="TenableSC.Asset",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Asset", mapped_asset, headers, removeNull=True),
    )


def create_asset_command(client: Client, args: dict[str, Any]):
    """
    Create an asset.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    name = args.get("name")
    description = args.get("description")
    owner_id = args.get("owner_id")
    tags = args.get("tags")
    ips = args.get("ip_list")

    res = client.create_asset(name, description, owner_id, tags, ips)

    if not res or "response" not in res:
        raise DemistoException("Error: Could not retrieve the asset")

    asset = res["response"]

    mapped_asset = {
        "ID": asset["id"],
        "Name": asset["name"],
        "OwnerName": asset["owner"].get("username"),
        "Tags": asset["tags"],
    }

    headers = ["ID", "Name", "OwnerName", "Tags"]

    return CommandResults(
        outputs=createContext(mapped_asset, removeNull=True),
        outputs_prefix="TenableSC.Asset",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Asset created successfully", mapped_asset, headers=headers, removeNull=True),
    )


def delete_asset_command(client: Client, args: dict[str, Any]):
    """
    Delete an asset by a given asset ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response and the human readable section.
    """
    asset_id = args.get("asset_id")

    res = client.delete_asset(asset_id)

    if not res:
        raise DemistoException("Error: Could not delete the asset")

    return CommandResults(raw_response=res, readable_output=f"Asset {asset_id} was deleted successfully.")


def list_report_definitions_command(client: Client, args: dict[str, Any]):
    """
    Lists report definitions.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_report_definitions("id,name,description,modifiedTime,type,ownerGroup,owner")

    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No report definitions found")

    reports = get_elements(res["response"], manageable)
    # Remove duplicates, take latest
    reports = [
        functools.reduce(
            lambda x, y: x if int(x["modifiedTime"]) > int(y["modifiedTime"]) else y, filter(lambda e: e["name"] == n, reports)
        )
        for n in {r["name"] for r in reports}
    ]

    if len(reports) == 0:
        raise DemistoException("No report definitions found")

    headers = ["ID", "Name", "Description", "Type", "Group", "Owner"]

    mapped_reports = [
        {
            "ID": r["id"],
            "Name": r["name"],
            "Description": r["description"],
            "Type": r["type"],
            "Group": r.get("ownerGroup", {}).get("name"),
            "Owner": r.get("owner", {}).get("username"),
        }
        for r in reports
    ]

    hr = tableToMarkdown("Tenable.sc Report Definitions", mapped_reports, headers, removeNull=True)
    for r in mapped_reports:
        del r["Description"]

    return CommandResults(
        outputs=createContext(mapped_reports, removeNull=True),
        outputs_prefix="TenableSC.ReportDefinition",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def list_zones_command(client: Client, args: dict[str, Any]):
    """
    Lists zones
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_zones()
    if not res or "response" not in res:
        raise DemistoException("No zones found")
    zones = res["response"]
    if len(zones) == 0:
        zones = [{"id": 0, "name": "All Zones", "description": "", "ipList": "", "activeScanners": ""}]
    headers = ["ID", "Name", "Description", "IPList", "activeScanners"]

    mapped_zones = [
        {
            "ID": z.get("id", ""),
            "Name": z.get("name", ""),
            "Description": z.get("description", ""),
            "IPList": z.get("ipList", ""),
            "activeScanners": z.get("activeScanners", ""),
        }
        for z in zones
    ]

    hr = tableToMarkdown("Tenable.sc Scan Zones", mapped_zones, headers, removeNull=True)

    mapped_scanners_total, found_ids = [], []
    for index, zone in enumerate(zones):
        if scanners := zone.get("scanners"):
            mapped_scanners = [
                {"ID": scanner["id"], "Name": scanner["name"], "Description": scanner["description"], "Status": scanner["status"]}
                for scanner in scanners
            ]
            mapped_zones[index]["Scanner"] = mapped_scanners
            for scanner in mapped_scanners:
                if scanner.get("ID") not in found_ids:
                    found_ids.append(scanner.get("ID"))
                    mapped_scanners_total.append(scanner)
        headers = ["ID", "Name", "Description", "Status"]

    if mapped_scanners_total:
        hr += tableToMarkdown("Tenable.sc Scanners", mapped_scanners_total, headers, removeNull=True)

    return CommandResults(
        outputs=createContext(mapped_zones, removeNull=True),
        outputs_prefix="TenableSC.ScanZone",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def get_elements(elements, manageable):
    """
    Extracts a list from the given dictionary by given filter
    Args:
        elements (Dict): The dictionary to extract from
        manageable (str): Wether to retrieve manageable or usable list
    Returns:
        List: The desired extracted list.
    """
    if manageable == "false":
        return elements.get("usable", [])

    return elements.get("manageable", [])


def create_scan_command(client: Client, args: dict[str, Any]):
    """
    Creates a scan.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    validate_create_scan_inputs(args)

    res = client.create_scan(args)

    if not res or "response" not in res:
        raise DemistoException("Error: Could not retrieve the scan")

    scan = res["response"]

    headers = ["ID", "CreatorID", "Name", "Type", "CreationTime", "OwnerName", "Reports"]

    mapped_scan = {
        "ID": scan["id"],
        "CreatorID": scan["creator"].get("id"),
        "Name": scan["name"],
        "Type": scan["type"],
        "CreationTime": timestamp_to_utc(scan["createdTime"]),
        "OwnerName": scan["owner"].get("name"),
        "Reports": demisto.dt(scan["reports"], "id"),
    }

    return CommandResults(
        outputs=createContext(mapped_scan, removeNull=True),
        outputs_prefix="TenableSC.Scan",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Scan created successfully", mapped_scan, headers, removeNull=True),
    )


def validate_create_scan_inputs(args: dict[str, Any]):
    """
    Validate all given arguments are valid for create scan command.
    Args:
        args (Dict): the demisto.args() object.
    Returns:
        None: return error if arguments are invalid.
    """
    schedule = args.get("schedule")
    asset_ids = args.get("asset_ids")
    ips = args.get("ip_list")
    dependent = args.get("dependent_id")
    time_zone = args.get("time_zone")

    if time_zone and time_zone not in pytz.all_timezones:
        raise DemistoException(
            "Invalid time zone ID. Please choose one of the following: "
            "https://docs.oracle.com/middleware/1221/wcs/tag-ref/MISC/TimeZones.html"
        )
    if not asset_ids and not ips:
        raise DemistoException("Error: Assets and/or IPs must be provided")

    if schedule == "dependent" and not dependent:
        raise DemistoException("Error: Dependent schedule must include a dependent scan ID")


def process_launch_scan_response(res: dict[str, Any]):
    """
    Process the launch scan response.
    Args:
        res (Dict): the launch scan response.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    scan_result = res["response"]["scanResult"]

    headers = ["Name", "ID", "OwnerID", "JobID", "Status"]

    mapped_scan = {
        "Name": scan_result["name"],
        "ID": scan_result["id"],
        "OwnerID": scan_result["ownerID"],
        "JobID": scan_result["jobID"],
        "Status": scan_result["status"],
    }

    return CommandResults(
        outputs=createContext(mapped_scan, removeNull=True),
        outputs_prefix="TenableSC.ScanResults",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Scan", mapped_scan, headers, removeNull=True),
    )


@polling_function(
    name="tenable-sc-launch-scan",
    requires_polling_arg=True,
    poll_message="Scan in progress.",
    timeout=arg_to_number(demisto.args().get("timeout_in_seconds", "10800")),
)
def launch_scan_command(args: dict[str, Any], client: Client):
    """
    Polling command. Launch a scan by a given scan ID, following the scan status and retrieve the scan report.
    Args:
        args (Dict): demisto.args() object.
        client (Client): The tenable.sc client object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    first_execution = not args.get("scan_results_id")
    if first_execution:
        res = launch_scan(client, args)
        if not argToBoolean(args.get("polling", "false")):
            return PollResult(process_launch_scan_response(res))
        scan_results_id = res.get("response", {}).get("scanResult", {}).get("id")
        args["scan_results_id"] = scan_results_id
        demisto.info(f"Running poll command for results id: {scan_results_id}")
    else:
        scan_results_id = args.get("scan_results_id")
        args["hide_polling_output"] = True
    scan_results, _ = get_scan_status(client, args)
    scan_status = scan_results[0].get("status")
    if scan_status == "Error":
        raise DemistoException(f"Encountered the following error during the execution {scan_results[0].get('errorDetails')}")
    elif scan_status != "Completed":
        return PollResult(continue_to_poll=True, response=scan_results, args_for_next_run=args)
    else:
        return PollResult(get_scan_report_command(client, args))


def launch_scan(client: Client, args: dict[str, Any]):
    """
    Launching a scan with a given scan ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        Dict: The response for the launch scan request.
    """
    scan_id = args.get("scan_id")
    target_address = args.get("diagnostic_target")
    target_password = args.get("diagnostic_password")

    if (target_address and not target_password) or (target_password and not target_address):
        raise DemistoException("Error: If one of diagnostic target or password is provided, both of them must be provided.")

    res = client.launch_scan(scan_id, {"address": target_address, "password": target_password})

    if not res or "response" not in res or not res["response"] or "scanResult" not in res["response"]:
        raise DemistoException("Error: Could not retrieve the scan.")

    return res


def get_scan_status_command(client: Client, args: dict[str, Any]):
    """
    Return information about the scan status by a given scan results ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    scans_results, res = get_scan_status(client, args)

    headers = ["ID", "Name", "Status", "Description", "Error"]

    mapped_scans_results = [
        {
            "ID": scan_result["id"],
            "Name": scan_result["name"],
            "Status": scan_result["status"],
            "Description": scan_result["description"],
            "Error": scan_result["errorDetails"] if scan_result["status"] == "Error" else "",
        }
        for scan_result in scans_results
    ]

    return CommandResults(
        outputs=createContext(mapped_scans_results, removeNull=True),
        outputs_prefix="TenableSC.ScanResults",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Scan Status", mapped_scans_results, headers, removeNull=True),
    )


def get_scan_status(client: Client, args: dict[str, Any]):
    """
    Return information about the scan status by a given scan results ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        Dict: The relevant extracted section from the response.
        Dict: The response.
    """
    scan_results_ids = argToList(args.get("scan_results_id"))

    scans_results = []
    for scan_results_id in scan_results_ids:
        res = client.get_scan_results(scan_results_id)
        if not (res and res.get("response")):
            raise DemistoException("Scan results not found")

        scans_results.append(res["response"])
    return scans_results, res


def get_scan_report_command(client: Client, args: dict[str, Any]):
    """
    Return scan report information by a given scan results ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    scan_results_id = args.get("scan_results_id")
    vulnerabilities_to_get = argToList(args.get("vulnerability_severity", []))

    res = client.get_scan_report(scan_results_id)

    if not (res and res.get("response")):
        raise DemistoException("Scan results not found")

    scan_results = res["response"]

    headers = [
        "ID",
        "Name",
        "Description",
        "Policy",
        "Group",
        "Owner",
        "ScannedIPs",
        "StartTime",
        "EndTime",
        "Duration",
        "Checks",
        "ImportTime",
        "RepositoryName",
        "Status",
        "Scan Type",
        "ImportStatus",
        "IsScanRunning",
        "CompletedIPs",
    ]
    vuln_headers = ["ID", "Name", "Family", "Severity", "Total"]

    mapped_results = {
        "Scan Type": res.get("type", ""),
        "ID": scan_results.get("id", ""),
        "Name": scan_results.get("name", ""),
        "Status": scan_results.get("status", ""),
        "Description": scan_results.get("description", ""),
        "Policy": scan_results.get("details", ""),
        "Group": scan_results.get("ownerGroup", {}).get("name"),
        "Checks": scan_results.get("completedChecks", ""),
        "StartTime": timestamp_to_utc(scan_results.get("startTime", "")),
        "EndTime": timestamp_to_utc(scan_results.get("finishTime", "")),
        "Duration": scan_duration_to_demisto_format(scan_results.get("scanDuration", "")),
        "ImportTime": timestamp_to_utc(scan_results.get("importStart", "")),
        "ScannedIPs": scan_results.get("scannedIPs", ""),
        "Owner": scan_results.get("owner", {}).get("username", ""),
        "RepositoryName": scan_results.get("repository", {}).get("name", ""),
        "ImportStatus": scan_results.get("importStatus", ""),
        "IsScanRunning": scan_results.get("running", ""),
    }

    if progress := scan_results.get("progress", {}):
        mapped_results["Completed IPs"] = progress.get("completedIPs", "")

    hr = tableToMarkdown("Tenable.sc Scan " + mapped_results["ID"] + " Report", mapped_results, headers, removeNull=True)

    if len(vulnerabilities_to_get) > 0 and scan_results.get("importStatus", "") != "Error":
        vulns = get_vulnerabilities(client, scan_results_id)

        if isinstance(vulns, list):
            vulnerabilities = list(filter(lambda v: v["Severity"] in vulnerabilities_to_get, vulns))
            if vulnerabilities and len(vulnerabilities) > 0:
                hr += tableToMarkdown("Vulnerabilities", vulnerabilities, vuln_headers, removeNull=True)
                mapped_results["Vulnerability"] = vulnerabilities

    return CommandResults(
        outputs=createContext(mapped_results, removeNull=True),
        outputs_prefix="TenableSC.ScanResults",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def get_vulnerabilities(client: Client, scan_results_id):
    """
    Lists vulnerabilities from a scan by a given scan results ID.
    Args:
        client (Client): The tenable.sc client object.
        scan_results_id (str): The ID of the scan results to get the information from.
    Returns:
        List: Sorted vulnerabilities list.
    """
    query = client.create_query(scan_results_id, "vulnipdetail")

    if not query or "response" not in query:
        return "Could not get vulnerabilites query"

    body = {
        "type": "vuln",
        "view": "all",
        "sourceType": "individual",
        "scanID": scan_results_id,
        "query": {"id": query.get("response", {}).get("id")},
    }

    analysis = client.get_analysis(body=body)

    client.delete_query(query.get("response", {}).get("id"))

    if not analysis or "response" not in analysis:
        return "Could not get vulnerabilites analysis"

    results = analysis["response"]["results"]

    if not results or len(results) == 0:
        return "No vulnerabilities found"

    mapped_vulns = []

    for vuln in results:
        mapped_vuln = {
            "ID": vuln["pluginID"],
            "Name": vuln["name"],
            "Description": vuln["pluginDescription"],
            "Family": vuln["family"].get("name"),
            "Severity": vuln["severity"].get("name"),
            "Total": vuln["total"],
        }

        mapped_vulns.append(mapped_vuln)

    sv_level = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1, "Info": 0}

    mapped_vulns.sort(key=lambda r: sv_level[r["Severity"]])

    return mapped_vulns


def get_vulnerability_command(client: Client, args: dict[str, Any]):
    """
    Return information about a vulnerability by a given vulnerability ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    vuln_id = args.get("vulnerability_id")
    scan_results_id = args.get("scan_results_id")

    analysis = client.get_analysis(args=args)

    if not analysis or "response" not in analysis:
        raise DemistoException("Error: Could not get vulnerability analysis")

    results = analysis["response"]["results"]

    if not results or len(results) == 0:
        raise DemistoException("Error: Vulnerability not found in the scan results")

    vuln_response = client.get_vulnerability(vuln_id)

    if not vuln_response or "response" not in vuln_response:
        raise DemistoException("Vulnerability not found")

    vuln = vuln_response["response"]
    vuln["severity"] = results[0]["severity"]  # The vulnerability severity is the same in all the results

    hosts = get_vulnerability_hosts_from_analysis(results)

    cves = None
    cves_output = []  # type: List[dict]
    if vuln.get("xrefs"):
        # Extract CVE
        cve_filter = list(filter(lambda x: x.strip().startswith("CVE"), vuln["xrefs"].split(",")))
        if cve_filter and len(cve_filter) > 0:
            cves = [c.replace("CVE:", "").strip() for c in cve_filter]
            cves_output += ({"ID": c} for c in cves)

    mapped_vuln = {
        "ID": vuln["id"],
        "Name": vuln["name"],
        "Description": vuln["description"],
        "Type": vuln["type"],
        "Severity": vuln.get("severity", {}).get("name"),
        "Synopsis": vuln["synopsis"],
        "Solution": vuln["solution"],
    }

    vuln_info = {"Published": timestamp_to_utc(vuln["vulnPubDate"]), "CPE": vuln["cpe"], "CVE": cves}

    exploit_info = {"ExploitAvailable": vuln["exploitAvailable"], "ExploitEase": vuln["exploitEase"]}

    risk_info = {
        "RiskFactor": vuln["riskFactor"],
        "CVSSBaseScore": vuln["baseScore"],
        "CVSSTemporalScore": vuln["temporalScore"],
        "CVSSVector": vuln["cvssVector"],
    }

    plugin_details = {
        "Family": vuln["family"].get("name"),
        "Published": timestamp_to_utc(vuln["pluginPubDate"]),
        "Modified": timestamp_to_utc(vuln["pluginModDate"]),
        "CheckType": vuln["checkType"],
    }

    hr = "## Vulnerability: {} ({})\n".format(mapped_vuln["Name"], mapped_vuln["ID"])
    hr += "### Synopsis\n{}\n### Description\n{}\n### Solution\n{}\n".format(
        mapped_vuln["Synopsis"], mapped_vuln["Description"], mapped_vuln["Solution"]
    )
    hr += tableToMarkdown("Hosts", hosts, removeNull=True)
    hr += tableToMarkdown("Risk Information", risk_info, removeNull=True)
    hr += tableToMarkdown("Exploit Information", exploit_info, removeNull=True)
    hr += tableToMarkdown("Plugin Details", plugin_details, removeNull=True)
    hr += tableToMarkdown("Vulnerability Information", vuln_info, removeNull=True)

    mapped_vuln.update(vuln_info)
    mapped_vuln.update(exploit_info)
    mapped_vuln.update(risk_info)
    mapped_vuln["PluginDetails"] = plugin_details
    mapped_vuln["Host"] = hosts

    scan_result = {
        "ID": scan_results_id,
        "Vulnerability": mapped_vuln,
    }
    command_results = [
        CommandResults(
            outputs=createContext(scan_result["Vulnerability"], removeNull=True),
            outputs_prefix="TenableSC.ScanResults.Vulnerability",
            raw_response=vuln_response,
            outputs_key_field="ID",
            readable_output=hr,
        )
    ]

    if len(cves_output) > 0:
        command_results.append(CommandResults(outputs=createContext(cves_output), outputs_prefix="CVE", outputs_key_field="ID"))

    return command_results


def get_vulnerability_hosts_from_analysis(results):
    """
    Lists the vulnerability hosts from given analysis.
    Args:
        results (Dict): The analysis results.
    Returns:
        List: list of all the vulnerability hosts extracted from the results.
    """
    return [
        {"IP": host.get("ip"), "MAC": host.get("macAddress"), "Port": host.get("port"), "Protocol": host.get("protocol")}
        for host in results
    ]


def delete_scan_command(client: Client, args: dict[str, Any]):
    """
    Deletes a scan.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, and the human readable section.
    """
    scan_id = args.get("scan_id")

    res = client.delete_scan(scan_id)

    if not res:
        raise DemistoException("Error: Could not delete the scan")

    return CommandResults(raw_response=res, readable_output=f"Scan {scan_id} was deleted successfully.")


def get_device_command(client: Client, args: dict[str, Any]):
    """
    Returns device info by a given device UUID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    uuid = args.get("uuid")
    ip = args.get("ip")
    dns_name = args.get("dns_name")
    repo = args.get("repository_id")

    res = client.get_device(uuid, ip, dns_name, repo)

    if not res or "response" not in res:
        raise DemistoException("Device not found")

    device = res["response"]

    headers = [
        "IP",
        "UUID",
        "MacAddress",
        "RepositoryID",
        "RepositoryName",
        "NetbiosName",
        "DNSName",
        "OS",
        "OsCPE",
        "LastScan",
        "TotalScore",
        "LowSeverity",
        "MediumSeverity",
        "HighSeverity",
        "CriticalSeverity",
    ]

    mapped_device = {
        "IP": device["ip"],
        "UUID": device.get("uuid"),
        "MacAddress": device.get("macAddress"),
        "RepositoryID": device.get("repository", {}).get("id"),
        "RepositoryName": device.get("repository", {}).get("name"),
        "NetbiosName": device.get("netbiosName"),
        "DNSName": device.get("dnsName"),
        "OS": re.sub("<[^<]+?>", " ", device["os"]).lstrip() if device.get("os") else "",
        "OsCPE": device.get("osCPE"),
        "LastScan": timestamp_to_utc(device.get("lastScan")),
        "TotalScore": device.get("total"),
        "LowSeverity": device.get("severityLow"),
        "MediumSeverity": device.get("severityMedium"),
        "HighSeverity": device.get("severityHigh"),
        "CriticalSeverity": device.get("severityCritical"),
    }

    endpoint = {
        "IPAddress": mapped_device["IP"],
        "MACAddress": mapped_device["MacAddress"],
        "Hostname": mapped_device["DNSName"],
        "OS": mapped_device["OS"],
    }

    command_results = [
        CommandResults(
            outputs=createContext(mapped_device, removeNull=True),
            outputs_prefix="TenableSC.Device",
            raw_response=res,
            outputs_key_field="UUID",
            readable_output=tableToMarkdown("Tenable.sc Device", mapped_device, headers=headers, removeNull=True),
        ),
        CommandResults(outputs=createContext(endpoint, removeNull=True), outputs_prefix="Endpoint", outputs_key_field="IP"),
    ]

    return command_results


def list_users_command(client: Client, args: dict[str, Any]):
    """
    Lists all users.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    user_id = args.get("id")
    username = args.get("username")
    email = args.get("email")

    res = client.get_users("id,username,firstname,lastname,title,email,createdTime,modifiedTime,lastLogin,role", user_id)

    if not res or "response" not in res:
        raise DemistoException("No users found")

    users = res["response"]

    if not isinstance(users, list):
        users = [users]

    if not user_id:
        if username:
            users = list(filter(lambda u: u["username"] == username, users))
        elif email:
            users = list(filter(lambda u: u["email"] == email, users))

    if len(users) == 0:
        raise DemistoException("No users found")

    headers = ["ID", "Username", "Firstname", "Lastname", "Title", "Email", "Created", "Modified", "LastLogin", "Role"]

    mapped_users = [
        {
            "ID": user["id"],
            "Username": user["username"],
            "FirstName": user["firstname"],
            "LastName": user["lastname"],
            "Title": user["title"],
            "Email": user["email"],
            "Created": timestamp_to_utc(user["createdTime"]),
            "Modified": timestamp_to_utc(user["modifiedTime"]),
            "LastLogin": timestamp_to_utc(user["lastLogin"]),
            "Role": user["role"].get("name"),
        }
        for user in users
    ]

    return CommandResults(
        outputs=createContext(mapped_users, removeNull=True),
        outputs_prefix="TenableSC.User",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Users", mapped_users, headers=headers, removeNull=True),
    )


def get_system_licensing_command(client: Client, args: dict[str, Any]):
    """
    Returns system licensing information.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_system_licensing()

    if not res or "response" not in res:
        raise DemistoException("Error: Could not retrieve system licensing")

    status = res["response"]

    mapped_licensing = {
        "License": status["licenseStatus"],
        "LicensedIPS": status["licensedIPs"],
        "ActiveIPS": status["activeIPs"],
    }

    headers = ["License", "LicensedIPS", "ActiveIPS"]

    return CommandResults(
        outputs=createContext(mapped_licensing, removeNull=True),
        outputs_prefix="TenableSC.Status",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Licensing information", mapped_licensing, headers=headers, removeNull=True),
    )


def get_system_information_command(client: Client, args: dict[str, Any]):
    """
    Return system information.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    sys_res = client.get_system()

    if not sys_res or "response" not in sys_res:
        raise DemistoException("Error: Could not retrieve system information")

    diag_res = client.get_system_diagnostics()

    if not diag_res or "response" not in diag_res:
        raise DemistoException("Error: Could not retrieve system information")

    sys_res.update(diag_res)
    diagnostics = diag_res["response"]
    system = sys_res["response"]

    mapped_information = {
        "Version": system.get("version"),
        "BuildID": system.get("buildID"),
        "ReleaseID": system.get("releaseID"),
        "License": system.get("licenseStatus"),
        "RPMStatus": diagnostics.get("statusRPM"),
        "JavaStatus": diagnostics.get("statusJava"),
        "DiskStatus": diagnostics.get("statusDisk"),
        "DiskThreshold": diagnostics.get("statusThresholdDisk"),
        "LastCheck": timestamp_to_utc(diagnostics.get("statusLastChecked")),
    }

    headers = [
        "Version",
        "BuildID",
        "ReleaseID",
        "License",
        "RPMStatus",
        "JavaStatus",
        "DiskStatus",
        "DiskThreshold",
        "LastCheck",
    ]

    return CommandResults(
        outputs=createContext(mapped_information, removeNull=True),
        outputs_prefix="TenableSC.System",
        raw_response=sys_res,
        outputs_key_field="BuildID",
        readable_output=tableToMarkdown("Tenable.sc System information", mapped_information, headers=headers, removeNull=True),
    )


def list_alerts_command(client: Client, args: dict[str, Any]):
    """
    Lists all alerts.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_alerts(
        fields="id,name,description,didTriggerLastEvaluation,lastTriggered,action,lastEvaluated,ownerGroup,owner"
    )
    manageable = args.get("manageable", "false").lower()

    if not (res and res.get("response")):
        raise DemistoException("No alerts found")

    alerts = get_elements(res["response"], manageable)

    if len(alerts) == 0:
        raise DemistoException("No alerts found")

    headers = ["ID", "Name", "Actions", "State", "LastTriggered", "LastEvaluated", "Group", "Owner"]
    mapped_alerts = [
        {
            "ID": a["id"],
            "Name": a["name"],
            "State": "Triggered" if a["didTriggerLastEvaluation"] == "true" else "Not Triggered",
            "Actions": demisto.dt(a["action"], "type"),
            "LastTriggered": timestamp_to_utc(a["lastTriggered"], default_returned_value="Never"),
            "LastEvaluated": timestamp_to_utc(a["lastEvaluated"]),
            "Group": a["ownerGroup"].get("name"),
            "Owner": a["owner"].get("username"),
        }
        for a in alerts
    ]

    return CommandResults(
        outputs=createContext(mapped_alerts, removeNull=True),
        outputs_prefix="TenableSC.Alert",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Alerts", mapped_alerts, headers=headers, removeNull=True),
    )


def get_alert_command(client: Client, args: dict[str, Any]):
    """
    Return information about an alert by a given alert ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    alert_id = args.get("alert_id")
    res = client.get_alerts(alert_id=alert_id)

    if not (res and res.get("response")):
        raise DemistoException("Alert not found")

    alert = res["response"]
    query_res = client.get_query(alert.get("query", {}).get("id"))
    query = query_res.get("response")

    alert_headers = ["ID", "Name", "Description", "LastTriggered", "State", "Behavior", "Actions"]
    query_headers = ["Trigger", "Query"]
    action_headers = ["Type", "Values"]

    filter_headers = ["Name", "Values"]
    mapped_alert = {
        "ID": alert["id"],
        "Name": alert["name"],
        "Description": alert["description"],
        "LastTriggered": timestamp_to_utc(alert["lastTriggered"], default_returned_value="Never"),
        "State": "Triggered" if alert["didTriggerLastEvaluation"] == "true" else "Not Triggered",
        "Behavior": "Execute on every trigger " if alert["executeOnEveryTrigger"] == "true" else "Execute only on first trigger",
    }

    mapped_condition = {
        "Trigger": "{} {} {}".format(alert["triggerName"], alert["triggerOperator"], alert["triggerValue"]),
        "Query": alert["query"].get("name"),
    }

    mapped_filters = None
    if query:
        mapped_filters = [
            {"Name": f["filterName"], "Values": demisto.dt(f["value"], "name") if isinstance(f["value"], list) else f["value"]}
            for f in query.get("filters", [])
        ]
        mapped_condition["Filter"] = mapped_filters

    mapped_actions = [
        {"Type": a["type"], "Values": demisto.dt(a, "{}.{}".format("definition", ACTION_TYPE_TO_VALUE[a["type"]]))}
        for a in alert["action"]
    ]

    hr = tableToMarkdown("Tenable.sc Alert", mapped_alert, headers=alert_headers, removeNull=True)
    hr += tableToMarkdown("Condition", mapped_condition, headers=query_headers, removeNull=True)
    if mapped_filters:
        hr += tableToMarkdown("Filters", mapped_filters, headers=filter_headers, removeNull=True)
    if mapped_actions:
        hr += tableToMarkdown("Actions", mapped_actions, headers=action_headers, removeNull=True)
        mapped_alert["Action"] = mapped_actions

    mapped_alert["Condition"] = mapped_condition

    return CommandResults(
        outputs=createContext(mapped_alert, removeNull=True),
        outputs_prefix="TenableSC.Alert",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def get_organization_command(client: Client, args: dict[str, Any]):
    """
    Returns organization information.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    fields = argToList(args.get("fields", None))
    res = client.get_organization(fields)

    if not res or "response" not in res:
        raise DemistoException("Error: Could not retrieve organization information")

    response = res.get("response", {})
    res_output = []
    if response:
        for curr_res in response:
            restrictedIPMap = {
                "ID": curr_res.get("id", ""),
                "Name": curr_res.get("name", ""),
            }
            for field in fields:
                restrictedIPMap[field] = curr_res.get(field, "")
            res_output.append(restrictedIPMap)

    return CommandResults(
        outputs=createContext(response, removeNull=True),
        outputs_prefix="TenableSC.Organization",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Tenable.sc Orgnization", res_output, removeNull=True),
    )


def fetch_incidents(client: Client, first_fetch: str = "3 days"):
    """
    fetches incidents and upload them to demisto.incidents().
    Args:
        client (Client): The tenable.sc client object.
        first_fetch (str): The first_fetch integration param.
    """
    incidents = []
    last_run = demisto.getLastRun()
    if not last_run:
        last_run = {}
    if "time" not in last_run:
        # get timestamp in seconds
        timestamp, _ = parse_date_range(first_fetch, to_timestamp=True)
        timestamp /= 1000
    else:
        timestamp = last_run["time"]

    max_timestamp = timestamp
    res = client.get_alerts(
        fields="id,name,description,lastTriggered,triggerName,triggerOperator,"
        "triggerValue,action,query,owner,ownerGroup,schedule,canManage"
    )

    alerts = get_elements(res.get("response", {}), manageable="false")
    for alert in alerts:
        # 0 corresponds to never triggered
        if int(alert.get("lastTriggered", 0)) > timestamp:
            incidents.append(
                {
                    "name": "Tenable.sc Alert Triggered - " + alert["name"],
                    "occurred": timestamp_to_utc(alert["lastTriggered"]),
                    "rawJSON": json.dumps(alert),
                }
            )

            if int(alert["lastTriggered"]) > max_timestamp:
                max_timestamp = int(alert["lastTriggered"])

    demisto.incidents(incidents)
    demisto.setLastRun({"time": max_timestamp})


def list_groups_command(client: Client, args: dict[str, Any]):
    """
    Lists all groups
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    show_users = argToBoolean(args.get("show_users", True))
    limit = int(args.get("limit", "50"))
    res = client.list_groups(show_users)
    if not res or not res.get("response", []):
        raise DemistoException("No groups found")
    groups = res.get("response", [])
    if len(groups) > limit:
        groups = groups[:limit]
    mapped_groups = [
        {"ID": group.get("id"), "Name": group.get("name"), "Description": group.get("description")} for group in groups
    ]
    headers = ["ID", "Name", "Description"]
    hr = tableToMarkdown("Tenable.sc groups", mapped_groups, headers, removeNull=True)
    if show_users:
        headers = ["Username", "Firstname", "Lastname"]
        users = []
        for index, group in enumerate(groups):
            users = [
                {
                    "Username": user.get("username", ""),
                    "Firstname": user.get("firstname", ""),
                    "Lastname": user.get("lastname", ""),
                    "ID": user.get("id", ""),
                    "UUID": user.get("UUID", ""),
                }
                for user in group.get("users")
            ]
            mapped_groups[index]["Users"] = users
            group_id = group.get("id")
            hr += f"{tableToMarkdown(f'Group id:{group_id}', users, headers, removeNull=True)}\n"
    return CommandResults(
        outputs=createContext(response_to_context(groups), removeNull=True),
        outputs_prefix="TenableSC.Group",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def get_all_scan_results_command(client: Client, args: dict[str, Any]):
    """
    Lists all scan results.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.get_all_scan_results()
    get_manageable_results = args.get("manageable", "false").lower()  # 'true' or 'false'
    page = int(args.get("page", "0"))
    limit = int(args.get("limit", "50"))
    if limit > 200:
        limit = 200

    if not (res and res.get("response")):
        raise DemistoException("Scan results not found")

    elements = get_elements(res["response"], get_manageable_results)

    headers = [
        "ID",
        "Name",
        "Status",
        "Description",
        "Policy",
        "Group",
        "Owner",
        "ScannedIPs",
        "StartTime",
        "EndTime",
        "Duration",
        "Checks",
        "ImportTime",
        "RepositoryName",
    ]

    scan_results = [
        {
            "ID": elem["id"],
            "Name": elem["name"],
            "Status": elem["status"],
            "Description": elem.get("description", None),
            "Policy": elem["details"],
            "Group": elem.get("ownerGroup", {}).get("name"),
            "Checks": elem.get("completedChecks", None),
            "StartTime": timestamp_to_utc(elem["startTime"]),
            "EndTime": timestamp_to_utc(elem["finishTime"]),
            "Duration": scan_duration_to_demisto_format(elem["scanDuration"]),
            "ImportTime": timestamp_to_utc(elem["importStart"]),
            "ScannedIPs": elem["scannedIPs"],
            "Owner": elem["owner"].get("username"),
            "RepositoryName": elem["repository"].get("name"),
            "ImportStatus": elem.get("importStatus", ""),
        }
        for elem in elements[page : page + limit]
    ]

    readable_title = f"Tenable.sc Scan results - {page}-{page + limit - 1}"
    hr = tableToMarkdown(
        readable_title, scan_results, headers, removeNull=True, metadata=f"Total number of elements is {len(elements)}"
    )

    return CommandResults(
        outputs=createContext(scan_results, removeNull=True),
        outputs_prefix="TenableSC.ScanResults",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def create_user_command(client: Client, args: dict[str, Any]):
    """
    Create a user.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    validate_user_body_params(args, "create")
    res = client.create_user(args)
    hr_header = f'User {args.get("user_name")} was created successfully.'
    return process_update_and_create_user_response(res, hr_header)


def update_user_command(client: Client, args: dict[str, Any]):
    """
    Update a user by given user ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    user_id = args.get("user_id")
    validate_user_body_params(args, "update")
    res = client.update_user(args, user_id)
    hr_header = f'user {args.get("user_id")} was updated successfully.'
    return process_update_and_create_user_response(res, hr_header)


def process_update_and_create_user_response(res, hr_header):
    """
    Process the response returned from the update and create user requests
    Args:
        res (Dict): The response returned from the request
        hr_header (Dict): The header to add to the hr section.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    if not res or not res.get("response", {}):
        raise DemistoException("User wasn't created successfully.")
    headers = [
        "User type",
        "User ID",
        "User Status",
        "User Name",
        "First Name",
        "Lat Name ",
        "Email ",
        "User Role Name",
        "User Group Name",
        "User  LDAP  Name",
    ]
    response = res.get("response", {})
    mapped_response = {
        "User type": res.get("type"),
        "User ID": response.get("id"),
        "User Status": response.get("status"),
        "User Name": response.get("username"),
        "First Name": response.get("firstname"),
        "Lat Name ": response.get("lastname"),
        "Email ": response.get("email"),
        "User Role Name": response.get("role", {}).get("name"),
        "User Group Name": response.get("group", {}).get("name"),
        "User  LDAP  Name": response.get("ldap", {}).get("name"),
    }

    return CommandResults(
        outputs=createContext(response_to_context(response), removeNull=True),
        outputs_prefix="TenableSC.User",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown(hr_header, mapped_response, headers, removeNull=True),
    )


def delete_user_command(client: Client, args: dict[str, Any]):
    """
    Delete a user by a given user ID.
     Args:
         client (Client): The tenable.sc client object.
         args (Dict): demisto.args() object.
     Returns:
         CommandResults: command results object with the response, and human readable section.
    """
    user_id = args.get("user_id")
    res = client.delete_user(user_id)

    return CommandResults(raw_response=res, readable_output=f"User {user_id} was deleted successfully.")


def list_plugin_family_command(client: Client, args: dict[str, Any]):
    """
    return info about a query / list of queries.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    is_active = args.get("is_active")
    limit = int(args.get("limit", "50"))
    plugin_id = args.get("plugin_id", "")
    res = client.list_plugin_family(plugin_id, is_active)
    if not res or not res.get("response", []):
        raise DemistoException("No plugins found")
    plugins = res.get("response")
    if isinstance(plugins, dict):
        if plugin_type := plugins.get("type") in ["active", "passive"]:
            is_active = "false" if plugin_type == "passive" else "true"
        plugins = [plugins]
    if len(plugins) > limit:
        plugins = plugins[:limit]
    mapped_plugins = [{"Plugin ID": plugin.get("id"), "Plugin Name": plugin.get("name")} for plugin in plugins]
    if is_active:
        for mapped_plugin in mapped_plugins:
            mapped_plugin["Is Active"] = is_active
    headers = ["Plugin ID", "Plugin Name", "Is Active"]
    return CommandResults(
        outputs=createContext(response_to_context(plugins), removeNull=True),
        outputs_prefix="TenableSC.PluginFamily",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Plugin families:", mapped_plugins, headers, removeNull=True),
    )


def create_policy_command(client: Client, args: dict[str, Any]):
    """
    Creates a policy.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    res = client.create_policy(args)
    created_policy = res.get("response")
    mapped_created_policy = {
        "Policy type": res.get("type"),
        "Policy ID": created_policy.get("id"),
        "name": created_policy.get("name"),
        "Description": created_policy.get("description"),
        "Created Time": created_policy.get("createdTime"),
        "Plugin Families": created_policy.get("families"),
        "Policy  Status": created_policy.get("status"),
        "Policy UUID": created_policy.get("uuid"),
        "Policy can Manage": created_policy.get("canManage"),
        "Creator Username": created_policy.get("creator", {}).get("username"),
        "Owner ID": created_policy.get("ownerID"),
        "policyTemplate ID": created_policy.get("policyTemplate", {}).get("id"),
        "policyTemplate Name": created_policy.get("policyTemplate", {}).get("name"),
    }
    headers = [
        "Policy type",
        "Policy ID",
        "name",
        "Description",
        "Created Time",
        "Plugin Families",
        "Policy  Status",
        "Policy UUID",
        "Policy can Manage",
        "Creator Username",
        "Owner ID",
        "policyTemplate id",
        "policyTemplate Name",
    ]

    return CommandResults(
        outputs=createContext(response_to_context(created_policy), removeNull=True),
        outputs_prefix="TenableSC.ScanPolicy",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Policy was created successfully:", mapped_created_policy, headers, removeNull=True),
    )


def create_remediation_scan_command(client: Client, args: dict[str, Any]):
    """
    Creates remediation scan.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    args["policy_template_id"] = "1"
    args["scan_type"] = "policy"
    args["schedule"] = "now"
    validate_create_scan_inputs(args)
    res = client.create_policy(args)
    created_policy = res.get("response")
    args["policy_id"] = created_policy.get("id")
    res = client.create_scan(args)
    if not res or "response" not in res:
        raise DemistoException("Error: Could not retrieve the scan")

    scan = res.get("response", {})

    headers = [
        "Scan ID",
        "Scan Name",
        "Scan Description",
        "Scan Type",
        "Dhcp Tracking status",
        "Created Time",
        "Modified Time",
        "Max Scan Time",
        "Policy id ",
        "Policy context",
        "Policy description",
        "Schedule type",
        "Start Time",
        "Group",
        "Owner",
    ]

    mapped_scan = {
        "Scan ID": scan["id"],
        "Scan Name": scan["name"],
        "Scan Description": scan["description"],
        "Scan Type": scan["type"],
        "Dhcp Tracking status": scan["dhcpTracking"],
        "Created Time": timestamp_to_utc(scan["createdTime"]),
        "Modified Time": scan["modifiedTime"],
        "Max Scan Time": scan["maxScanTime"],
        "Policy id ": scan["policy"]["id"],
        "Policy context": scan["policy"]["context"],
        "Policy description": scan["policy"]["description"],
        "Schedule type": scan["schedule"]["type"],
        "Start Time": scan["schedule"]["start"],
        "Group": scan["ownerGroup"]["name"],
        "Owner": scan["owner"]["username"],
    }

    return CommandResults(
        outputs=createContext(response_to_context(scan), removeNull=True),
        outputs_prefix="TenableSC.Scan",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Remediation scan created successfully", mapped_scan, headers, removeNull=True),
    )


def list_query_command(client: Client, args: dict[str, Any]):
    """
    return info about a query / list of queries.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response, human readable section, and the context entries to add.
    """
    type = args.get("type")
    query_id = args.get("query_id", "")
    if query_id:
        res, hr, ec = get_query(client, query_id)
    else:
        res, hr, ec = list_queries(client, type)

    return CommandResults(
        outputs=createContext(response_to_context(ec), removeNull=True),
        outputs_prefix="TenableSC.Query",
        raw_response=res,
        outputs_key_field="ID",
        readable_output=hr,
    )


def update_asset_command(client: Client, args: dict[str, Any]):
    """
    Update an asset by a given asset ID.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        CommandResults: command results object with the response and human readable.
    """
    asset_id = args.get("asset_id")
    res = client.update_asset(args, asset_id)
    if not res or not res.get("response", []):
        raise DemistoException("Couldn't update asset.")

    return CommandResults(raw_response=res, readable_output=f"asset {asset_id} was updated successfully.")


def get_query(client: Client, query_id):
    """
    get a query by ID and return the processed results.
    Args:
        client (Client): The tenable.sc client object.
        query_id (str): The query ID to search.
    Returns:
        Dict: The response from the server.
        str: The processed human readable.
        Dict: The relevant section from the response.
    """
    res = client.get_query(query_id)
    if not res or not res.get("response", []):
        raise DemistoException(f"The query {query_id} wasn't found")
    query = res.get("response")
    mapped_query = {
        "Query ID": query_id,
        "Query  Name": query.get("name"),
        "Query Description": query.get("description"),
        "Query Filters": query.get("filters"),
    }
    headers = ["Query ID", "Query  Name", "Query Description", "Query Filters"]
    hr = tableToMarkdown(f"Query {query_id}", mapped_query, headers, removeNull=True)
    return res, hr, query


def list_queries(client: Client, type):
    """
    Lists queries and return the processed results.
    Args:
        client (Client): The tenable.sc client object.
        type (str): query time to filter by.
    Returns:
        Dict: The response from the server.
        str: The processed human readable.
        Dict: The relevant section from the response.
    """
    res = client.list_queries(type)
    if not res or not res.get("response", []):
        raise DemistoException("No queries found.")
    queries = res.get("response")
    manageable_queries = queries.get("manageable", [])
    usable_queries = queries.get("usable", [])
    mapped_queries, mapped_usable_queries = [], []
    found_ids = []

    for manageable_query in manageable_queries:
        query_id = manageable_query.get("id")
        mapped_queries.append(
            {
                "Query ID": query_id,
                "Query  Name": manageable_query.get("name"),
                "Query Description": manageable_query.get("description"),
                "Query Filters": manageable_query.get("filters"),
                "Query Manageable": "True",
            }
        )
        found_ids.append(query_id)
    for usable_query in usable_queries:
        query_id = usable_query.get("id")
        if query_id not in found_ids:
            mapped_usable_queries.append(
                {
                    "Query ID": usable_query.get("id"),
                    "Query  Name": usable_query.get("name"),
                    "Query Description": usable_query.get("description"),
                    "Query Filters": usable_query.get("filters"),
                    "Query Usable": "True",
                    "Query Manageable": "False",
                }
            )
        else:
            for mapped_query in mapped_queries:
                if query_id == mapped_query["Query ID"]:
                    mapped_query["Query Usable"] = "True"

    for mapped_query in mapped_queries:
        if not mapped_query.get("Query Usable"):
            mapped_query["Query Usable"] = "False"

    mapped_queries.extend(mapped_usable_queries)
    headers = ["Query ID", "Query  Name", "Query Description", "Query Filters", "Query Manageable", "Query Usable"]
    hr = tableToMarkdown("Queries:", mapped_queries, headers, removeNull=True)
    return res, hr, queries


""" FETCH ASSETS FUNCTIONS """


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 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 skip_fetch_assets(last_run: dict) -> bool:
    """
    Check if enough time has passed since the last completed fetch cycle.

    Args:
        last_run: The last run object containing fetch state.

    Returns:
        bool: True if the fetch should be skipped (not enough time has passed).
    """
    time_to_check = last_run.get("assets_last_fetch")
    if not time_to_check:
        return False
    # If there's an ongoing fetch (assets or vulns), don't skip
    if is_assets_fetch_in_progress(last_run) or is_vulns_fetch_in_progress(last_run):
        return False
    passed_minutes = (time.time() - time_to_check) / 60
    to_skip = passed_minutes < MIN_ASSETS_INTERVAL
    if to_skip:
        demisto.info(
            f"Skipping fetch-assets command. Only {passed_minutes:.1f} minutes have passed since the last fetch. "
            f"Minimum interval is {MIN_ASSETS_INTERVAL} minutes."
        )
    return to_skip


def is_assets_fetch_in_progress(last_run: dict) -> bool:
    """
    Check if an assets fetch is still in progress (has more pages to fetch).

    Args:
        last_run: The last run object containing fetch state.

    Returns:
        bool: True if there is an ongoing paginated fetch.
    """
    return "current_offset" in last_run


def fetch_assets_page(client: Client, last_run: dict) -> list:
    """
    Fetch a single page of host assets from the Tenable.sc API.

    Args:
        client: Client class object.
        last_run: The last run object containing fetch state.

    Returns:
        list: List of host asset records from this page.
    """
    current_offset = last_run.get("current_offset", 0)
    end_offset = current_offset + ASSETS_PAGE_SIZE

    demisto.debug(f"Fetching assets page: offset {current_offset} to {end_offset}")

    response = client.search_hosts(
        fields=HOST_FIELDS,
        start_offset=current_offset,
        end_offset=end_offset,
    )

    if not response or "response" not in response:
        demisto.debug("No response received from hosts/search endpoint.")
        # Clear fetch state on error
        last_run.pop("current_offset", None)
        last_run.pop("total_records", None)
        return []

    response_data = response["response"]

    # Handle both list response (simple) and dict response (paginated)
    if isinstance(response_data, list):
        results = response_data
        total_records = len(results)
    else:
        results = response_data.get("results", [])
        total_records = int(response_data.get("totalRecords", 0))

    demisto.info(f"Received {len(results)} assets (offset {current_offset}, total {total_records})")

    # Update cumulative count
    cumulative = last_run.get("total_assets_fetched", 0) + len(results)
    last_run["total_assets_fetched"] = cumulative

    # Determine if there are more pages
    next_offset = current_offset + len(results)
    if next_offset < total_records and len(results) > 0:
        # More pages to fetch
        last_run["current_offset"] = next_offset
        last_run["total_records"] = total_records
        last_run["nextTrigger"] = "30"
        demisto.debug(f"More assets to fetch. Next offset: {next_offset}, total: {total_records}")
    else:
        # All pages fetched - clear pagination state
        last_run.pop("current_offset", None)
        last_run.pop("total_records", None)
        demisto.info(f"Asset fetch complete. Total assets fetched: {cumulative}")

    return results


def fetch_assets_command(client: Client, last_run: dict) -> list:
    """
    Fetch assets from the Tenable.sc API, handling pagination state.

    Args:
        client: Client class object.
        last_run: The last run object containing fetch state.

    Returns:
        list: Assets fetched from the API for this page.
    """
    return fetch_assets_page(client, last_run)


def run_assets_fetch(client: Client, last_run: dict) -> list:  # pragma: no cover
    """
    Entry point for running the assets fetch.

    Initializes fetch state if starting a new cycle, then fetches one page.

    Args:
        client: Client class object.
        last_run: The last run object containing fetch state.

    Returns:
        list: Assets fetched from the API.
    """
    demisto.info("fetch assets from the API")

    # Starting new fetch cycle - initialize state
    if not is_assets_fetch_in_progress(last_run):
        snapshot_id = generate_snapshot_id()
        demisto.debug(f"Starting new asset fetch cycle with snapshot_id: {snapshot_id}")
        last_run.update(
            {
                "current_offset": 0,
                "total_assets_fetched": 0,
                "snapshot_id": snapshot_id,
            }
        )

    return fetch_assets_command(client, last_run)


""" FETCH VULNERABILITIES FUNCTIONS """


def is_vulns_fetch_in_progress(last_run: dict) -> bool:
    """
    Check if a vulnerabilities fetch is still in progress (has more pages to fetch).

    Args:
        last_run: The last run object containing fetch state.

    Returns:
        bool: True if there is an ongoing paginated vulnerability fetch.
    """
    return "vuln_current_offset" in last_run


def fetch_vulnerabilities_page(client: Client, last_run: dict) -> list:
    """
    Fetch a single page of cumulative vulnerabilities from the Tenable.sc Analysis API.

    Args:
        client: Client class object.
        last_run: The last run object containing fetch state.

    Returns:
        list: List of vulnerability records from this page.
    """
    current_offset = last_run.get("vuln_current_offset", 0)
    end_offset = current_offset + VULNS_PAGE_SIZE

    demisto.debug(f"Fetching vulnerabilities page: offset {current_offset} to {end_offset}")

    response = client.fetch_vulnerabilities_analysis(
        start_offset=current_offset,
        end_offset=end_offset,
    )

    if not response or "response" not in response:
        demisto.debug("No response received from analysis endpoint for vulnerabilities.")
        # Clear vuln fetch state on error
        last_run.pop("vuln_current_offset", None)
        last_run.pop("total_vulns_fetched", None)
        return []

    response_data = response["response"]

    results = response_data.get("results", [])
    total_records = int(response_data.get("totalRecords", 0))

    demisto.info(f"Received {len(results)} vulnerabilities (offset {current_offset}, total {total_records})")

    # Update cumulative count
    cumulative = last_run.get("total_vulns_fetched", 0) + len(results)
    last_run["total_vulns_fetched"] = cumulative

    # Determine if there are more pages
    next_offset = current_offset + len(results)
    if next_offset < total_records and len(results) > 0:
        # More pages to fetch
        last_run["vuln_current_offset"] = next_offset
        last_run["nextTrigger"] = "30"
        demisto.debug(f"More vulnerabilities to fetch. Next offset: {next_offset}, total: {total_records}")
    else:
        # All pages fetched - clear vuln pagination state
        last_run.pop("vuln_current_offset", None)
        demisto.info(f"Vulnerability fetch complete. Total vulnerabilities fetched: {cumulative}")

    return results


def parse_vulnerabilities(vulns: list) -> list:
    """
    Parse and prepare vulnerabilities for XSIAM ingestion.

    Adds _time field, truncates oversized entries, and marks truncation status.
    Mirrors the Tenable_io parse_vulnerabilities pattern.

    Args:
        vulns: List of raw vulnerability records from the analysis API.

    Returns:
        list: Parsed vulnerability records ready for XSIAM.
    """
    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:
        # Set _time from lastSeen or firstSeen (Tenable.sc uses these fields)
        vuln["_time"] = vuln.get("firstSeen") or vuln.get("lastSeen")
        vuln_str = json.dumps(vuln)
        if sys.getsizeof(vuln_str) > XSIAM_EVENT_CHUNK_SIZE_LIMIT:
            demisto.debug(f"found oversized vulnerability object: {sys.getsizeof(vuln_str)} bytes")
            if vuln.get("pluginText"):
                demisto.debug("truncating pluginText field")
                vuln["pluginText"] = ""
                vuln["isTruncated"] = True
            elif vuln.get("pluginDescription"):
                demisto.debug("truncating pluginDescription field")
                vuln["pluginDescription"] = ""
                vuln["isTruncated"] = True
            else:
                demisto.debug("skipping oversized object...")
                continue
        else:
            vuln["isTruncated"] = False
    return vulns


def run_vulns_fetch(client: Client, last_run: dict) -> list:  # pragma: no cover
    """
    Entry point for running the vulnerabilities fetch.

    Initializes vuln fetch state if starting a new cycle, then fetches one page.

    Args:
        client: Client class object.
        last_run: The last run object containing fetch state.

    Returns:
        list: Vulnerabilities fetched from the API.
    """
    demisto.info("fetch vulnerabilities from the API")

    # Starting new vuln fetch cycle - initialize state
    if not is_vulns_fetch_in_progress(last_run):
        demisto.debug("Starting new vulnerability fetch cycle")
        last_run.update(
            {
                "vuln_current_offset": 0,
                "total_vulns_fetched": 0,
            }
        )

    return fetch_vulnerabilities_page(client, last_run)


def test_module(client: Client, args: dict[str, Any]):
    """
    Test the connection to the Tenable.sc server.
    Args:
        client (Client): The tenable.sc client object.
        args (Dict): demisto.args() object.
    Returns:
        str: 'ok' if the connection is successful.
    """
    try:
        # Validate assets fetch interval if configured
        params = demisto.params()
        assets_fetch_interval = params.get("assetsFetchInterval") or MIN_ASSETS_INTERVAL
        if int(assets_fetch_interval) < MIN_ASSETS_INTERVAL:
            raise DemistoException(f"Assets fetch interval must be at least {MIN_ASSETS_INTERVAL} minutes (1 hour).")
        client.get_users()
        return "ok"
    except DemistoException:
        raise
    except Exception:
        raise Exception("Authorization Error: make sure your API Key and Secret Key are correctly set")


def main():  # pragma: no cover
    params = demisto.params()
    command = demisto.command()
    args = demisto.args()
    verify_ssl = not params.get("unsecure", False)
    proxy = params.get("proxy", False)
    user_name = params.get("credentials", {}).get("identifier")
    password = params.get("credentials", {}).get("password")
    access_key = params.get("creds_keys", {}).get("identifier")
    secret_key = params.get("creds_keys", {}).get("password")
    url = params.get("server")

    demisto.info(f"Executing command {command}")

    command_dict = {
        "test-module": test_module,
        "tenable-sc-list-scans": list_scans_command,
        "tenable-sc-list-policies": list_policies_command,
        "tenable-sc-list-repositories": list_repositories_command,
        "tenable-sc-list-credentials": list_credentials_command,
        "tenable-sc-list-zones": list_zones_command,
        "tenable-sc-list-report-definitions": list_report_definitions_command,
        "tenable-sc-list-assets": list_assets_command,
        "tenable-sc-get-asset": get_asset_command,
        "tenable-sc-create-asset": create_asset_command,
        "tenable-sc-delete-asset": delete_asset_command,
        "tenable-sc-create-scan": create_scan_command,
        "tenable-sc-get-scan-status": get_scan_status_command,
        "tenable-sc-get-scan-report": get_scan_report_command,
        "tenable-sc-delete-scan": delete_scan_command,
        "tenable-sc-list-users": list_users_command,
        "tenable-sc-list-alerts": list_alerts_command,
        "tenable-sc-get-alert": get_alert_command,
        "tenable-sc-get-system-information": get_system_information_command,
        "tenable-sc-get-system-licensing": get_system_licensing_command,
        "tenable-sc-get-all-scan-results": get_all_scan_results_command,
        "tenable-sc-list-groups": list_groups_command,
        "tenable-sc-create-user": create_user_command,
        "tenable-sc-update-user": update_user_command,
        "tenable-sc-delete-user": delete_user_command,
        "tenable-sc-list-plugin-family": list_plugin_family_command,
        "tenable-sc-create-policy": create_policy_command,
        "tenable-sc-list-query": list_query_command,
        "tenable-sc-update-asset": update_asset_command,
        "tenable-sc-get-vulnerability": get_vulnerability_command,
        "tenable-sc-get-device": get_device_command,
        "tenable-sc-create-remediation-scan": create_remediation_scan_command,
        "tenable-sc-get-organization": get_organization_command,
    }

    try:
        with Client(
            verify_ssl=verify_ssl,
            proxy=proxy,
            user_name=user_name,
            password=password,
            access_key=access_key,
            secret_key=secret_key,
            url=url,
        ) as client:
            if command == "fetch-incidents":
                first_fetch = params.get("fetch_time").strip()
                fetch_incidents(client, first_fetch)
            elif command == "fetch-assets":
                assets_last_run = demisto.getAssetsLastRun()
                demisto.debug(f"saved lastrun assets: {assets_last_run}")

                if skip_fetch_assets(assets_last_run):
                    return

                # Mark the start of a new fetch cycle if not resuming
                if not is_assets_fetch_in_progress(assets_last_run) and not is_vulns_fetch_in_progress(assets_last_run):
                    assets_last_run["assets_last_fetch"] = time.time()

                assets_last_run_copy = assets_last_run.copy()
                assets: list = []
                vulnerabilities: list = []

                # Fetch Assets: Run if assets fetch is in progress or starting new cycle (vulns not yet started)
                if is_assets_fetch_in_progress(assets_last_run_copy) or not is_vulns_fetch_in_progress(assets_last_run_copy):
                    assets = run_assets_fetch(client, assets_last_run)

                # Fetch Vulnerabilities: Run if vulns fetch is in progress or assets fetch is complete
                if is_vulns_fetch_in_progress(assets_last_run_copy) or not is_assets_fetch_in_progress(assets_last_run):
                    vulnerabilities = run_vulns_fetch(client, assets_last_run)

                demisto.info(f"Received {len(assets)} assets and {len(vulnerabilities)} vulnerabilities.")
                demisto.setAssetsLastRun(assets_last_run)

                # Get snapshot_id for this fetch cycle
                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)

                assets_fetch_in_progress = is_assets_fetch_in_progress(assets_last_run)

                if assets:
                    cumulative_total = assets_last_run.get("total_assets_fetched", 0)
                    # Per XSIAM spec: items_count=1 if not finished, else total count
                    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),
                        should_update_health_module=False,
                        add_proxy_to_request=proxy,
                    )

                elif not assets_fetch_in_progress:
                    # Seal empty snapshot if we had previously sent data
                    cumulative_total = assets_last_run.get("total_assets_fetched", 0)
                    if cumulative_total > 0:
                        demisto.debug(
                            f"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,
                            add_proxy_to_request=proxy,
                        )

                # Send vulnerabilities to XSIAM
                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",
                        add_proxy_to_request=proxy,
                    )

                # Update module health
                if (
                    assets
                    or vulnerabilities
                    or (not assets_fetch_in_progress and not is_vulns_fetch_in_progress(assets_last_run))
                ):
                    cumulative_total = assets_last_run.get("total_assets_fetched", 0)
                    demisto.updateModuleHealth({"assetsPulled": cumulative_total})

                # Clean up snapshot state when BOTH assets and vulns fetch are complete
                if not assets_fetch_in_progress and not is_vulns_fetch_in_progress(assets_last_run):
                    assets_last_run.pop("snapshot_id", None)
                    assets_last_run.pop("total_assets_fetched", None)
                    assets_last_run.pop("total_vulns_fetched", None)
                    demisto.setAssetsLastRun(assets_last_run)

                demisto.info("Done sending asset and vulnerability data to XSIAM.")

            elif command == "tenable-sc-launch-scan":
                return_results(launch_scan_command(args, client))
            else:
                return_results(command_dict[command](client, args))
    except Exception as e:
        return_error(f"Failed to execute {command} command. Error: {e!s}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()