A dependency-free Python module that exports everything out of cloud.tenable.com. Companion to
ec_client.py (ManageEngine). Stdlib only — no pip install, Python 3.8+.
📄 In a hurry? See QUICKSTART.md — a one-page command reference.
Same exporter, in the language you reach for:
| Language | Repo |
|---|---|
| Python | tenable-io-python (this repo) |
| PowerShell | tenable-io-powershell |
Create a key pair in Tenable: Settings → My Account → API Keys (or a service-account user under Access Control). The client resolves keys in this order: explicit arg → environment → OS secret store.
The secret store is picked automatically per platform — no extra module to install:
| OS | Store used |
|---|---|
| macOS | login Keychain (via security) |
| Windows | Credential Manager (via advapi32 / ctypes) |
| Linux / RHEL | Secret Service if present (secret-tool), else a 0600 owner-only file under ~/.local/share/tio_client/ |
Easiest — the store-keys command (hidden prompts for both keys, saves to whichever store fits
your OS, then validates). Works the same on macOS, Windows, and Linux:
python3 tio_client.py store-keys # enter ACCESS key, then SECRET key at the hidden prompts
python3 tio_client.py whoami # (store-keys already validates, but confirm anytime)
python3 tio_client.py export-all ./tio_dataOn a server the strongest option is to keep the keys out of any local file entirely. Two ways, both higher-precedence than the OS store:
# a) inject env vars from your secrets manager (CI secret, systemd LoadCredential, etc.)
export TIO_ACCESS_KEY=... export TIO_SECRET_KEY=...
# b) point the client at your vault — it runs the command and uses stdout as the key,
# writing nothing to disk. Works with Vault, CyberArk, AWS/Azure Secrets Manager, `pass`, …
export TIO_ACCESS_KEY_CMD="vault kv get -field=access secret/tenable"
export TIO_SECRET_KEY_CMD="vault kv get -field=secret secret/tenable"If you do fall back to the 0600 file, the client fails closed: it refuses to read keys.json
unless it's owner-only and owned by you (a loose umask or a planted file can't leak your keys).
Resolution order: CLI flag → TIO_* env value → TIO_*_CMD vault command → OS secret store.
Or store them manually (same result, two commands):
security add-generic-password -s tenable-io -a access -w # paste your ACCESS key at the prompt
security add-generic-password -s tenable-io -a secret -w # paste your SECRET key at the promptFirst run, macOS may show a Keychain "allow access" dialog for security / python — click Always
Allow. To update a key later, add -U to the add-generic-password command. To remove:
security delete-generic-password -s tenable-io -a access.
Alternative — environment variables (good for Linux/servers, CI):
export TIO_ACCESS_KEY=... export TIO_SECRET_KEY=...
python3 tio_client.py whoamiAlternative — command-line flags (quick one-offs / CI with masked secrets):
python3 tio_client.py --access-key AK --secret-key SK whoami
python3 tio_client.py --base https://fedcloud.tenable.com export-all ./tio_data # e.g. FedRAMPFlags may appear anywhere in the args and accept --flag value or --flag=value. Caveat: flag values
are visible in ps and shell history, so prefer Keychain/env for anything long-lived.
Resolution order: flag → environment → Keychain.
The keys are sent only to cloud.tenable.com over TLS, in the X-ApiKeys header. Nothing is stored or
transmitted elsewhere.
Big datasets — via Tenable's async export APIs (start → poll → download chunks), streamed to .jsonl
so they're memory-safe at any scale and drop straight into pandas / Power BI:
| Output | Contents |
|---|---|
vulns.jsonl |
Every vulnerability finding — the full object (plugin detail, ports, output, state, first/last found, VPR + its key drivers, recast, severity mods). Default includes OPEN + REOPENED and FIXED (full history) — drop FIXED for current-only. |
assets.jsonl |
Every asset/host — full object incl. ACR, AES/exposure score, tags, interfaces, sources, last-seen/scan. |
compliance.jsonl |
Compliance/audit findings (if you run compliance scans). |
plugins.jsonl |
The full plugin catalog — every plugin definition (~335k) with all attributes (CVSS, VPR, CVE, CPE, description, solution, see-also…), not just the ones in your findings. |
The export API returns the complete object per record — it's not a column subset, so these are already "all properties."
Configuration / inventory — .json: scans, policies, scanners, scanner_groups, agents,
agent_groups, networks, tag_values, tag_categories, exclusions, target_groups, access_groups,
credentials, credential_types, scan_templates, policy_templates, folders, users, groups,
plugin_families, vuln_fields + asset_fields (the catalog of every available column),
audit_log, server_properties, server_status, session.
The exact coverage depends on the role of the key's user:
- A Scan Manager key (perms 40) reaches everything above except admin-scoped endpoints, which
return
403 Insufficient scope: reports, audit log, roles, permissions, recast/accept-risk rules, access-groups v2, Lumin exposure, settings/tags. To pull those, use an Administrator key (perms 64). - WAS (Web App Scanning) data returns
404unless that module is licensed on the tenant. export_alltreats every endpoint as best-effort — anything that 403s/404s is skipped and noted in_manifest.json, so a lower-privileged key still gets a complete dump of what it's allowed to see.
export-all also writes _manifest.json listing every file + record count (or the error, if a given
endpoint was skipped — some depend on your license/permissions).
from tio_client import TenableIO
tio = TenableIO() # reads env keys
for v in tio.export_vulns(): # generator — memory-safe
...
tio.export_all("./tio_data")
# current-only vulns, criticals + highs, since a date:
tio.export_vulns(states=("OPEN","REOPENED"), severities=("critical","high"), since=1719792000)- Rate limits / transient errors are retried with backoff (honors
Retry-After). - Agents are listed per-scanner and aggregated (Tenable has no single "all agents" endpoint); each
record is tagged with
_scanner_id. - Endpoints that need a specific license/permission (compliance, audit log, access groups) are best-effort —
they're skipped and noted in
_manifest.jsonrather than failing the whole run. - Fits the connector plan (see the app's
docs/BACKEND-CONNECTOR.md): run this on the gateway server, land the.jsonlin Power BI via a dataflow, and the browser reads it directly.