diff --git a/Packs/Tenable_io/Integrations/Tenable_io/README.md b/Packs/Tenable_io/Integrations/Tenable_io/README.md index 578d2b32bcb7..aa7cc98e3728 100644 --- a/Packs/Tenable_io/Integrations/Tenable_io/README.md +++ b/Packs/Tenable_io/Integrations/Tenable_io/README.md @@ -32,6 +32,11 @@ This integration was integrated and tested with January 2023 release of Tenable. | tenable-io-get-asset-details | BASIC [16] user permissions. | | tenable-io-export-assets | ADMINISTRATOR [64] user permissions. | | tenable-io-export-vulnerabilities | ADMINISTRATOR [64] user permissions. | +| tenable-io-list-scan-filters | BASIC [16] user permissions | +| tenable-io-get-scan-history | SCAN OPERATOR [24] user permissions and CAN VIEW [16] scan permissions | +| tenable-io-export-scan | SCAN OPERATOR [24] user permissions and CAN VIEW [16] scan permissions | + + ## Concurrency Limits @@ -1355,3 +1360,238 @@ When inserting invalid arguments, an error message could be returned. >|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| >| fake_uuid | 1.1.1.1 | 1.1.1.1 | Linux Kernel 3.13 on Ubuntu 14.04 (trusty) | general-purpose | fqdn | info | 00000 | Name | | | TCP | 22 | 2024-11-07T11:11:05.906Z | 2024-11-07T11:11:05.906Z | Description | N/A | >| fake_uuid | 1.3.2.1 | 1.3.2.1 | Nutanix | general-purpose | fqdn | info | 00000 | Name | | | TCP | 0 | 2024-11-07T11:11:05.906Z | 2024-11-07T11:11:05.906Z | Description | N/A | +### tenable-io-list-scan-filters + +*** +Lists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them. + +#### Base Command + +`tenable-io-list-scan-filters` + +#### Input + +--- +There are no inputs for this command. + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| TenableIO.ScanFilter.name | String | The name of the scan filter. | +| TenableIO.ScanFilter.readable_name | String | The readable name of the scan filter. | +| TenableIO.ScanFilter.control.type | String | The type of control associated with the scan filter. | +| TenableIO.ScanFilter.control.regex | String | The regular expression used by the scan filter. | +| TenableIO.ScanFilter.control.readable_regex | String | An example expression that the filter's regular expression would match. | +| TenableIO.ScanFilter.operators | String | The operators available for the scan filter. | +| TenableIO.ScanFilter.group_name | String | The group name associated with the scan filter. | + +#### Command example +```!tenable-io-list-scan-filters``` +#### Context Example +```json +{ + "TenableIO": { + "ScanFilter": [ + { + "control": { + "readable_regex": "01234567-abcd-ef01-2345-6789abcdef01", + "regex": "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})*", + "type": "entry" + }, + "group_name": null, + "name": "host.id", + "operators": [ + "eq", + "neq", + "match", + "nmatch" + ], + "readable_name": "Asset ID" + }, + { + "control": { + "maxlength": 18, + "readable_regex": "NUMBER", + "regex": "^[0-9]+(,[0-9]+)*", + "type": "entry" + }, + "group_name": null, + "name": "plugin.attributes.bid", + "operators": [ + "eq", + "neq", + "match", + "nmatch" + ], + "readable_name": "Bugtraq ID" + } + ] + } +} +``` + +#### Human Readable Output + +>### Tenable IO Scan Filters +>|Filter name|Filter Readable name|Filter Control type|Filter regex|Readable regex|Filter operators| +>|---|---|---|---|---|---| +>| host.id | Asset ID | entry | [0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})* | 01234567-abcd-ef01-2345-6789abcdef01 | eq,
neq,
match,
nmatch | +>| plugin.attributes.bid | Bugtraq ID | entry | ^[0-9]+(,[0-9]+)* | NUMBER | eq,
neq,
match,
nmatch | + +### tenable-io-get-scan-history + +*** +Lists the individual runs of the specified scan. + +#### Base Command + +`tenable-io-get-scan-history` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| scanId | The ID of the scan of which to get the runs. | Required | +| sortFields | A comma-separated list of fields by which to sort, in the order defined by "sortOrder". Possible values are: start_date, end_date, status. | Optional | +| sortOrder | A comma-separated list of directions in which to sort the fields defined by "sortFields".
If multiple directions are chosen, they will be sequentially matched with "sortFields".
If only one direction is chosen it will be used to sort all values in "sortFields".
For example:
If sortFields is "start_date,status" and sortOrder is "asc,desc",
then start_date is sorted in ascending order and status in descending order.
If sortFields is "start_date,status" and sortOrder is simply "asc",
then both start_date and status are sorted in ascending order.
. Possible values are: asc, desc. Default is asc. | Optional | +| excludeRollover | Whether to exclude rollover scans from the scan history. Possible values are: true, false. Default is false. | Optional | +| page | The page number of scan records to retrieve (used for pagination) starting from 1. The page size is defined by the "pageSize" argument. | Optional | +| pageSize | The number of scan records per page to retrieve (used for pagination). The page number is defined by the "page" argument. | Optional | +| limit | The maximum number of records to retrieve. If "pageSize" is defined, this argument is ignored. Default is 50. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| TenableIO.ScanHistory.time_end | Number | The end time of the scan. | +| TenableIO.ScanHistory.scan_uuid | String | The UUID (Universally Unique Identifier) of the scan. | +| TenableIO.ScanHistory.id | Number | The ID of the scan history. | +| TenableIO.ScanHistory.is_archived | Boolean | Indicates whether the scan is archived or not. | +| TenableIO.ScanHistory.time_start | Number | The start time of the scan. | +| TenableIO.ScanHistory.visibility | String | The visibility of the scan. | +| TenableIO.ScanHistory.targets.custom | Boolean | Indicates whether custom targets were used in the scan. | +| TenableIO.ScanHistory.targets.default | Boolean | Indicates whether the default targets were used in the scan. | +| TenableIO.ScanHistory.status | String | The status of the scan. | + +#### Command example +```!tenable-io-get-scan-history scanId=16 excludeRollover=true sortFields=end_date,status sortOrder=desc page=2 pageSize=4``` +#### Context Example +```json +{ + "TenableIO": { + "ScanHistory": [ + { + "id": 17235445, + "is_archived": true, + "reindexing": null, + "scan_uuid": "69a55b8e-0d52-427a-81e0-7dfe4dc6eda6", + "status": "completed", + "targets": { + "custom": null, + "default": false + }, + "time_end": 1677425182, + "time_start": 1677424566, + "visibility": "public" + }, + { + "id": 17235342, + "is_archived": true, + "reindexing": null, + "scan_uuid": "2c592d52-df56-42e0-9f18-d892bdeb1e18", + "status": "completed", + "targets": { + "custom": null, + "default": false + }, + "time_end": 1677424556, + "time_start": 1677423906, + "visibility": "public" + }, + { + "id": 17235033, + "is_archived": true, + "reindexing": null, + "scan_uuid": "44586b4f-1051-415c-b375-db86f6bd8c13", + "status": "completed", + "targets": { + "custom": null, + "default": false + }, + "time_end": 1677423865, + "time_start": 1677423247, + "visibility": "public" + }, + { + "id": 17234969, + "is_archived": true, + "reindexing": null, + "scan_uuid": "06c12bf7-436f-489d-bb04-aae511ea9f5c", + "status": "completed", + "targets": { + "custom": null, + "default": false + }, + "time_end": 1677423205, + "time_start": 1677422585, + "visibility": "public" + } + ] + } +} +``` + +#### Human Readable Output + +>### Tenable IO Scan History +>|History id|History uuid|Status|Is archived|Targets default|Visibility|Time start|Time end| +>|---|---|---|---|---|---|---|---| +>| 17235445 | 69a55b8e-0d52-427a-81e0-7dfe4dc6eda6 | completed | true | false | public | 1677424566 | 1677425182 | +>| 17235342 | 2c592d52-df56-42e0-9f18-d892bdeb1e18 | completed | true | false | public | 1677423906 | 1677424556 | +>| 17235033 | 44586b4f-1051-415c-b375-db86f6bd8c13 | completed | true | false | public | 1677423247 | 1677423865 | +>| 17234969 | 06c12bf7-436f-489d-bb04-aae511ea9f5c | completed | true | false | public | 1677422585 | 1677423205 | + +### tenable-io-export-scan + +*** +Export and download a scan report. +Scan results older than 35 days are supported in Nessus and CSV formats only, and filters cannot be applied. +Scans that are actively running cannot be exported (run "tenable-io-list-scans" to view scan statuses) + + +#### Base Command + +`tenable-io-export-scan` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| scanId | The identifier for the scan to export. Run the "tenable-io-list-scans" command to get all available scans. | Required | +| historyId | The unique identifier of the historical data to export. Run the "tenable-io-get-scan-history" command to get history IDs. | Optional | +| historyUuid | The UUID of the historical data to export. Run the "tenable-io-get-scan-history" command to get history UUIDs. | Optional | +| format | The file format to export the scan in. Scans can be export in the HTML and PDF formats for up to 35 days.
For scans that are older than 35 days, only the Nessus and CSV formats are supported.
The "chapters" argument must be defined if the chosen format is HTML or PDF.
. Possible values are: Nessus, HTML, PDF, CSV. Default is CSV. | Required | +| chapters | A comma-separated list of chapters to include in the export. This argument is required if the file format is PDF or HTML. Possible values are: vuln_hosts_summary, vuln_by_host, compliance_exec, remediations, vuln_by_plugin, compliance. | Optional | +| filter | A comma-separated list of filters, in the format of "name quality value" to apply to the exported scan report.
Example: "port.protocol eq tcp, plugin_id eq 1234567"
Note: when used literally, commas and spaces should be escaped. (i.e. "\\\\," for comma and "\\\\s" for space)
Filters cannot be applied to scans older than 35 days.
Run "tenable-io-list-scan-filters" to get all available filters, ("Filter name" (name), "Filter operators" (quality) and "Readable regex" (value) in response).
For more information: https://developer.tenable.com/docs/scan-export-filters-tio
. | Optional | +| filterSearchType | For multiple filters, specifies whether to use the AND or the OR logical operator. Possible values are: AND, OR. Default is AND. | Optional | +| assetId | The ID of the asset scanned. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| InfoFile.Size | number | The size of the file in bytes. | +| InfoFile.Name | string | The name of the file. | +| InfoFile.EntryID | string | The War Room entry ID of the file. | +| InfoFile.Info | string | The format and encoding of the file. | +| InfoFile.Type | string | The type of the file. | +| InfoFile.Extension | unknown | The file extension of the file. | + +#### Command example +```!tenable-io-export-scan scanId=16 format=HTML chapters="compliance_exec,remediations,vuln_by_plugin" historyId=19540157 historyUuid=f7eaad37-23bd-4aac-a979-baab0e9a465b filterSearchType=OR filter="port.protocol eq tcp, plugin_id eq 1234567" assetId=10``` +#### Human Readable Output + +>Preparing scan report: + +>Returned file: scan_16_SSE-144f3dc6-cb2d-42fc-b6cc-dd20b807735f-html.html [Download](https://www.paloaltonetworks.com/cortex) \ No newline at end of file diff --git a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.py b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.py index 59921418cbad..5fafb9abad37 100644 --- a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.py +++ b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.py @@ -6,6 +6,7 @@ import traceback from datetime import datetime import urllib3 +import re import requests @@ -136,30 +137,65 @@ 'Low', 'Medium', 'High', - 'Critical'] - -BASE_URL = demisto.params()['url'] -ACCESS_KEY = demisto.params().get('credentials_access_key', {}).get('password') or demisto.params()['access-key'] -SECRET_KEY = demisto.params().get('credentials_secret_key', {}).get('password') or demisto.params()['secret-key'] + 'Critical' +] +PARAMS = demisto.params() +BASE_URL = PARAMS['url'] +ACCESS_KEY = PARAMS.get('credentials_access_key', {}).get('password') or PARAMS.get('access-key') +SECRET_KEY = PARAMS.get('credentials_secret_key', {}).get('password') or PARAMS.get('secret-key') USER_AGENT_HEADERS_VALUE = 'Integration/1.0 (PAN; Cortex-XSOAR; Build/2.0)' AUTH_HEADERS = {'X-ApiKeys': f"accessKey={ACCESS_KEY}; secretKey={SECRET_KEY}"} -NEW_HEADERS = { - 'X-ApiKeys': f'accessKey={ACCESS_KEY}; secretKey={SECRET_KEY}', +HEADERS = AUTH_HEADERS | { 'accept': "application/json", - 'content-type': "application/json" + 'content-type': "application/json", + 'User-Agent': USER_AGENT_HEADERS_VALUE } -USE_SSL = not demisto.params()['unsecure'] +USE_SSL = not PARAMS['unsecure'] +USE_PROXY = PARAMS['proxy'] -if not demisto.params()['proxy']: +if not USE_PROXY: del os.environ['HTTP_PROXY'] del os.environ['HTTPS_PROXY'] del os.environ['http_proxy'] del os.environ['https_proxy'] +class Client(BaseClient): + + def list_scan_filters(self): + return self._http_request( + 'GET', 'filters/scans/reports') + + def get_scan_history(self, scan_id, params) -> dict: + remove_nulls_from_dictionary(params) + return self._http_request( + 'GET', f'scans/{scan_id}/history', + params=params) + + def initiate_export_scan(self, scan_id: str, params: dict, body: dict) -> dict: + remove_nulls_from_dictionary(params) + remove_nulls_from_dictionary(body) + return self._http_request( + 'POST', + f'scans/{scan_id}/export', + params=params, json_data=body) + + def check_export_scan_status(self, scan_id: str, file_id: str) -> dict: + return self._http_request( + 'GET', f'scans/{scan_id}/export/{file_id}/status') + + def download_export_scan(self, scan_id: str, file_id: str, file_format: str) -> dict: + return fileResult( + f'scan_{scan_id}_{file_id}.{file_format.lower()}', + self._http_request( + 'GET', f'scans/{scan_id}/export/{file_id}/download', + resp_type='content'), + EntryType.ENTRY_INFO_FILE) + + def flatten(d): r = {} # type: ignore - for k, v in d.items(): + for v in d.values(): if isinstance(v, dict): r.update(flatten(v)) d.update(r) @@ -294,14 +330,15 @@ def get_scan_error_message(response, scan_id): def send_scan_request(scan_id="", endpoint="", method='GET', ignore_license_error=False, body=None, **kwargs): if endpoint: endpoint = '/' + endpoint - full_url = "{0}scans/{1!s}{2}".format(BASE_URL, scan_id, endpoint) + full_url = f"{BASE_URL}scans/{scan_id!s}{endpoint}" try: res = requests.request(method, full_url, headers=AUTH_HEADERS, verify=USE_SSL, json=body, params=kwargs) res.raise_for_status() return res.json() - except HTTPError: + except HTTPError as e: + demisto.debug(str(e)) if ignore_license_error and res.status_code in (403, 500): - return + return None err_msg = get_scan_error_message(res, scan_id) if demisto.command() != 'test-module': return_error(err_msg) @@ -318,6 +355,7 @@ def get_scan_info(scans_result_elem): if response: response['info'].update(scans_result_elem) return response['info'] + return None def send_vuln_details_request(plugin_id, date_range=None): @@ -400,8 +438,8 @@ def send_asset_attributes_request(asset_id: str) -> Dict[str, Any]: return res.json() -def test_module(): - send_scan_request() +def test_module(client: Client): + client.list_scan_filters() return 'ok' @@ -555,10 +593,10 @@ def get_asset_details_command() -> CommandResults: info = send_asset_details_request(asset_id) attrs = send_asset_attributes_request(asset_id) if attrs: - attributes = [] - for attr in attrs.get("attributes", []): - attributes.append({attr.get('name', ''): attr.get('value', '')}) - info["info"]["attributes"] = attributes + info["info"]["attributes"] = [ + {attr.get('name', ''): attr.get('value', '')} + for attr in attrs.get("attributes", []) + ] except DemistoException as e: return_error(f'Failed to include custom attributes. {e}') @@ -595,6 +633,7 @@ def get_vulnerabilities_by_asset_command(): 'Hostname': indicator } return entry + return None def get_scan_status_command(): @@ -682,7 +721,7 @@ def export_request(request_params: dict, assets_or_vulns: str) -> dict: dict: The UUID of the assets export job or raise DemistoException. """ full_url = f'{BASE_URL}{assets_or_vulns}/export' - res = requests.post(full_url, headers=NEW_HEADERS, verify=USE_SSL, json=request_params) + res = requests.post(full_url, headers=HEADERS, verify=USE_SSL, json=request_params) if res.status_code != 200: raise DemistoException(res.text) return res.json() @@ -699,7 +738,7 @@ def export_request_with_export_uuid(export_uuid: str, assets_or_vulns: str) -> d dict: Status of the export job or raise DemistoException. """ full_url = f'{BASE_URL}{assets_or_vulns}/export/{export_uuid}/status' - res = requests.get(full_url, headers=NEW_HEADERS, verify=USE_SSL) + res = requests.get(full_url, headers=HEADERS, verify=USE_SSL) if res.status_code != 200: raise DemistoException(res.text) return res.json() @@ -717,7 +756,7 @@ def get_chunks_request(export_uuid: str, chunk_id: str, assets_or_vulns: str) -> dict: Status of the export job or raise DemistoException. """ full_url = f'{BASE_URL}{assets_or_vulns}/export/{export_uuid}/chunks/{chunk_id}' - res = requests.get(full_url, headers=NEW_HEADERS, verify=USE_SSL) + res = requests.get(full_url, headers=HEADERS, verify=USE_SSL) if res.status_code != 200: raise DemistoException(res.text) return res.json() @@ -757,16 +796,16 @@ def export_assets_build_command_result(chunks_details_list: list[dict]) -> Comma for chunk_details in chunks_details_list: human_readable_to_append = {} if fqdns := chunk_details.get('fqdns'): - human_readable_to_append.update({'DNS NAME (FQDN)': fqdns[0]}) - if tag := chunk_details.get("tags"): - if first_tag := tag[0]: - human_readable_to_append.update({'TAGS': f'{first_tag.get("key")}:{first_tag.get("value")}'}) - if sources := chunk_details.get("sources"): - if first_source := sources[0]: - human_readable_to_append.update({'SOURCE': first_source.get('name')}) - if network_interfaces := chunk_details.get('network_interfaces'): - if first_network_interfaces := network_interfaces[0]: - human_readable_to_append.update({'IPV4 ADDRESS': first_network_interfaces.get('ipv4s')}) + human_readable_to_append['DNS NAME (FQDN)'] = fqdns[0] + if (tag := chunk_details.get("tags")) and (first_tag := tag[0]): + human_readable_to_append['TAGS'] = f'{first_tag.get("key")}:{first_tag.get("value")}' + if (sources := chunk_details.get("sources")) and (first_source := sources[0]): + human_readable_to_append['SOURCE'] = first_source.get('name') + if ( + (network_interfaces := chunk_details.get('network_interfaces')) + and (first_network_interfaces := network_interfaces[0]) + ): + human_readable_to_append['IPV4 ADDRESS'] = first_network_interfaces.get('ipv4s') human_readable_to_append.update( {'ASSET ID': chunk_details.get('id'), 'SYSTEM TYPE': chunk_details.get('system_types'), @@ -964,7 +1003,7 @@ def export_assets_command(args: Dict[str, Any]) -> PollResult: chunks_details_list = get_export_chunks_details(export_uuid_status_response, export_uuid, 'assets') command_results = export_assets_build_command_result(chunks_details_list) return PollResult(command_results) - elif status == 'PROCESSING' or status == 'QUEUED': + elif status in ('PROCESSING', 'QUEUED'): return PollResult( response=None, partial_result=CommandResults( @@ -1054,16 +1093,10 @@ def validate_range(range: Optional[str]) -> tuple[Optional[float], Optional[floa Range if valid else raise DemistoException. """ if range: - range_without_spaces = range.strip() - nums_list = range_without_spaces.split("-") - if len(nums_list) < 2: - raise DemistoException('Please specify valid vprScoreRange. VPR values range are 0.1-10.0.') - elif float(nums_list[0]) > float(nums_list[1]): - raise DemistoException('Please specify valid vprScoreRange. VPR values range are 0.1-10.0.') - elif float(nums_list[0]) < 0.1 or float(nums_list[1]) > 10.0: - raise DemistoException('Please specify valid vprScoreRange. VPR values range are 0.1-10.0.') - else: - return float(nums_list[0]), float(nums_list[1]) + nums = tuple(map(float, range.split("-"))) + if len(nums) != 2 or not 0.1 <= nums[0] <= nums[1] <= 10.0: + raise DemistoException('Please specify a valid vprScoreRange. The VPR values range is 0.1-10.0.') + return nums # type: ignore return None, None @@ -1092,7 +1125,7 @@ def export_vulnerabilities_command(args: Dict[str, Any]) -> PollResult: chunks_details_list = get_export_chunks_details(export_uuid_status_response, export_uuid, 'vulns') command_results = export_vulnerabilities_build_command_result(chunks_details_list) return PollResult(command_results) - elif status == 'PROCESSING' or status == 'QUEUED': + elif status in ('PROCESSING', 'QUEUED'): return PollResult( response=None, partial_result=CommandResults( @@ -1114,33 +1147,284 @@ def export_vulnerabilities_command(args: Dict[str, Any]) -> PollResult: return request_uuid_export_vulnerabilities(args) +def scan_filters_human_readable(filters: list) -> str: + context_to_hr = { + 'name': 'Filter name', + 'readable_name': 'Filter Readable name', + 'type': 'Filter Control type', + 'regex': 'Filter regex', + 'readable_regex': 'Readable regex', + 'operators': 'Filter operators', + 'group_name': 'Filter group name', + } + return tableToMarkdown( + 'Tenable IO Scan Filters', + [d | d.get('control', {}) for d in filters], + headers=list(context_to_hr), + headerTransform=context_to_hr.get, + removeNull=True) + + +def list_scan_filters_command(client: Client) -> CommandResults: + + response_dict = client.list_scan_filters() + filters = response_dict.get('filters', []) + + return CommandResults( + outputs_prefix='TenableIO.ScanFilter', + outputs_key_field='name', + outputs=filters, + readable_output=scan_filters_human_readable(filters), + raw_response=response_dict) + + +def scan_history_readable(history: list) -> str: + context_to_hr = { + 'id': 'History id', + 'scan_uuid': 'History uuid', + 'status': 'Status', + 'is_archived': 'Is archived', + 'custom': 'Targets custom', + 'default': 'Targets default', + 'visibility': 'Visibility', + 'time_start': 'Time start', + 'time_end': 'Time end', + } + return tableToMarkdown( + 'Tenable IO Scan History', + [d | d.get('targets', {}) for d in history], + headers=list(context_to_hr), + headerTransform=context_to_hr.get, + removeNull=True) + + +def scan_history_pagination_params(args: dict) -> dict: + ''' + Generate pagination parameters for scanning history based on the given arguments. + + This function calculates the 'limit' and 'offset' parameters for pagination + based on the provided 'page' and 'pageSize' arguments. If 'page' and 'pageSize' + are valid integer values, the function returns a dictionary containing 'limit' + and 'offset' calculated accordingly. If 'page' or 'pageSize' are not valid integers, + the function falls back to using the 'limit' argument or defaults to 50 with an + 'offset' of 0. + + Args: + args (dict): The demisto.args() dictionary containing the optional arguments for pagination: 'page', 'pageSize', 'limit'. + + Returns: + dict: A dictionary containing the calculated 'limit' and 'offset' parameters + for pagination. + ''' + page = arg_to_number(args.get('page')) + page_size = arg_to_number(args.get('pageSize')) + if isinstance(page, int) and isinstance(page_size, int): + return { + 'limit': page_size, + 'offset': (page - 1) * page_size + } + + else: + return { + 'limit': args.get('limit', 50), + 'offset': 0 + } + + +def scan_history_params(args: dict) -> dict: + sort_fields = argToList(args.get('sortFields')) + sort_order = argToList(args.get('sortOrder')) + + if len(sort_order) == 1: + sort_order *= len(sort_fields) + + return { + 'sort': ','.join( + f'{field}:{order}' + for field, order + in zip(sort_fields, sort_order)), + 'exclude_rollover': args['excludeRollover'], + } | scan_history_pagination_params(args) + + +def get_scan_history_command(args: dict[str, Any], client: Client) -> CommandResults: + + response_json = client.get_scan_history( + args['scanId'], + scan_history_params(args)) + history = response_json.get('history', '') + + return CommandResults( + outputs_prefix='TenableIO.ScanHistory', + outputs_key_field='id', + outputs=history, + readable_output=scan_history_readable(history)) + + +def build_filters(filters: str | None) -> dict: + """ + Build a dictionary of filter information from a filters string. + + Args: + filters (str, optional): A string containing filters in the format "name quality value" separated by commas. + Escaped commas (\\,) and spaces (\\s) are treated as literal characters. + Defaults to None. + + Returns: + dict: A dictionary where keys are in the format 'filter.i.filter', 'filter.i.quality', and 'filter.i.value', + and values correspond to the name, quality, and value of each filter component. + + Example: + filters = "name1 good value1\\,with\\,commas, name2\\swith\\sspaces excellent value2" + result = build_filters(filters) + # Output: + # { + # 'filter.0.filter': 'name1', + # 'filter.0.quality': 'good', + # 'filter.0.value': 'value1,with,commas', + # 'filter.1.filter': 'name2 with spaces', + # 'filter.1.quality': 'excellent', + # 'filter.1.value': 'value2' + # } + """ + if not filters: + return {} + + # split by comma without escaped commas + split_filters = re.split(r'(? dict: + + if chapters := args.get('chapters'): + chapters = ';'.join(argToList(chapters)) + elif args['format'] in ('PDF', 'HTML'): + raise DemistoException('The "chapters" field must be provided for PDF or HTML formats.') + + body = { + 'format': args['format'].lower(), + 'chapters': chapters, + 'filter.search_type': args['filterSearchType'].lower(), + 'asset_id': args.get('assetId'), + } | build_filters(args.get('filter')) + + return body + + +def initiate_export_scan(args: dict, client: Client) -> str: + return client.initiate_export_scan( + args['scanId'], + params={ + 'history_id': args.get('historyId'), + 'history_uuid': args.get('historyUuid') + }, + body=export_scan_body(args) + ).get('file', '') + + +@polling_function( + 'tenable-io-export-scan', + poll_message='Preparing scan report:', + interval=15, + requires_polling_arg=False) +def export_scan_command(args: dict[str, Any], client: Client) -> PollResult: + ''' + Calls three endpoints. The first (called with initiate_export_scan) initiates an export and returns a file ID. + The second (called with client.check_export_scan_status) checks the status of the export and the function polls + until the status is 'ready'. The third endpoint is then called (with client.download_export_scan) which downloads + the file and returns a dict with it's contents (using fileResult). + ''' + + scan_id = args['scanId'] + file_id = ( + args.get('fileId') + or initiate_export_scan(args, client)) + demisto.debug(f'{file_id=}') + + status_response = client.check_export_scan_status(scan_id, file_id) + demisto.debug(f'{status_response=}') + + match status_response.get('status'): + case 'ready': + return PollResult( + client.download_export_scan( + scan_id, file_id, args['format']), + continue_to_poll=False) + + case 'loading': + return PollResult( + None, + continue_to_poll=True, + args_for_next_run={ + 'fileId': file_id, + 'scanId': scan_id, + 'format': args['format'], # not necessary but avoids confusion + }) + + case _: + raise DemistoException( + 'Tenable IO encountered an error while exporting the scan report file.\n' + f'Scan ID: {scan_id}\n' + f'File ID: {file_id}\n') + + def main(): # pragma: no cover + if not (ACCESS_KEY and SECRET_KEY): raise DemistoException('Access Key and Secret Key must be provided.') - if demisto.command() == 'test-module': - demisto.results(test_module()) - elif demisto.command() == 'tenable-io-list-scans': + + client = Client( + BASE_URL, + verify=USE_SSL, + proxy=USE_PROXY, + ok_codes=(200,), + headers=HEADERS + ) + args = demisto.args() + command = demisto.command() + + if command == 'test-module': + demisto.results(test_module(client)) + elif command == 'tenable-io-list-scans': demisto.results(get_scans_command()) - elif demisto.command() == 'tenable-io-launch-scan': + elif command == 'tenable-io-launch-scan': demisto.results(launch_scan_command()) - elif demisto.command() == 'tenable-io-get-scan-report': + elif command == 'tenable-io-get-scan-report': demisto.results(get_report_command()) - elif demisto.command() == 'tenable-io-get-vulnerability-details': + elif command == 'tenable-io-get-vulnerability-details': demisto.results(get_vulnerability_details_command()) - elif demisto.command() == 'tenable-io-get-vulnerabilities-by-asset': + elif command == 'tenable-io-get-vulnerabilities-by-asset': demisto.results(get_vulnerabilities_by_asset_command()) - elif demisto.command() == 'tenable-io-get-scan-status': + elif command == 'tenable-io-get-scan-status': demisto.results(get_scan_status_command()) - elif demisto.command() == 'tenable-io-pause-scan': + elif command == 'tenable-io-pause-scan': demisto.results(pause_scan_command()) - elif demisto.command() == 'tenable-io-resume-scan': + elif command == 'tenable-io-resume-scan': demisto.results(resume_scan_command()) - elif demisto.command() == 'tenable-io-get-asset-details': + elif command == 'tenable-io-get-asset-details': return_results(get_asset_details_command()) - elif demisto.command() == 'tenable-io-export-assets': - return_results(export_assets_command(demisto.args())) - elif demisto.command() == 'tenable-io-export-vulnerabilities': - return_results(export_vulnerabilities_command(demisto.args())) + elif command == 'tenable-io-export-assets': + return_results(export_assets_command(args)) + elif command == 'tenable-io-export-vulnerabilities': + return_results(export_vulnerabilities_command(args)) + elif command == 'tenable-io-list-scan-filters': + return_results(list_scan_filters_command(client)) + elif command == 'tenable-io-get-scan-history': + return_results(get_scan_history_command(args, client)) + elif command == 'tenable-io-export-scan': + return_results(export_scan_command(args, client)) if __name__ in ['__main__', 'builtin', 'builtins']: diff --git a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.yml b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.yml index b08f849d3c24..5fcc8c85977f 100644 --- a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.yml +++ b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io.yml @@ -1088,11 +1088,188 @@ script: - contextPath: TenableIO.Vulnerability.indexed description: The date and time (in Unix time) when the vulnerability was indexed into Tenable.io. type: Date + - arguments: [] + description: Lists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them. + name: tenable-io-list-scan-filters + outputs: + - contextPath: TenableIO.ScanFilter.name + description: The name of the scan filter. + type: String + - contextPath: TenableIO.ScanFilter.readable_name + description: The readable name of the scan filter. + type: String + - contextPath: TenableIO.ScanFilter.control.type + description: The type of control associated with the scan filter. + type: String + - contextPath: TenableIO.ScanFilter.control.regex + description: The regular expression used by the scan filter. + type: String + - contextPath: TenableIO.ScanFilter.control.readable_regex + description: An example expression that the filter's regular expression would match. + type: String + - contextPath: TenableIO.ScanFilter.operators + description: The operators available for the scan filter. + type: String + - contextPath: TenableIO.ScanFilter.group_name + description: The group name associated with the scan filter. + type: String + - arguments: + - name: scanId + description: The ID of the scan of which to get the runs. + required: true + - name: sortFields + description: A comma-separated list of fields by which to sort, in the order defined by "sortOrder". + isArray: true + auto: PREDEFINED + predefined: + - start_date + - end_date + - status + - name: sortOrder + description: | + A comma-separated list of directions in which to sort the fields defined by "sortFields". + If multiple directions are chosen, they will be sequentially matched with "sortFields". + If only one direction is chosen it will be used to sort all values in "sortFields". + For example: + If sortFields is "start_date,status" and sortOrder is "asc,desc", + then start_date is sorted in ascending order and status in descending order. + If sortFields is "start_date,status" and sortOrder is simply "asc", + then both start_date and status are sorted in ascending order. + isArray: true + defaultValue: asc + auto: PREDEFINED + predefined: + - asc + - desc + - name: excludeRollover + description: Whether to exclude rollover scans from the scan history. + defaultValue: 'false' + auto: PREDEFINED + predefined: + - 'true' + - 'false' + - name: page + description: The page number of scan records to retrieve (used for pagination) starting from 1. The page size is defined by the "pageSize" argument. + - name: pageSize + description: The number of scan records per page to retrieve (used for pagination). The page number is defined by the "page" argument. + - name: limit + description: The maximum number of records to retrieve. If "pageSize" is defined, this argument is ignored. + defaultValue: '50' + description: Lists the individual runs of the specified scan. + name: tenable-io-get-scan-history + outputs: + - contextPath: TenableIO.ScanHistory.time_end + description: The end time of the scan. + type: Number + - contextPath: TenableIO.ScanHistory.scan_uuid + description: The UUID (Universally Unique Identifier) of the scan. + type: String + - contextPath: TenableIO.ScanHistory.id + description: The ID of the scan history. + type: Number + - contextPath: TenableIO.ScanHistory.is_archived + description: Indicates whether the scan is archived or not. + type: Boolean + - contextPath: TenableIO.ScanHistory.time_start + description: The start time of the scan. + type: Number + - contextPath: TenableIO.ScanHistory.visibility + description: The visibility of the scan. + type: String + - contextPath: TenableIO.ScanHistory.targets.custom + description: Indicates whether custom targets were used in the scan. + type: Boolean + - contextPath: TenableIO.ScanHistory.targets.default + description: Indicates whether the default targets were used in the scan. + type: Boolean + - contextPath: TenableIO.ScanHistory.status + description: The status of the scan. + type: String + - arguments: + - name: scanId + description: The identifier for the scan to export. Run the "tenable-io-list-scans" command to get all available scans. + required: true + - name: historyId + description: The unique identifier of the historical data to export. Run the "tenable-io-get-scan-history" command to get history IDs. + - name: historyUuid + description: The UUID of the historical data to export. Run the "tenable-io-get-scan-history" command to get history UUIDs. + - name: format + required: true + description: | + The file format to export the scan in. Scans can be export in the HTML and PDF formats for up to 35 days. + For scans that are older than 35 days, only the Nessus and CSV formats are supported. + The "chapters" argument must be defined if the chosen format is HTML or PDF. + defaultValue: CSV + auto: PREDEFINED + predefined: + - Nessus + - HTML + - PDF + - CSV + - name: chapters + description: A comma-separated list of chapters to include in the export. This argument is required if the file format is PDF or HTML. + isArray: true + auto: PREDEFINED + predefined: + - vuln_hosts_summary + - vuln_by_host + - compliance_exec + - remediations + - vuln_by_plugin + - compliance + - name: filter + description: | + A comma-separated list of filters, in the format of "name quality value" to apply to the exported scan report. + Example: "port.protocol eq tcp, plugin_id eq 1234567" + Note: when used literally, commas and spaces should be escaped. (i.e. "\\," for comma and "\\s" for space) + Filters cannot be applied to scans older than 35 days. + Run "tenable-io-list-scan-filters" to get all available filters, ("Filter name" (name), "Filter operators" (quality) and "Readable regex" (value) in response). + For more information: https://developer.tenable.com/docs/scan-export-filters-tio + isArray: true + - name: filterSearchType + description: For multiple filters, specifies whether to use the AND or the OR logical operator. + defaultValue: AND + auto: PREDEFINED + predefined: + - AND + - OR + - name: assetId + description: The ID of the asset scanned. + - name: fileId + description: '' + hidden: true + - name: hide_polling_output + description: '' + hidden: true + polling: true + description: | + Export and download a scan report. + Scan results older than 35 days are supported in Nessus and CSV formats only, and filters cannot be applied. + Scans that are actively running cannot be exported (run "tenable-io-list-scans" to view scan statuses) + name: tenable-io-export-scan + outputs: + - contextPath: InfoFile.Size + description: The size of the file in bytes. + type: number + - contextPath: InfoFile.Name + description: The name of the file. + type: string + - contextPath: InfoFile.EntryID + description: The War Room entry ID of the file. + type: string + - contextPath: InfoFile.Info + description: The format and encoding of the file. + type: string + - contextPath: InfoFile.Type + description: The type of the file. + type: string + - contextPath: InfoFile.Extension + description: The file extension of the file. runonce: false script: '-' subtype: python3 type: python - dockerimage: demisto/python3:3.10.12.63474 + dockerimage: demisto/python3:3.10.12.68714 tests: - Tenable.io test fromversion: 5.0.0 diff --git a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io_test.py b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io_test.py index 0a58a0f85c80..45e3c97b5e73 100644 --- a/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io_test.py +++ b/Packs/Tenable_io/Integrations/Tenable_io/Tenable_io_test.py @@ -2,6 +2,8 @@ import pytest from freezegun import freeze_time from CommonServerPython import * +import json +# mypy: disable-error-code="operator" MOCK_PARAMS = { 'access-key': 'fake_access_key', @@ -42,12 +44,26 @@ 'VulnerabilityState': 'Resurfaced' } ] +MOCK_CLIENT_ARGS = { + 'base_url': MOCK_PARAMS['url'], + 'verify': True, + 'proxy': True, + 'ok_codes': (200,), +} + + +def load_json(filename): + with open(f'test_data/{filename}.json') as f: + return json.load(f) -def mock_demisto(mocker, mock_args): +def mock_demisto(mocker, mock_args=None): mocker.patch.object(demisto, 'params', return_value=MOCK_PARAMS) mocker.patch.object(demisto, 'args', return_value=mock_args) + mocker.patch.object(demisto, 'uniqueFile', return_value='file') + mocker.patch.object(demisto, 'investigation', return_value={'id': 'id'}) mocker.patch.object(demisto, 'results') + mocker.patch.object(demisto, 'debug') def test_get_scan_status(mocker, requests_mock): @@ -179,6 +195,8 @@ def test_get_asset_details_command(mocker, requests_mock): ('2.5-3.5', 2.5, 3.5), ('0.1-3', 0.1, 3), ('0.1 - 3', 0.1, 3), + ('0-1', 'exception', 'exception'), + ('3-100', 'exception', 'exception'), ('0', 'exception', 'exception'), ('3-0', 'exception', 'exception') ]) @@ -193,11 +211,10 @@ def test_validate_range(range_str, expected_lower_range_bound, expected_upper_ra """ from Tenable_io import validate_range - if isinstance(expected_lower_range_bound, str) and isinstance(expected_upper_range_bound, str): - with pytest.raises(DemistoException) as de: + if expected_lower_range_bound == expected_upper_range_bound == 'exception': + err_msg = 'Please specify a valid vprScoreRange. The VPR values range is 0.1-10.0.' + with pytest.raises(DemistoException, match=err_msg): lower_range_bound, upper_range_bound = validate_range(range_str) - - assert de.value.message == 'Please specify valid vprScoreRange. VPR values range are 0.1-10.0.' else: lower_range_bound, upper_range_bound = validate_range(range_str) assert lower_range_bound == expected_lower_range_bound @@ -348,7 +365,7 @@ def test_export_assets_command(mocker, args, return_value_export_request_with_ex from Tenable_io import export_assets_command import Tenable_io from test_data.response_and_results import export_assets_response - mocker.patch.object(ScheduledCommand, 'raise_error_if_not_supported', return_value=None) + mocker.patch.object(ScheduledCommand, 'raise_error_if_not_supported') mocker.patch.object(Tenable_io, 'export_request', return_value={"export_uuid": 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'}) mocker.patch.object(Tenable_io, 'export_request_with_export_uuid', return_value={"export_uuid": 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'}) @@ -417,3 +434,259 @@ def test_export_vulnerabilities_command(mocker, args, return_value_export_reques ' 2023-08-15T15:56:18.852Z | 2023-08-15T15:56:18.852Z |' \ ' some_description | solution. |\n' assert response.raw_response == export_vulnerabilities_response + + +@pytest.mark.parametrize( + 'args, expected_result', + [ + # Test case 1: Basic input with limit and offset + ( + { + 'sortFields': '', + 'sortOrder': 'asc', + 'excludeRollover': False, + 'limit': 10, + 'offset': 0 + }, + { + 'sort': '', + 'exclude_rollover': False, + 'limit': 10, + 'offset': 0, + } + ), + # Test case 2: Sorting by a multiple fields in ascending order + ( + { + 'sortFields': 'name,date,status', + 'sortOrder': 'asc', + 'excludeRollover': False, + }, + { + 'sort': 'name:asc,date:asc,status:asc', + 'exclude_rollover': False, + 'limit': 50, + 'offset': 0 + } + ), + # Test case 3: Sorting by multiple fields in different orders + ( + { + 'sortFields': 'name,date,status', + 'sortOrder': 'asc,desc,asc', + 'excludeRollover': False, + }, + { + 'sort': 'name:asc,date:desc,status:asc', + 'exclude_rollover': False, + 'limit': 50, + 'offset': 0 + } + ) + ] +) +def test_scan_history_params(args, expected_result): + """ + Given: + Case 1: Only sortOrder is defined (Default). + Case 2: The sortFields has multiple values and sortOrder only one. + Case 3: Both sort lists have multiple values. + + When: + - Running the tenable-io-get-scan-history command. + + Then: + Case 1: Return empty sort. + Case 2: Sort all sortFields by sortOrder. + Case 3: Match sortFields and sortOrder's values by index. + """ + from Tenable_io import scan_history_params + + result = scan_history_params(args) + + assert result == expected_result + + +def test_list_scan_filters_command(mocker): + ''' + Given: + - A request to list Tenable IO scan filters. + + When: + - Running the "list-scan-filters" command. + + Then: + - Verify that tenable-io-list-scan-filters command works as expected. + ''' + from Tenable_io import list_scan_filters_command, Client + + test_data = load_json('list_scan_filters') + + request = mocker.patch.object(BaseClient, '_http_request', return_value=test_data['response_json']) + mock_demisto(mocker) + + results = list_scan_filters_command(Client(**MOCK_CLIENT_ARGS)) + + assert results.outputs == test_data['outputs'] + assert results.readable_output == test_data['readable_output'] + assert results.outputs_prefix == 'TenableIO.ScanFilter' + assert results.outputs_key_field == 'name' + + request.assert_called_with(*test_data['called_with']['args']) + + +def test_get_scan_history_command(mocker): + ''' + Given: + - A request to get Tenable IO scan history. + + When: + - Running the "get-scan-history" command. + + Then: + - Verify that tenable-io-get-scan-history command works as expected. + ''' + from Tenable_io import get_scan_history_command, Client + + test_data = load_json('get_scan_history') + + request = mocker.patch.object( + BaseClient, '_http_request', return_value=test_data['response_json']) + mock_demisto(mocker) + + results = get_scan_history_command(test_data['args'], Client(**MOCK_CLIENT_ARGS)) + + assert results.outputs_prefix == 'TenableIO.ScanHistory' + assert results.outputs_key_field == 'id' + assert results.outputs == test_data['outputs'] + assert results.readable_output == test_data['readable_output'] + + request.assert_called_with( + *test_data['called_with']['args'], + **test_data['called_with']['kwargs']) + + +def test_initiate_export_scan(mocker): + ''' + Given: + - A request to export a scan report. + + When: + - Running the "export-scan" command. + + Then: + - Initiate an export scan request. + ''' + + from Tenable_io import initiate_export_scan, Client + + test_data = load_json('initiate_export_scan') + mock_demisto(mocker) + request = mocker.patch.object( + BaseClient, '_http_request', return_value=test_data['response_json']) + file = initiate_export_scan(test_data['args'], Client(**MOCK_CLIENT_ARGS)) + + assert file == test_data['expected_file'] + request.assert_called_with( + *test_data['called_with']['args'], + **test_data['called_with']['kwargs']) + + +def test_download_export_scan(mocker): + ''' + Given: + - A request to export a scan report. + + When: + - Running the "export-scan" command. + + Then: + - Initiate an export scan request. + ''' + from Tenable_io import Client + + mock_demisto(mocker) + mocker.patch.object(ScheduledCommand, 'raise_error_if_not_supported') + request = mocker.patch.object( + BaseClient, '_http_request', return_value=b'') + + result = Client(**MOCK_CLIENT_ARGS).download_export_scan('scan_id', 'file_id', 'HTML') + + assert result == { + 'Contents': '', + 'ContentsFormat': 'text', + 'Type': 9, + 'File': 'scan_scan_id_file_id.html', + 'FileID': 'file', + } + request.assert_called_with( + 'GET', 'scans/scan_id/export/file_id/download', resp_type='content') + + +@pytest.mark.parametrize( + 'args, response_json, message', + [ + ({'scanId': '', 'format': 'HTML', 'filterSearchType': ''}, {}, + 'The "chapters" field must be provided for PDF or HTML formats.'), + ({'scanId': '', 'format': '', 'filterSearchType': ''}, {'status': 'error'}, + 'Tenable IO encountered an error while exporting the scan report file.') + ] +) +def test_export_scan_command_errors(mocker, args, response_json, message): + ''' + Given: + - A request to export a scan report. + + When: + - Running the "export-scan" command in any of the following cases: + - Case A: The "format" arg is HTML or PDF but "chapters" is not defined. + - Case B: An export scan request has been made and the report status is "error" or unrecognized. + + Then: + - Return an error. + ''' + + from Tenable_io import export_scan_command, Client + + mock_demisto(mocker) + mocker.patch.object(ScheduledCommand, 'raise_error_if_not_supported') + mocker.patch.object(BaseClient, '_http_request', return_value=response_json) + mocker.patch.object(Client, 'initiate_export_scan', return_value={'file': 'file_id'}) + + with pytest.raises(DemistoException, match=message): + export_scan_command(args, Client(**MOCK_CLIENT_ARGS)) + + +@pytest.mark.parametrize( + 'args, expected_result', + ( + ( + { + 'page': '10', + 'pageSize': '5', + 'limit': '50' + }, + { + 'limit': 5, + 'offset': 45 + } + ), + ( + { + 'limit': '50', + 'page': '23' + }, + { + 'limit': '50', + 'offset': 0 + } + ) + ) +) +def test_scan_history_pagination_params(args, expected_result): + + from Tenable_io import scan_history_pagination_params + + result = scan_history_pagination_params(args) + + assert result == expected_result diff --git a/Packs/Tenable_io/Integrations/Tenable_io/command_examples.txt b/Packs/Tenable_io/Integrations/Tenable_io/command_examples.txt new file mode 100644 index 000000000000..82f0929a0016 --- /dev/null +++ b/Packs/Tenable_io/Integrations/Tenable_io/command_examples.txt @@ -0,0 +1,3 @@ +!tenable-io-list-scan-filters +!tenable-io-get-scan-history scanId=16 excludeRollover=true sortFields=end_date,status sortOrder=desc page=2 pageSize=30 +!tenable-io-export-scan scanId=16 format=HTML chapters="compliance_exec,remediations,vuln_by_plugin" historyId=19540157 historyUuid=f7eaad37-23bd-4aac-a979-baab0e9a465b filterSearchType=OR filter="port.protocol eq tcp, plugin_id eq 1234567" assetId=10 \ No newline at end of file diff --git a/Packs/Tenable_io/Integrations/Tenable_io/test_data/get_scan_history.json b/Packs/Tenable_io/Integrations/Tenable_io/test_data/get_scan_history.json new file mode 100644 index 000000000000..132361671042 --- /dev/null +++ b/Packs/Tenable_io/Integrations/Tenable_io/test_data/get_scan_history.json @@ -0,0 +1,83 @@ +{ + "response_json": { + "history": [ + { + "time_end": 1545945607, + "scan_uuid": "fc9dc7c5-8eec-4d39-ad9c-20e833cca69b", + "id": 10535512, + "is_archived": true, + "time_start": 1545945482, + "visibility": "public", + "targets": { + "custom": false, + "default": null + }, + "status": "canceled" + }, + { + "time_end": 1545945457, + "scan_uuid": "9e3a89e5-f3d0-4708-9ec9-403a34e7cd5e", + "id": 10535505, + "is_archived": true, + "time_start": 1545945321, + "visibility": "public", + "targets": { + "custom": false, + "default": null + }, + "status": "completed" + } + ] + }, + "outputs": [ + { + "time_end": 1545945607, + "scan_uuid": "fc9dc7c5-8eec-4d39-ad9c-20e833cca69b", + "id": 10535512, + "is_archived": true, + "time_start": 1545945482, + "visibility": "public", + "targets": { + "custom": false, + "default": null + }, + "status": "canceled" + }, + { + "time_end": 1545945457, + "scan_uuid": "9e3a89e5-f3d0-4708-9ec9-403a34e7cd5e", + "id": 10535505, + "is_archived": true, + "time_start": 1545945321, + "visibility": "public", + "targets": { + "custom": false, + "default": null + }, + "status": "completed" + } + ], + "readable_output": "### Tenable IO Scan History\n|History id|History uuid|Status|Is archived|Targets custom|Visibility|Time start|Time end|\n|---|---|---|---|---|---|---|---|\n| 10535512 | fc9dc7c5-8eec-4d39-ad9c-20e833cca69b | canceled | true | false | public | 1545945482 | 1545945607 |\n| 10535505 | 9e3a89e5-f3d0-4708-9ec9-403a34e7cd5e | completed | true | false | public | 1545945321 | 1545945457 |\n", + "args": { + "scanId": "16", + "excludeRollover": "true", + "sortFields": "end_date,status", + "sortOrder": "desc", + "page": "2", + "pageSize": "30" + }, + "called_with": { + "args": [ + "GET", + "scans/16/history" + ], + "kwargs": { + "params": { + "sort": "end_date:desc,status:desc", + "exclude_rollover": "true", + "limit": 30, + "offset": 30 + } + } + } +} \ No newline at end of file diff --git a/Packs/Tenable_io/Integrations/Tenable_io/test_data/initiate_export_scan.json b/Packs/Tenable_io/Integrations/Tenable_io/test_data/initiate_export_scan.json new file mode 100644 index 000000000000..22233f8b11ca --- /dev/null +++ b/Packs/Tenable_io/Integrations/Tenable_io/test_data/initiate_export_scan.json @@ -0,0 +1,43 @@ +{ + "args": { + "scanId": "16", + "format": "HTML", + "chapters": "compliance_exec,remediations,vuln_by_plugin", + "historyId": "19540157", + "historyUuid": "f7eaad37-23bd-4aac-a979-baab0e9a465b", + "filterSearchType": "or", + "filter": "port.protocol eq tcp, plugin_id eq 1234567, spaced\\sname comma\\,quality value", + "assetId": "10" + }, + "response_json": { + "file": "123456789" + }, + "expected_file": "123456789", + "called_with": { + "args": [ + "POST", + "scans/16/export" + ], + "kwargs": { + "params": { + "history_id": "19540157", + "history_uuid": "f7eaad37-23bd-4aac-a979-baab0e9a465b" + }, + "json_data": { + "format": "html", + "chapters": "compliance_exec;remediations;vuln_by_plugin", + "filter.search_type": "or", + "asset_id": "10", + "filter.0.filter": "port.protocol", + "filter.0.quality": "eq", + "filter.0.value": "tcp", + "filter.1.filter": "plugin_id", + "filter.1.quality": "eq", + "filter.1.value": "1234567", + "filter.2.filter": "spaced name", + "filter.2.quality": "comma,quality", + "filter.2.value": "value" + } + } + } +} \ No newline at end of file diff --git a/Packs/Tenable_io/Integrations/Tenable_io/test_data/list_scan_filters.json b/Packs/Tenable_io/Integrations/Tenable_io/test_data/list_scan_filters.json new file mode 100644 index 000000000000..0321a72825b4 --- /dev/null +++ b/Packs/Tenable_io/Integrations/Tenable_io/test_data/list_scan_filters.json @@ -0,0 +1,79 @@ +{ + "response_json": { + "filters": [ + { + "name": "host.id", + "readable_name": "Asset ID", + "control": { + "type": "entry", + "regex": "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})*", + "readable_regex": "a94fd560-f8d9-4ed1-9b46-cba00c21bcdb" + }, + "operators": [ + "eq", + "neq", + "match", + "nmatch" + ], + "group_name": null + }, + { + "name": "plugin.attributes.exploit_framework_canvas", + "readable_name": "CANVAS Exploit Framework", + "control": { + "type": "dropdown", + "list": [ + "true", + "false" + ] + }, + "operators": [ + "eq", + "neq" + ], + "group_name": null + } + ] + }, + "outputs": [ + { + "name": "host.id", + "readable_name": "Asset ID", + "control": { + "type": "entry", + "regex": "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})*", + "readable_regex": "a94fd560-f8d9-4ed1-9b46-cba00c21bcdb" + }, + "operators": [ + "eq", + "neq", + "match", + "nmatch" + ], + "group_name": null + }, + { + "name": "plugin.attributes.exploit_framework_canvas", + "readable_name": "CANVAS Exploit Framework", + "control": { + "type": "dropdown", + "list": [ + "true", + "false" + ] + }, + "operators": [ + "eq", + "neq" + ], + "group_name": null + } + ], + "readable_output": "### Tenable IO Scan Filters\n|Filter name|Filter Readable name|Filter Control type|Filter regex|Readable regex|Filter operators|\n|---|---|---|---|---|---|\n| host.id | Asset ID | entry | [0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}(,[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})* | a94fd560-f8d9-4ed1-9b46-cba00c21bcdb | eq,
neq,
match,
nmatch |\n| plugin.attributes.exploit_framework_canvas | CANVAS Exploit Framework | dropdown | | | eq,
neq |\n", + "called_with": { + "args": [ + "GET", + "filters/scans/reports" + ] + } +} \ No newline at end of file diff --git a/Packs/Tenable_io/ReleaseNotes/2_1_12.md b/Packs/Tenable_io/ReleaseNotes/2_1_12.md new file mode 100644 index 000000000000..28f2cf42a169 --- /dev/null +++ b/Packs/Tenable_io/ReleaseNotes/2_1_12.md @@ -0,0 +1,10 @@ + +#### Integrations + +##### Tenable.io + +- Added the following commands: + - ***tenable-io-list-scan-filters*** + - ***tenable-io-get-scan-history*** + - ***tenable-io-export-scan*** +- Updated the Docker image to: *demisto/python3:3.10.12.68714*. \ No newline at end of file diff --git a/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_Scan_Test.yml b/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_Scan_Test.yml index 0bf91be74344..532a43bf3711 100644 --- a/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_Scan_Test.yml +++ b/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_Scan_Test.yml @@ -5,10 +5,10 @@ starttaskid: "0" tasks: "0": id: "0" - taskid: e5bcdce1-05b5-40be-8a73-c38ae3dbee9f + taskid: cad70594-65e7-4983-8d68-7edf282b4c1f type: start task: - id: e5bcdce1-05b5-40be-8a73-c38ae3dbee9f + id: cad70594-65e7-4983-8d68-7edf282b4c1f version: -1 name: "" iscommand: false @@ -35,10 +35,10 @@ tasks: isautoswitchedtoquietmode: false "2": id: "2" - taskid: ed65bc4b-8534-4ee5-8eb2-9be5a3cae6d7 + taskid: b69f20c4-3994-45ef-8930-ed1404fd2dc0 type: regular task: - id: ed65bc4b-8534-4ee5-8eb2-9be5a3cae6d7 + id: b69f20c4-3994-45ef-8930-ed1404fd2dc0 version: -1 name: DeleteContext description: Delete field from context @@ -57,7 +57,7 @@ tasks: { "position": { "x": 50, - "y": 175 + "y": 195 } } note: false @@ -70,10 +70,10 @@ tasks: isautoswitchedtoquietmode: false "3": id: "3" - taskid: bc7d35a9-3cdc-4724-8bab-0a8dbe1648d3 + taskid: f6fbf897-9776-43f0-833d-10ab0e329430 type: playbook task: - id: bc7d35a9-3cdc-4724-8bab-0a8dbe1648d3 + id: f6fbf897-9776-43f0-833d-10ab0e329430 version: -1 name: Tenable.io Scan playbookName: Tenable.io Scan @@ -85,7 +85,7 @@ tasks: retry-count: simple: "2" scan-id: - simple: "16" + simple: "55" separatecontext: true loop: iscommand: false @@ -96,7 +96,7 @@ tasks: { "position": { "x": 50, - "y": 650 + "y": 720 } } note: false @@ -109,10 +109,10 @@ tasks: isautoswitchedtoquietmode: false "4": id: "4" - taskid: 6174091e-cfc1-4fc4-87ea-115a501e6c53 + taskid: 5d13467d-5039-4ceb-8d80-a11777c6c54f type: playbook task: - id: 6174091e-cfc1-4fc4-87ea-115a501e6c53 + id: 5d13467d-5039-4ceb-8d80-a11777c6c54f version: -1 name: GenericPolling description: |- @@ -132,17 +132,7 @@ tasks: - "3" scriptarguments: Ids: - complex: - root: TenableIO.Scan.Id - filters: - - - operator: containsGeneral - left: - value: - simple: TenableIO.Scan.Id - iscontext: true - right: - value: - simple: "16" + simple: "55" Interval: simple: "1" PollingCommandArgName: @@ -150,9 +140,9 @@ tasks: PollingCommandName: simple: tenable-io-get-scan-status Timeout: - simple: "10" + simple: "30" dt: - simple: TenableIO.Scan(val.Status == 'pending' || val.Status == 'running' || val.Status == 'publishing').Id + simple: TenableIO.Scan(val.Status != 'completed').Id separatecontext: true continueonerrortype: "" loop: @@ -164,7 +154,7 @@ tasks: { "position": { "x": 50, - "y": 490 + "y": 545 } } note: false @@ -176,14 +166,14 @@ tasks: isautoswitchedtoquietmode: false "5": id: "5" - taskid: 906f1daf-f844-4d7c-884a-8892b71ca63e + taskid: e1f6435c-3757-40d3-83c4-35ae1463ceea type: regular task: - id: 906f1daf-f844-4d7c-884a-8892b71ca63e + id: e1f6435c-3757-40d3-83c4-35ae1463ceea version: -1 - name: 'tenable.io list scans ' - description: Retrieves scans from the Tenable platform. - script: '|||tenable-io-list-scans' + name: 'Get scan status' + description: 'Checks the status of a specific scan using the scan ID. Possible values: "Running", "Completed", and "Empty" (Ready to run).' + script: '|||tenable-io-get-scan-status' type: regular iscommand: true brand: "" @@ -196,7 +186,7 @@ tasks: { "position": { "x": 50, - "y": 340 + "y": 370 } } note: false @@ -206,12 +196,15 @@ tasks: quietmode: 0 isoversize: false isautoswitchedtoquietmode: false + scriptarguments: + scanId: + simple: "55" view: |- { "linkLabelsPosition": {}, "paper": { "dimensions": { - "height": 695, + "height": 765, "width": 380, "x": 50, "y": 50 diff --git a/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_test.yml b/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_test.yml index 2e2d52e42838..b8202cbccc78 100644 --- a/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_test.yml +++ b/Packs/Tenable_io/TestPlaybooks/playbook-Tenable.io_test.yml @@ -5,10 +5,10 @@ starttaskid: "0" tasks: "0": id: "0" - taskid: f7932817-4391-4aa3-863d-73d83a011ecf + taskid: 2a8ea8d7-fc77-4104-8059-4de9874c0fe7 type: start task: - id: f7932817-4391-4aa3-863d-73d83a011ecf + id: 2a8ea8d7-fc77-4104-8059-4de9874c0fe7 version: -1 name: "" iscommand: false @@ -35,10 +35,10 @@ tasks: isautoswitchedtoquietmode: false "1": id: "1" - taskid: 56c528de-0cec-449f-8506-afe329207973 + taskid: e5b07211-3c08-4203-8828-b4a8449d948b type: regular task: - id: 56c528de-0cec-449f-8506-afe329207973 + id: e5b07211-3c08-4203-8828-b4a8449d948b version: -1 name: tenable-io-list-scans description: Retrive scans from the Tenable platform. @@ -57,7 +57,7 @@ tasks: { "position": { "x": 50, - "y": 360 + "y": 370 } } note: false @@ -70,10 +70,10 @@ tasks: isautoswitchedtoquietmode: false "2": id: "2" - taskid: 426ddd77-8f48-46a1-89e8-aa6d4782246a + taskid: 15756edb-296a-4c16-8459-6b8947f49f5a type: regular task: - id: 426ddd77-8f48-46a1-89e8-aa6d4782246a + id: 15756edb-296a-4c16-8459-6b8947f49f5a version: -1 name: tenable-io-get-scan-status description: 'Check the status of a specific scan using its ID. The status can hold following possible values : Running, Completed and Empty (Ready to run).' @@ -94,7 +94,7 @@ tasks: { "position": { "x": 50, - "y": 540 + "y": 545 } } note: false @@ -107,10 +107,10 @@ tasks: isautoswitchedtoquietmode: false "3": id: "3" - taskid: 14a9feb3-a1f1-4464-8063-c83bb3fe4eef + taskid: e8e416e2-87d8-481c-81a1-7067f6ab1d71 type: regular task: - id: 14a9feb3-a1f1-4464-8063-c83bb3fe4eef + id: e8e416e2-87d8-481c-81a1-7067f6ab1d71 version: -1 name: tenable-io-get-scan-report description: Retrive scan-report for the given scan. @@ -148,10 +148,10 @@ tasks: isautoswitchedtoquietmode: false "4": id: "4" - taskid: 7139c144-e5bd-4840-8266-7f2cf0b0055c + taskid: ccd80e24-0b34-4387-8c14-a0439031f6b3 type: regular task: - id: 7139c144-e5bd-4840-8266-7f2cf0b0055c + id: ccd80e24-0b34-4387-8c14-a0439031f6b3 version: -1 name: DeleteContext description: Delete field from context @@ -183,10 +183,10 @@ tasks: isautoswitchedtoquietmode: false "5": id: "5" - taskid: 23bbbd46-8773-4cf5-847f-ebe65b135f1c + taskid: 97d4cdbe-354d-4cf5-815e-526cc33af179 type: regular task: - id: 23bbbd46-8773-4cf5-847f-ebe65b135f1c + id: 97d4cdbe-354d-4cf5-815e-526cc33af179 version: -1 name: tenable-io-get-vulnerability-details description: Retrieve details for the given vulnerability. @@ -222,10 +222,10 @@ tasks: isautoswitchedtoquietmode: false "6": id: "6" - taskid: d3cd8fc0-900f-480e-8e14-dea887416c2d + taskid: c3ea6b10-1adb-4539-85e1-f897d2a1ccfe type: regular task: - id: d3cd8fc0-900f-480e-8e14-dea887416c2d + id: c3ea6b10-1adb-4539-85e1-f897d2a1ccfe version: -1 name: tenable-io-get-vulnerabilities-by-asset description: Get a list of up to 5000 of the vulnerabilities recorded for a given asset. By default, this list is sorted by vulnerability count, descending. @@ -259,10 +259,10 @@ tasks: isautoswitchedtoquietmode: false "7": id: "7" - taskid: 5a69fe6a-1c85-4736-84ad-ef961c1693c4 + taskid: 8668f8d2-3007-4ee9-81fd-c1e8752d2c3e type: regular task: - id: 5a69fe6a-1c85-4736-84ad-ef961c1693c4 + id: 8668f8d2-3007-4ee9-81fd-c1e8752d2c3e version: -1 name: Tenable-io-export-assets description: Retrieves details for the specified asset to include custom attributes. @@ -275,9 +275,11 @@ tasks: - "10" scriptarguments: chunkSize: - simple: "500" + simple: "100" serviceNowSysId: simple: "false" + retry-count: + simple: "5" separatecontext: false continueonerrortype: "" view: |- @@ -296,10 +298,10 @@ tasks: isautoswitchedtoquietmode: false "8": id: "8" - taskid: 1f3807f7-1ab6-4d00-844c-9f7d6f1a056d + taskid: b0488d35-5a4b-4b3e-858d-43c54d28a888 type: regular task: - id: 1f3807f7-1ab6-4d00-844c-9f7d6f1a056d + id: b0488d35-5a4b-4b3e-858d-43c54d28a888 version: -1 name: Tenable-io-export-vulnerability description: Retrieves details for the specified asset to include custom attributes. @@ -312,7 +314,9 @@ tasks: - "9" scriptarguments: numAssets: - simple: "500" + simple: "50" + timeOut: + simple: "120" separatecontext: false continueonerrortype: "" view: |- @@ -331,10 +335,10 @@ tasks: isautoswitchedtoquietmode: false "9": id: "9" - taskid: 17027ae1-c221-4daf-8bb0-9081d50375d8 + taskid: b1353dec-47ad-4833-8ae8-fd2942d5d376 type: condition task: - id: 17027ae1-c221-4daf-8bb0-9081d50375d8 + id: b1353dec-47ad-4833-8ae8-fd2942d5d376 version: -1 name: Verify vulnerabilities type: condition @@ -342,7 +346,7 @@ tasks: brand: "" nexttasks: "yes": - - "11" + - "13" separatecontext: false conditions: - label: "yes" @@ -369,10 +373,10 @@ tasks: isautoswitchedtoquietmode: false "10": id: "10" - taskid: 8115f6fa-a10f-4d84-87f4-ea8f4e059f66 + taskid: e9b3a100-5e5b-428c-877a-26dd3fb8b942 type: condition task: - id: 8115f6fa-a10f-4d84-87f4-ea8f4e059f66 + id: e9b3a100-5e5b-428c-877a-26dd3fb8b942 version: -1 name: Verify assets type: condition @@ -407,10 +411,10 @@ tasks: isautoswitchedtoquietmode: false "11": id: "11" - taskid: 10c45026-f73b-47fc-878a-26ed8b5ecab4 + taskid: 1ce016d5-80af-4005-8dbf-8785b39c121e type: title task: - id: 10c45026-f73b-47fc-878a-26ed8b5ecab4 + id: 1ce016d5-80af-4005-8dbf-8785b39c121e version: -1 name: Done type: title @@ -423,7 +427,241 @@ tasks: { "position": { "x": 50, - "y": 1920 + "y": 2645 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "12": + id: "12" + taskid: e06b064b-c26d-4125-8ee3-c2111dc11878 + type: regular + task: + id: e06b064b-c26d-4125-8ee3-c2111dc11878 + version: -1 + name: Run get-scan-history Command + description: Lists the individual runs of the specified scan. + script: Tenable.io|||tenable-io-get-scan-history + type: regular + iscommand: true + brand: Tenable.io + nexttasks: + '#none#': + - "14" + scriptarguments: + excludeRollover: + simple: "true" + limit: + simple: "20" + scanId: + complex: + root: TenableIO.Scan + filters: + - - operator: isEqualString + left: + value: + simple: TenableIO.Scan.Status + iscontext: true + right: + value: + simple: completed + accessor: Id + transformers: + - operator: atIndex + args: + index: + value: + simple: "0" + sortFields: + simple: end_date,status + sortOrder: + simple: desc + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2295 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "13": + id: "13" + taskid: 4d2d7fbd-eb29-42b7-84e6-b692d3e3ba4e + type: regular + task: + id: 4d2d7fbd-eb29-42b7-84e6-b692d3e3ba4e + version: -1 + name: Run list-scan-filters Command + description: Lists the filtering, sorting, and pagination capabilities available for scan records on endpoints/commands that support them. + script: Tenable.io|||tenable-io-list-scan-filters + type: regular + iscommand: true + brand: Tenable.io + nexttasks: + '#none#': + - "15" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1945 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "14": + id: "14" + taskid: cc10f48a-204b-4a51-8e71-69ded5e54112 + type: regular + task: + id: cc10f48a-204b-4a51-8e71-69ded5e54112 + version: -1 + name: Run export-scan Command + description: Export and download a scan report. + script: Tenable.io|||tenable-io-export-scan + type: regular + iscommand: true + brand: Tenable.io + nexttasks: + '#none#': + - "11" + scriptarguments: + chapters: + simple: vuln_hosts_summary,remediations,compliance_exec,vuln_by_host,compliance,vuln_by_plugin + format: + simple: CSV + historyId: + complex: + root: TenableIO.ScanHistory + filters: + - - operator: isEqualString + left: + value: + simple: TenableIO.ScanHistory.status + iscontext: true + right: + value: + simple: completed + accessor: id + transformers: + - operator: atIndex + args: + index: + value: + simple: "0" + historyUuid: + complex: + root: TenableIO.ScanHistory + filters: + - - operator: isEqualString + left: + value: + simple: TenableIO.ScanHistory.status + iscontext: true + right: + value: + simple: completed + accessor: scan_uuid + transformers: + - operator: atIndex + args: + index: + value: + simple: "0" + scanId: + complex: + root: TenableIO.Scan + filters: + - - operator: isEqualString + left: + value: + simple: TenableIO.Scan.Status + iscontext: true + right: + value: + simple: completed + accessor: Id + transformers: + - operator: atIndex + args: + index: + value: + simple: "0" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2470 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "15": + id: "15" + taskid: 0679e8ba-cb2e-4050-8c24-39861f642962 + type: condition + task: + id: 0679e8ba-cb2e-4050-8c24-39861f642962 + version: -1 + name: Verify list scan filters + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "12" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: isExists + left: + value: + simple: TenableIO.ScanFilter.name + iscontext: true + - - operator: isExists + left: + value: + simple: TenableIO.ScanFilter.control.readable_regex + iscontext: true + - - operator: isExists + left: + value: + simple: TenableIO.ScanFilter.operators + iscontext: true + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2120 } } note: false @@ -438,7 +676,7 @@ view: |- "linkLabelsPosition": {}, "paper": { "dimensions": { - "height": 1935, + "height": 2660, "width": 380, "x": 50, "y": 50 diff --git a/Packs/Tenable_io/pack_metadata.json b/Packs/Tenable_io/pack_metadata.json index bd318a50572c..08fd413709b1 100644 --- a/Packs/Tenable_io/pack_metadata.json +++ b/Packs/Tenable_io/pack_metadata.json @@ -1,8 +1,8 @@ { "name": "Tenable.io", - "description": "A comprehensive asset centric solution to accurately track\u00a0resources while accommodating\u00a0dynamic assets such as cloud, mobile devices, containers and web applications.", + "description": "A comprehensive asset centric solution to accurately track resources while accommodating dynamic assets such as cloud, mobile devices, containers and web applications.", "support": "xsoar", - "currentVersion": "2.1.11", + "currentVersion": "2.1.12", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Tests/conf.json b/Tests/conf.json index edcbc906b656..92f4df232b9d 100644 --- a/Tests/conf.json +++ b/Tests/conf.json @@ -1208,7 +1208,8 @@ }, { "integrations": "Tenable.io", - "playbookID": "Tenable.io test" + "playbookID": "Tenable.io test", + "timeout": 3600 }, { "playbookID": "URLDecode-Test"