Tenable#

The Tenable module ingests assets and vulnerability findings from the Tenable Export API. It uses the asynchronous bulk export workflow to retrieve assets and findings, then models related networks, cloud details, sources, tags, plugins, and scans.

The TenableTenant node uses the shared Tenant ontology label. Tenable asset tags use the shared Tag label and the TAGGED relationship. The deprecated HAS_TAG compatibility edge is still written in parallel and will be removed in v1.0.0.

Cloud compute#

When an asset is a cloud VM, its cloud details node links to the instance node that the AWS, Azure or GCP module wrote. The cloud modules must have synced first, which the default module order does.

Tenable node

Instance node

Matched on

TenableAssetAWS

AWSEC2Instance

EC2 instance ID

TenableAssetAzure

AzureVirtualMachine

Azure resource ID, ignoring case

TenableAssetGCP

GCPInstance

numeric instance ID and project ID

To find the EC2 instances affected by a finding:

MATCH (f:TenableFinding)-[:AFFECTS]->(:TenableAsset)-[:HAS_AWS_INFO]->(:TenableAssetAWS)
      -[:IS_INSTANCE]->(i:AWSEC2Instance)
RETURN f.id, i.id

CVEs#

Each CVE named by a plugin becomes a TenableCve node carrying the CVE ontology label. A finding reaches its CVEs through the plugin that detected it:

(:TenableFinding)-[:DETECTED_BY]->(:TenablePlugin)-[:HAS_CVE]->(:TenableCve:CVE)

Findings have no HAS_CVE edge of their own. A plugin for a cumulative OS update names hundreds of CVEs and is reported once per affected host, so an edge from every finding would multiply by the size of the fleet.

To find findings by CVE:

MATCH (:TenableCve {cve_id: 'CVE-2026-12345'})<-[:HAS_CVE]-(:TenablePlugin)
      <-[:DETECTED_BY]-(f:TenableFinding)
RETURN f

Plugin IDs are not yet tenant-qualified (see configuration), so a plugin node is shared by every tenant that reports it. In a graph holding several tenants, scope the findings with (f)<-[:RESOURCE]-(:TenableTenant {id: '<tenant id>'}).

Tenable’s export names CVEs as bare identifiers with no per-CVE scoring, and a plugin’s severity is Tenable’s roll-up over every CVE it covers. So TenableCve has no _ont_base_severity or _ont_vuln_status, and a query that filters CVE nodes on those fields will not return Tenable data. This differs from most scanner modules, which put the CVE label on the finding; Tenable puts it on the vulnerability record, like the cve and Ubuntu modules. Detection severity and state are TenableFinding.severity and TenableFinding.state.

The CVE label gives every TenableCve an _ont_cve_id, which lines it up with other providers’ records of the same CVE, including the NVD data from the cve module. To read a CVE’s own severity:

MATCH (f:TenableFinding)-[:DETECTED_BY]->(:TenablePlugin)-[:HAS_CVE]->(t:TenableCve)
MATCH (n:CVE {_ont_cve_id: t._ont_cve_id, _ont_source: 'cve'})
RETURN f.id, t.cve_id, f.severity AS detection_severity,
       n._ont_base_severity AS cve_severity

TenableCve.id is TNB|<tenant id>|<CVE ID>, for example TNB|cloud.tenable.com|CVE-2026-12345. Match on cve_id for the bare identifier.

Upgrading from an earlier version#

Earlier versions put the CVE label on TenableFinding and kept each finding’s CVE IDs in an indexed TenableFinding.cve_list. A plugin naming enough CVEs made that index reject the write and fail the whole sync. This version no longer writes the property, so the sync succeeds even while the old index exists, and every sync removes the legacy CVE label, _ont_* fields and cve_list from findings that carried the label.

Two leftovers do no harm and can be removed once, by hand. Do not run these before upgrading: an earlier version recreates both on every sync.

The now-unused index. Find its name:

SHOW INDEXES YIELD name, type, labelsOrTypes, properties
WHERE type = 'RANGE' AND labelsOrTypes = ['TenableFinding'] AND properties = ['cve_list']
RETURN name

and drop it:

DROP INDEX `<name>` IF EXISTS

Findings without CVEs never carried the label, but still hold _ont_* fields and an empty cve_list. Run this until it returns 0:

MATCH (f:TenableFinding)
WHERE f._ont_source IS NOT NULL OR f.cve_list IS NOT NULL
WITH f LIMIT 10000
REMOVE f._ont_source, f._ont_cve_id, f._ont_base_severity, f._ont_vuln_status,
       f.cve_list
RETURN count(f) AS removed

To look up findings by CVE, replace 'CVE-2026-12345' IN f.cve_list with the HAS_CVE query above. TenablePlugin.cve_list still lists a plugin’s CVEs.

See configuration for connection and scoping options, and the generated schema for fields and relationships.