tenable-exporter 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Surj Bains
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: tenable-exporter
3
+ Version: 1.1.0
4
+ Summary: Prometheus exporter for Tenable.io metrics
5
+ Author: Surj Bains
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Surj Bains
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: prometheus,tenable,exporter,security,metrics,devsecops
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Information Technology
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Topic :: Security
36
+ Classifier: Topic :: System :: Monitoring
37
+ Requires-Python: >=3.11
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: pytenable>=1.6.0
41
+ Requires-Dist: prometheus-client>=0.20.0
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8.0; extra == "dev"
44
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.4; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # tenable-exporter
49
+
50
+ ![tenable-exporter banner](assets/banner.png)
51
+
52
+ [![CI](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml)
53
+ [![CodeQL](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml)
54
+ [![PyPI](https://img.shields.io/pypi/v/tenable-exporter?logo=pypi&logoColor=white)](https://pypi.org/project/tenable-exporter/)
55
+ [![Python](https://img.shields.io/pypi/pyversions/tenable-exporter?logo=python&logoColor=white)](https://pypi.org/project/tenable-exporter/)
56
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
57
+ [![GHCR](https://img.shields.io/badge/ghcr.io-tenable--exporter-blue?logo=github)](https://github.com/polarpoint-io/tenable-exporter/pkgs/container/tenable-exporter)
58
+
59
+ A Prometheus exporter for [Tenable.io](https://www.tenable.com/) built with [pyTenable](https://github.com/tenable/pyTenable).
60
+
61
+ Exports vulnerability, asset, and scan metrics so you can alert on them in Grafana or any Prometheus-compatible stack.
62
+
63
+ > **PyPI**: `pip install tenable-exporter` &nbsp;·&nbsp; **Image**: `ghcr.io/polarpoint-io/tenable-exporter:latest` &nbsp;·&nbsp; **Repo**: <https://github.com/polarpoint-io/tenable-exporter>
64
+
65
+ ## Metrics
66
+
67
+ ### Vulnerabilities
68
+
69
+ | Metric | Labels | Description |
70
+ |---|---|---|
71
+ | `tenable_vulnerabilities_total` | `severity` | Total vulnerabilities by severity |
72
+ | `tenable_vulnerabilities_by_subscription_total` | `provider`, `subscription_id`, `severity` | Vulns per cloud provider and subscription |
73
+ | `tenable_vulnerabilities_by_region_total` | `provider`, `subscription_id`, `region`, `severity` | Vulns per subscription and region |
74
+ | `tenable_vulnerabilities_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `severity` | Vulns per Azure resource group |
75
+ | `tenable_vulnerabilities_by_resource_total` | `provider`, `subscription_id`, `resource_id`, `severity` | Vulns per individual cloud resource |
76
+ | `tenable_vulnerabilities_by_plugin_family_total` | `plugin_family`, `severity` | Vulns by Tenable plugin family and severity |
77
+ | `tenable_vulnerabilities_by_subscription_plugin_total` | `provider`, `subscription_id`, `region`, `plugin_family`, `severity` | Cross-dimension vuln count |
78
+ | `tenable_vulnerabilities_by_state_total` | `provider`, `subscription_id`, `state`, `severity` | Vulns by lifecycle state (`OPEN`, `REOPENED`, `FIXED`) — use `FIXED` to track remediation velocity |
79
+ | `tenable_vulnerabilities_by_exploit_risk_total` | `cve_category`, `severity` | Vulns by Tenable CVE category: `cisa known exploitable`, `ransomware`, `emerging threats`, `persistently exploited`, `top 50 vpr`, `recent active exploitation`, `in the news` |
80
+ | `tenable_vulnerabilities_by_vpr_band_total` | `provider`, `subscription_id`, `vpr_band` | Vulns by VPR (Vulnerability Priority Rating) band: `critical` (9–10), `high` (7–8.9), `medium` (4–6.9), `low` (<4) |
81
+
82
+ ### Assets
83
+
84
+ | Metric | Labels | Description |
85
+ |---|---|---|
86
+ | `tenable_assets_by_subscription_total` | `provider`, `subscription_id` | Asset count per cloud provider and subscription |
87
+ | `tenable_assets_by_region_total` | `provider`, `subscription_id`, `region` | Asset count per subscription and region |
88
+ | `tenable_assets_by_resource_group_total` | `provider`, `subscription_id`, `resource_group` | Asset count per Azure resource group |
89
+ | `tenable_assets_by_resource_type_total` | `provider`, `subscription_id`, `region`, `resource_type` | Asset count by resource type (e.g. `t3.medium`, `Standard_D2s_v3`) |
90
+ | `tenable_assets_by_source_total` | `source` | Assets by Tenable discovery source (`AWS`, `AZURE`, `GCP`, `NESSUS`, `WAS`, …) |
91
+ | `tenable_assets_by_tag_total` | `tag_category`, `tag_value` | Assets by Tenable tag or cloud-native resource tag. Use `tag_category=asset_type` with values like `database`, `container_registry`, `acr`, `aks`, `rds` to track specific resource classes |
92
+
93
+ ### Compliance
94
+
95
+ | Metric | Labels | Description |
96
+ |---|---|---|
97
+ | `tenable_compliance_findings_total` | `provider`, `subscription_id`, `audit_name`, `result` | CIS/DISA STIG compliance findings by audit and result (`PASSED`, `FAILED`, `WARNING`, `SKIPPED`) |
98
+ | `tenable_compliance_findings_by_region_total` | `provider`, `subscription_id`, `region`, `result` | Compliance findings per region |
99
+ | `tenable_compliance_findings_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `result` | Compliance findings per Azure resource group |
100
+
101
+ ### Scans & System
102
+
103
+ | Metric | Labels | Description |
104
+ |---|---|---|
105
+ | `tenable_scans_total` | — | Total number of scans |
106
+ | `tenable_scans_by_status_total` | `status` | Scans by status (running, completed, aborted, …) |
107
+ | `tenable_plugin_set_updated_timestamp` | — | Unix timestamp of the last plugin set update |
108
+
109
+ ### Label values by cloud provider
110
+
111
+ | Label | AWS | Azure | GCP |
112
+ |---|---|---|---|
113
+ | `provider` | `aws` | `azure` | `gcp` |
114
+ | `subscription_id` | Account ID | Subscription UUID | Project ID |
115
+ | `region` | e.g. `us-east-1` | Azure location | GCP zone |
116
+ | `resource_group` | `unknown` | Resource group name | `unknown` |
117
+ | `resource_id` | EC2 instance ID | Azure resource / VM ID | GCP instance ID |
118
+ | `resource_type` | EC2 instance type | VM size | Machine type |
119
+
120
+ ### Targeting databases, ACRs, and other resource types
121
+
122
+ Tenable doesn't have a dedicated field for resource class (database, container registry, etc.). The recommended approaches:
123
+
124
+ **Option 1 — Tenable tags (most reliable):** In the Tenable UI, create a tag category `AssetType` and assign values like `database`, `acr`, `aks`, `rds`, `cosmos_db` to assets. These appear immediately in `tenable_assets_by_tag_total{tag_category="assettype"}`.
125
+
126
+ **Option 2 — Cloud-native resource tags:** Enable `include_resource_tags=True` (already on). Any AWS tag, Azure tag, or GCP label on the resource appears as a `tenable_assets_by_tag_total` time series. For example, an Azure ACR tagged `{"resource_type": "container_registry"}` surfaces as `tag_category="resource_type", tag_value="container_registry"`.
127
+
128
+ **Option 3 — Plugin family:** Database vulnerabilities land in the `Databases` plugin family — visible in `tenable_vulnerabilities_by_plugin_family_total{plugin_family="databases"}`.
129
+
130
+ **Option 4 — Discovery source filter:** Scope the exporter to specific sources via `TENABLE_FILTER_PROVIDERS`. For ACR-specific scanning, Tenable uses the `AZURE` source; container-specific findings come from the `Containers` plugin family.
131
+
132
+ ## Quick start
133
+
134
+ ### pip
135
+
136
+ ```bash
137
+ pip install tenable-exporter
138
+ export TENABLE_ACCESS_KEY=your_access_key
139
+ export TENABLE_SECRET_KEY=your_secret_key
140
+
141
+ tenable-exporter
142
+ ```
143
+
144
+ Metrics will be available at `http://localhost:9190/metrics`.
145
+
146
+ ### Docker
147
+
148
+ ```bash
149
+ docker run -p 9190:9190 \
150
+ -e TENABLE_ACCESS_KEY=your_access_key \
151
+ -e TENABLE_SECRET_KEY=your_secret_key \
152
+ ghcr.io/polarpoint-io/tenable-exporter:latest
153
+ ```
154
+
155
+ ### Docker Compose
156
+
157
+ ```bash
158
+ cp .env.example .env
159
+ # Fill in your Tenable credentials in .env
160
+ docker compose up -d
161
+ ```
162
+
163
+ ## Configuration
164
+
165
+ | Environment variable | Default | Description |
166
+ |---|---|---|
167
+ | `TENABLE_ACCESS_KEY` | **required** | Tenable.io API access key |
168
+ | `TENABLE_SECRET_KEY` | **required** | Tenable.io API secret key |
169
+ | `EXPORTER_PORT` | `9190` | Port to expose metrics on |
170
+ | `SCRAPE_INTERVAL` | `300` | Seconds between Tenable API scrapes |
171
+ | `TENABLE_FILTER_PROVIDERS` | _(all)_ | Comma-separated providers to include: `aws`, `azure`, `gcp` |
172
+ | `TENABLE_FILTER_SUBSCRIPTIONS` | _(all)_ | Comma-separated subscription IDs to include (AWS account IDs, Azure subscription UUIDs, or GCP project IDs) |
173
+
174
+ ## Docker image tags
175
+
176
+ | Tag | When pushed |
177
+ |---|---|
178
+ | `latest` | Every merge to `main` |
179
+ | `sha-<short>` | Every merge to `main` |
180
+ | `1.2.3` / `1.2` | On a semantic-release version bump |
181
+
182
+ ## Required GitHub secrets
183
+
184
+ Add these at **GitHub repo → Settings → Secrets and variables → Actions**:
185
+
186
+ | Secret | Description |
187
+ |---|---|
188
+ | `POL_GH_TOKEN` | Personal access token with `repo` + `write:packages` scope |
189
+ | `PYPI_TOKEN` | PyPI API token for the `tenable-exporter` project |
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ git clone https://github.com/polarpoint-io/tenable-exporter.git
195
+ cd tenable-exporter
196
+ pip install -e ".[dev]"
197
+
198
+ export TENABLE_ACCESS_KEY=...
199
+ export TENABLE_SECRET_KEY=...
200
+
201
+ tenable-exporter
202
+ ```
203
+
204
+ ## License
205
+
206
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,159 @@
1
+ # tenable-exporter
2
+
3
+ ![tenable-exporter banner](assets/banner.png)
4
+
5
+ [![CI](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml)
6
+ [![CodeQL](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml)
7
+ [![PyPI](https://img.shields.io/pypi/v/tenable-exporter?logo=pypi&logoColor=white)](https://pypi.org/project/tenable-exporter/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/tenable-exporter?logo=python&logoColor=white)](https://pypi.org/project/tenable-exporter/)
9
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
10
+ [![GHCR](https://img.shields.io/badge/ghcr.io-tenable--exporter-blue?logo=github)](https://github.com/polarpoint-io/tenable-exporter/pkgs/container/tenable-exporter)
11
+
12
+ A Prometheus exporter for [Tenable.io](https://www.tenable.com/) built with [pyTenable](https://github.com/tenable/pyTenable).
13
+
14
+ Exports vulnerability, asset, and scan metrics so you can alert on them in Grafana or any Prometheus-compatible stack.
15
+
16
+ > **PyPI**: `pip install tenable-exporter` &nbsp;·&nbsp; **Image**: `ghcr.io/polarpoint-io/tenable-exporter:latest` &nbsp;·&nbsp; **Repo**: <https://github.com/polarpoint-io/tenable-exporter>
17
+
18
+ ## Metrics
19
+
20
+ ### Vulnerabilities
21
+
22
+ | Metric | Labels | Description |
23
+ |---|---|---|
24
+ | `tenable_vulnerabilities_total` | `severity` | Total vulnerabilities by severity |
25
+ | `tenable_vulnerabilities_by_subscription_total` | `provider`, `subscription_id`, `severity` | Vulns per cloud provider and subscription |
26
+ | `tenable_vulnerabilities_by_region_total` | `provider`, `subscription_id`, `region`, `severity` | Vulns per subscription and region |
27
+ | `tenable_vulnerabilities_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `severity` | Vulns per Azure resource group |
28
+ | `tenable_vulnerabilities_by_resource_total` | `provider`, `subscription_id`, `resource_id`, `severity` | Vulns per individual cloud resource |
29
+ | `tenable_vulnerabilities_by_plugin_family_total` | `plugin_family`, `severity` | Vulns by Tenable plugin family and severity |
30
+ | `tenable_vulnerabilities_by_subscription_plugin_total` | `provider`, `subscription_id`, `region`, `plugin_family`, `severity` | Cross-dimension vuln count |
31
+ | `tenable_vulnerabilities_by_state_total` | `provider`, `subscription_id`, `state`, `severity` | Vulns by lifecycle state (`OPEN`, `REOPENED`, `FIXED`) — use `FIXED` to track remediation velocity |
32
+ | `tenable_vulnerabilities_by_exploit_risk_total` | `cve_category`, `severity` | Vulns by Tenable CVE category: `cisa known exploitable`, `ransomware`, `emerging threats`, `persistently exploited`, `top 50 vpr`, `recent active exploitation`, `in the news` |
33
+ | `tenable_vulnerabilities_by_vpr_band_total` | `provider`, `subscription_id`, `vpr_band` | Vulns by VPR (Vulnerability Priority Rating) band: `critical` (9–10), `high` (7–8.9), `medium` (4–6.9), `low` (<4) |
34
+
35
+ ### Assets
36
+
37
+ | Metric | Labels | Description |
38
+ |---|---|---|
39
+ | `tenable_assets_by_subscription_total` | `provider`, `subscription_id` | Asset count per cloud provider and subscription |
40
+ | `tenable_assets_by_region_total` | `provider`, `subscription_id`, `region` | Asset count per subscription and region |
41
+ | `tenable_assets_by_resource_group_total` | `provider`, `subscription_id`, `resource_group` | Asset count per Azure resource group |
42
+ | `tenable_assets_by_resource_type_total` | `provider`, `subscription_id`, `region`, `resource_type` | Asset count by resource type (e.g. `t3.medium`, `Standard_D2s_v3`) |
43
+ | `tenable_assets_by_source_total` | `source` | Assets by Tenable discovery source (`AWS`, `AZURE`, `GCP`, `NESSUS`, `WAS`, …) |
44
+ | `tenable_assets_by_tag_total` | `tag_category`, `tag_value` | Assets by Tenable tag or cloud-native resource tag. Use `tag_category=asset_type` with values like `database`, `container_registry`, `acr`, `aks`, `rds` to track specific resource classes |
45
+
46
+ ### Compliance
47
+
48
+ | Metric | Labels | Description |
49
+ |---|---|---|
50
+ | `tenable_compliance_findings_total` | `provider`, `subscription_id`, `audit_name`, `result` | CIS/DISA STIG compliance findings by audit and result (`PASSED`, `FAILED`, `WARNING`, `SKIPPED`) |
51
+ | `tenable_compliance_findings_by_region_total` | `provider`, `subscription_id`, `region`, `result` | Compliance findings per region |
52
+ | `tenable_compliance_findings_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `result` | Compliance findings per Azure resource group |
53
+
54
+ ### Scans & System
55
+
56
+ | Metric | Labels | Description |
57
+ |---|---|---|
58
+ | `tenable_scans_total` | — | Total number of scans |
59
+ | `tenable_scans_by_status_total` | `status` | Scans by status (running, completed, aborted, …) |
60
+ | `tenable_plugin_set_updated_timestamp` | — | Unix timestamp of the last plugin set update |
61
+
62
+ ### Label values by cloud provider
63
+
64
+ | Label | AWS | Azure | GCP |
65
+ |---|---|---|---|
66
+ | `provider` | `aws` | `azure` | `gcp` |
67
+ | `subscription_id` | Account ID | Subscription UUID | Project ID |
68
+ | `region` | e.g. `us-east-1` | Azure location | GCP zone |
69
+ | `resource_group` | `unknown` | Resource group name | `unknown` |
70
+ | `resource_id` | EC2 instance ID | Azure resource / VM ID | GCP instance ID |
71
+ | `resource_type` | EC2 instance type | VM size | Machine type |
72
+
73
+ ### Targeting databases, ACRs, and other resource types
74
+
75
+ Tenable doesn't have a dedicated field for resource class (database, container registry, etc.). The recommended approaches:
76
+
77
+ **Option 1 — Tenable tags (most reliable):** In the Tenable UI, create a tag category `AssetType` and assign values like `database`, `acr`, `aks`, `rds`, `cosmos_db` to assets. These appear immediately in `tenable_assets_by_tag_total{tag_category="assettype"}`.
78
+
79
+ **Option 2 — Cloud-native resource tags:** Enable `include_resource_tags=True` (already on). Any AWS tag, Azure tag, or GCP label on the resource appears as a `tenable_assets_by_tag_total` time series. For example, an Azure ACR tagged `{"resource_type": "container_registry"}` surfaces as `tag_category="resource_type", tag_value="container_registry"`.
80
+
81
+ **Option 3 — Plugin family:** Database vulnerabilities land in the `Databases` plugin family — visible in `tenable_vulnerabilities_by_plugin_family_total{plugin_family="databases"}`.
82
+
83
+ **Option 4 — Discovery source filter:** Scope the exporter to specific sources via `TENABLE_FILTER_PROVIDERS`. For ACR-specific scanning, Tenable uses the `AZURE` source; container-specific findings come from the `Containers` plugin family.
84
+
85
+ ## Quick start
86
+
87
+ ### pip
88
+
89
+ ```bash
90
+ pip install tenable-exporter
91
+ export TENABLE_ACCESS_KEY=your_access_key
92
+ export TENABLE_SECRET_KEY=your_secret_key
93
+
94
+ tenable-exporter
95
+ ```
96
+
97
+ Metrics will be available at `http://localhost:9190/metrics`.
98
+
99
+ ### Docker
100
+
101
+ ```bash
102
+ docker run -p 9190:9190 \
103
+ -e TENABLE_ACCESS_KEY=your_access_key \
104
+ -e TENABLE_SECRET_KEY=your_secret_key \
105
+ ghcr.io/polarpoint-io/tenable-exporter:latest
106
+ ```
107
+
108
+ ### Docker Compose
109
+
110
+ ```bash
111
+ cp .env.example .env
112
+ # Fill in your Tenable credentials in .env
113
+ docker compose up -d
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ | Environment variable | Default | Description |
119
+ |---|---|---|
120
+ | `TENABLE_ACCESS_KEY` | **required** | Tenable.io API access key |
121
+ | `TENABLE_SECRET_KEY` | **required** | Tenable.io API secret key |
122
+ | `EXPORTER_PORT` | `9190` | Port to expose metrics on |
123
+ | `SCRAPE_INTERVAL` | `300` | Seconds between Tenable API scrapes |
124
+ | `TENABLE_FILTER_PROVIDERS` | _(all)_ | Comma-separated providers to include: `aws`, `azure`, `gcp` |
125
+ | `TENABLE_FILTER_SUBSCRIPTIONS` | _(all)_ | Comma-separated subscription IDs to include (AWS account IDs, Azure subscription UUIDs, or GCP project IDs) |
126
+
127
+ ## Docker image tags
128
+
129
+ | Tag | When pushed |
130
+ |---|---|
131
+ | `latest` | Every merge to `main` |
132
+ | `sha-<short>` | Every merge to `main` |
133
+ | `1.2.3` / `1.2` | On a semantic-release version bump |
134
+
135
+ ## Required GitHub secrets
136
+
137
+ Add these at **GitHub repo → Settings → Secrets and variables → Actions**:
138
+
139
+ | Secret | Description |
140
+ |---|---|
141
+ | `POL_GH_TOKEN` | Personal access token with `repo` + `write:packages` scope |
142
+ | `PYPI_TOKEN` | PyPI API token for the `tenable-exporter` project |
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ git clone https://github.com/polarpoint-io/tenable-exporter.git
148
+ cd tenable-exporter
149
+ pip install -e ".[dev]"
150
+
151
+ export TENABLE_ACCESS_KEY=...
152
+ export TENABLE_SECRET_KEY=...
153
+
154
+ tenable-exporter
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tenable-exporter"
7
+ version = "1.1.0"
8
+ description = "Prometheus exporter for Tenable.io metrics"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Surj Bains" }]
13
+ keywords = ["prometheus", "tenable", "exporter", "security", "metrics", "devsecops"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Information Technology",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Security",
22
+ "Topic :: System :: Monitoring",
23
+ ]
24
+ dependencies = [
25
+ "pytenable>=1.6.0",
26
+ "prometheus-client>=0.20.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=8.0",
32
+ "pytest-mock>=3.0",
33
+ "ruff>=0.4",
34
+ ]
35
+
36
+ [project.scripts]
37
+ tenable-exporter = "exporter:main"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["."]
41
+ include = ["exporter*"]
42
+
43
+ [tool.pytest.ini_options]
44
+ pythonpath = ["."]
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py311"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: tenable-exporter
3
+ Version: 1.1.0
4
+ Summary: Prometheus exporter for Tenable.io metrics
5
+ Author: Surj Bains
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Surj Bains
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: prometheus,tenable,exporter,security,metrics,devsecops
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Information Technology
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Topic :: Security
36
+ Classifier: Topic :: System :: Monitoring
37
+ Requires-Python: >=3.11
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: pytenable>=1.6.0
41
+ Requires-Dist: prometheus-client>=0.20.0
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8.0; extra == "dev"
44
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.4; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # tenable-exporter
49
+
50
+ ![tenable-exporter banner](assets/banner.png)
51
+
52
+ [![CI](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/ci.yml)
53
+ [![CodeQL](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/polarpoint-io/tenable-exporter/actions/workflows/codeql-analysis.yml)
54
+ [![PyPI](https://img.shields.io/pypi/v/tenable-exporter?logo=pypi&logoColor=white)](https://pypi.org/project/tenable-exporter/)
55
+ [![Python](https://img.shields.io/pypi/pyversions/tenable-exporter?logo=python&logoColor=white)](https://pypi.org/project/tenable-exporter/)
56
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
57
+ [![GHCR](https://img.shields.io/badge/ghcr.io-tenable--exporter-blue?logo=github)](https://github.com/polarpoint-io/tenable-exporter/pkgs/container/tenable-exporter)
58
+
59
+ A Prometheus exporter for [Tenable.io](https://www.tenable.com/) built with [pyTenable](https://github.com/tenable/pyTenable).
60
+
61
+ Exports vulnerability, asset, and scan metrics so you can alert on them in Grafana or any Prometheus-compatible stack.
62
+
63
+ > **PyPI**: `pip install tenable-exporter` &nbsp;·&nbsp; **Image**: `ghcr.io/polarpoint-io/tenable-exporter:latest` &nbsp;·&nbsp; **Repo**: <https://github.com/polarpoint-io/tenable-exporter>
64
+
65
+ ## Metrics
66
+
67
+ ### Vulnerabilities
68
+
69
+ | Metric | Labels | Description |
70
+ |---|---|---|
71
+ | `tenable_vulnerabilities_total` | `severity` | Total vulnerabilities by severity |
72
+ | `tenable_vulnerabilities_by_subscription_total` | `provider`, `subscription_id`, `severity` | Vulns per cloud provider and subscription |
73
+ | `tenable_vulnerabilities_by_region_total` | `provider`, `subscription_id`, `region`, `severity` | Vulns per subscription and region |
74
+ | `tenable_vulnerabilities_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `severity` | Vulns per Azure resource group |
75
+ | `tenable_vulnerabilities_by_resource_total` | `provider`, `subscription_id`, `resource_id`, `severity` | Vulns per individual cloud resource |
76
+ | `tenable_vulnerabilities_by_plugin_family_total` | `plugin_family`, `severity` | Vulns by Tenable plugin family and severity |
77
+ | `tenable_vulnerabilities_by_subscription_plugin_total` | `provider`, `subscription_id`, `region`, `plugin_family`, `severity` | Cross-dimension vuln count |
78
+ | `tenable_vulnerabilities_by_state_total` | `provider`, `subscription_id`, `state`, `severity` | Vulns by lifecycle state (`OPEN`, `REOPENED`, `FIXED`) — use `FIXED` to track remediation velocity |
79
+ | `tenable_vulnerabilities_by_exploit_risk_total` | `cve_category`, `severity` | Vulns by Tenable CVE category: `cisa known exploitable`, `ransomware`, `emerging threats`, `persistently exploited`, `top 50 vpr`, `recent active exploitation`, `in the news` |
80
+ | `tenable_vulnerabilities_by_vpr_band_total` | `provider`, `subscription_id`, `vpr_band` | Vulns by VPR (Vulnerability Priority Rating) band: `critical` (9–10), `high` (7–8.9), `medium` (4–6.9), `low` (<4) |
81
+
82
+ ### Assets
83
+
84
+ | Metric | Labels | Description |
85
+ |---|---|---|
86
+ | `tenable_assets_by_subscription_total` | `provider`, `subscription_id` | Asset count per cloud provider and subscription |
87
+ | `tenable_assets_by_region_total` | `provider`, `subscription_id`, `region` | Asset count per subscription and region |
88
+ | `tenable_assets_by_resource_group_total` | `provider`, `subscription_id`, `resource_group` | Asset count per Azure resource group |
89
+ | `tenable_assets_by_resource_type_total` | `provider`, `subscription_id`, `region`, `resource_type` | Asset count by resource type (e.g. `t3.medium`, `Standard_D2s_v3`) |
90
+ | `tenable_assets_by_source_total` | `source` | Assets by Tenable discovery source (`AWS`, `AZURE`, `GCP`, `NESSUS`, `WAS`, …) |
91
+ | `tenable_assets_by_tag_total` | `tag_category`, `tag_value` | Assets by Tenable tag or cloud-native resource tag. Use `tag_category=asset_type` with values like `database`, `container_registry`, `acr`, `aks`, `rds` to track specific resource classes |
92
+
93
+ ### Compliance
94
+
95
+ | Metric | Labels | Description |
96
+ |---|---|---|
97
+ | `tenable_compliance_findings_total` | `provider`, `subscription_id`, `audit_name`, `result` | CIS/DISA STIG compliance findings by audit and result (`PASSED`, `FAILED`, `WARNING`, `SKIPPED`) |
98
+ | `tenable_compliance_findings_by_region_total` | `provider`, `subscription_id`, `region`, `result` | Compliance findings per region |
99
+ | `tenable_compliance_findings_by_resource_group_total` | `provider`, `subscription_id`, `resource_group`, `result` | Compliance findings per Azure resource group |
100
+
101
+ ### Scans & System
102
+
103
+ | Metric | Labels | Description |
104
+ |---|---|---|
105
+ | `tenable_scans_total` | — | Total number of scans |
106
+ | `tenable_scans_by_status_total` | `status` | Scans by status (running, completed, aborted, …) |
107
+ | `tenable_plugin_set_updated_timestamp` | — | Unix timestamp of the last plugin set update |
108
+
109
+ ### Label values by cloud provider
110
+
111
+ | Label | AWS | Azure | GCP |
112
+ |---|---|---|---|
113
+ | `provider` | `aws` | `azure` | `gcp` |
114
+ | `subscription_id` | Account ID | Subscription UUID | Project ID |
115
+ | `region` | e.g. `us-east-1` | Azure location | GCP zone |
116
+ | `resource_group` | `unknown` | Resource group name | `unknown` |
117
+ | `resource_id` | EC2 instance ID | Azure resource / VM ID | GCP instance ID |
118
+ | `resource_type` | EC2 instance type | VM size | Machine type |
119
+
120
+ ### Targeting databases, ACRs, and other resource types
121
+
122
+ Tenable doesn't have a dedicated field for resource class (database, container registry, etc.). The recommended approaches:
123
+
124
+ **Option 1 — Tenable tags (most reliable):** In the Tenable UI, create a tag category `AssetType` and assign values like `database`, `acr`, `aks`, `rds`, `cosmos_db` to assets. These appear immediately in `tenable_assets_by_tag_total{tag_category="assettype"}`.
125
+
126
+ **Option 2 — Cloud-native resource tags:** Enable `include_resource_tags=True` (already on). Any AWS tag, Azure tag, or GCP label on the resource appears as a `tenable_assets_by_tag_total` time series. For example, an Azure ACR tagged `{"resource_type": "container_registry"}` surfaces as `tag_category="resource_type", tag_value="container_registry"`.
127
+
128
+ **Option 3 — Plugin family:** Database vulnerabilities land in the `Databases` plugin family — visible in `tenable_vulnerabilities_by_plugin_family_total{plugin_family="databases"}`.
129
+
130
+ **Option 4 — Discovery source filter:** Scope the exporter to specific sources via `TENABLE_FILTER_PROVIDERS`. For ACR-specific scanning, Tenable uses the `AZURE` source; container-specific findings come from the `Containers` plugin family.
131
+
132
+ ## Quick start
133
+
134
+ ### pip
135
+
136
+ ```bash
137
+ pip install tenable-exporter
138
+ export TENABLE_ACCESS_KEY=your_access_key
139
+ export TENABLE_SECRET_KEY=your_secret_key
140
+
141
+ tenable-exporter
142
+ ```
143
+
144
+ Metrics will be available at `http://localhost:9190/metrics`.
145
+
146
+ ### Docker
147
+
148
+ ```bash
149
+ docker run -p 9190:9190 \
150
+ -e TENABLE_ACCESS_KEY=your_access_key \
151
+ -e TENABLE_SECRET_KEY=your_secret_key \
152
+ ghcr.io/polarpoint-io/tenable-exporter:latest
153
+ ```
154
+
155
+ ### Docker Compose
156
+
157
+ ```bash
158
+ cp .env.example .env
159
+ # Fill in your Tenable credentials in .env
160
+ docker compose up -d
161
+ ```
162
+
163
+ ## Configuration
164
+
165
+ | Environment variable | Default | Description |
166
+ |---|---|---|
167
+ | `TENABLE_ACCESS_KEY` | **required** | Tenable.io API access key |
168
+ | `TENABLE_SECRET_KEY` | **required** | Tenable.io API secret key |
169
+ | `EXPORTER_PORT` | `9190` | Port to expose metrics on |
170
+ | `SCRAPE_INTERVAL` | `300` | Seconds between Tenable API scrapes |
171
+ | `TENABLE_FILTER_PROVIDERS` | _(all)_ | Comma-separated providers to include: `aws`, `azure`, `gcp` |
172
+ | `TENABLE_FILTER_SUBSCRIPTIONS` | _(all)_ | Comma-separated subscription IDs to include (AWS account IDs, Azure subscription UUIDs, or GCP project IDs) |
173
+
174
+ ## Docker image tags
175
+
176
+ | Tag | When pushed |
177
+ |---|---|
178
+ | `latest` | Every merge to `main` |
179
+ | `sha-<short>` | Every merge to `main` |
180
+ | `1.2.3` / `1.2` | On a semantic-release version bump |
181
+
182
+ ## Required GitHub secrets
183
+
184
+ Add these at **GitHub repo → Settings → Secrets and variables → Actions**:
185
+
186
+ | Secret | Description |
187
+ |---|---|
188
+ | `POL_GH_TOKEN` | Personal access token with `repo` + `write:packages` scope |
189
+ | `PYPI_TOKEN` | PyPI API token for the `tenable-exporter` project |
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ git clone https://github.com/polarpoint-io/tenable-exporter.git
195
+ cd tenable-exporter
196
+ pip install -e ".[dev]"
197
+
198
+ export TENABLE_ACCESS_KEY=...
199
+ export TENABLE_SECRET_KEY=...
200
+
201
+ tenable-exporter
202
+ ```
203
+
204
+ ## License
205
+
206
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tenable_exporter.egg-info/PKG-INFO
5
+ tenable_exporter.egg-info/SOURCES.txt
6
+ tenable_exporter.egg-info/dependency_links.txt
7
+ tenable_exporter.egg-info/entry_points.txt
8
+ tenable_exporter.egg-info/requires.txt
9
+ tenable_exporter.egg-info/top_level.txt
10
+ tests/test_exporter.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tenable-exporter = exporter:main
@@ -0,0 +1,7 @@
1
+ pytenable>=1.6.0
2
+ prometheus-client>=0.20.0
3
+
4
+ [dev]
5
+ pytest>=8.0
6
+ pytest-mock>=3.0
7
+ ruff>=0.4
@@ -0,0 +1,171 @@
1
+ """Unit tests for tenable-exporter."""
2
+
3
+ import pytest
4
+ from exporter import (
5
+ AssetCloud,
6
+ cloud_from_asset,
7
+ _str,
8
+ _vpr_band,
9
+ TenableCollector,
10
+ UNKNOWN,
11
+ )
12
+
13
+
14
+ # ── _str ──────────────────────────────────────────────────────────────────────
15
+
16
+ def test_str_none():
17
+ assert _str(None) == UNKNOWN
18
+
19
+
20
+ def test_str_empty():
21
+ assert _str("") == UNKNOWN
22
+
23
+
24
+ def test_str_whitespace():
25
+ assert _str(" ") == UNKNOWN
26
+
27
+
28
+ def test_str_value():
29
+ assert _str("us-east-1") == "us-east-1"
30
+
31
+
32
+ def test_str_strips():
33
+ assert _str(" hello ") == "hello"
34
+
35
+
36
+ # ── _vpr_band ─────────────────────────────────────────────────────────────────
37
+
38
+ @pytest.mark.parametrize("score,expected", [
39
+ (10.0, "critical"),
40
+ (9.0, "critical"),
41
+ (8.9, "high"),
42
+ (7.0, "high"),
43
+ (6.9, "medium"),
44
+ (4.0, "medium"),
45
+ (3.9, "low"),
46
+ (0.0, "low"),
47
+ (None, UNKNOWN),
48
+ ])
49
+ def test_vpr_band(score, expected):
50
+ assert _vpr_band(score) == expected
51
+
52
+
53
+ # ── cloud_from_asset ──────────────────────────────────────────────────────────
54
+
55
+ def test_cloud_from_asset_aws():
56
+ asset = {
57
+ "id": "abc",
58
+ "aws_account_id": "123456789012",
59
+ "aws_region": "us-east-1",
60
+ "aws_ec2_instance_id": "i-0abc123",
61
+ "aws_ec2_instance_type": "t3.medium",
62
+ "aws_vpc_id": "vpc-001",
63
+ "network_name": "default",
64
+ }
65
+ ctx = cloud_from_asset(asset)
66
+ assert ctx.provider == "aws"
67
+ assert ctx.subscription_id == "123456789012"
68
+ assert ctx.region == "us-east-1"
69
+ assert ctx.resource_id == "i-0abc123"
70
+ assert ctx.resource_type == "t3.medium"
71
+ assert ctx.vpc_id == "vpc-001"
72
+ assert ctx.resource_group == UNKNOWN
73
+
74
+
75
+ def test_cloud_from_asset_azure():
76
+ asset = {
77
+ "id": "def",
78
+ "azure_subscription_id": "aaaa-bbbb-cccc",
79
+ "azure_location": "eastus",
80
+ "azure_resource_group": "my-rg",
81
+ "azure_resource_id": "/subscriptions/aaaa/resourceGroups/my-rg/providers/vm",
82
+ "azure_vm_size": "Standard_D2s_v3",
83
+ "azure_virtual_network": "my-vnet",
84
+ }
85
+ ctx = cloud_from_asset(asset)
86
+ assert ctx.provider == "azure"
87
+ assert ctx.subscription_id == "aaaa-bbbb-cccc"
88
+ assert ctx.region == "eastus"
89
+ assert ctx.resource_group == "my-rg"
90
+ assert ctx.resource_type == "Standard_D2s_v3"
91
+ assert ctx.vpc_id == "my-vnet"
92
+
93
+
94
+ def test_cloud_from_asset_gcp():
95
+ asset = {
96
+ "id": "ghi",
97
+ "gcp_project_id": "my-project",
98
+ "gcp_zone": "us-central1-a",
99
+ "gcp_instance_id": "1234567890",
100
+ "gcp_machine_type": "n2-standard-4",
101
+ "gcp_network": "default",
102
+ }
103
+ ctx = cloud_from_asset(asset)
104
+ assert ctx.provider == "gcp"
105
+ assert ctx.subscription_id == "my-project"
106
+ assert ctx.region == "us-central1-a"
107
+ assert ctx.resource_id == "1234567890"
108
+ assert ctx.resource_type == "n2-standard-4"
109
+ assert ctx.vpc_id == "default"
110
+ assert ctx.resource_group == UNKNOWN
111
+
112
+
113
+ def test_cloud_from_asset_unknown():
114
+ asset = {"id": "xyz", "sources": [{"name": "NESSUS"}]}
115
+ ctx = cloud_from_asset(asset)
116
+ assert ctx.provider == "nessus"
117
+ assert ctx.subscription_id == UNKNOWN
118
+ assert ctx.region == UNKNOWN
119
+
120
+
121
+ def test_cloud_from_asset_empty():
122
+ ctx = cloud_from_asset({})
123
+ assert ctx.provider == UNKNOWN
124
+ assert ctx.subscription_id == UNKNOWN
125
+
126
+
127
+ # ── TenableCollector._include ─────────────────────────────────────────────────
128
+
129
+ def _collector(providers=None, subscriptions=None):
130
+ """Return a TenableCollector with a stub TIO (not used in these tests)."""
131
+ return TenableCollector(
132
+ tio=None,
133
+ filter_providers=providers or set(),
134
+ filter_subscriptions=subscriptions or set(),
135
+ )
136
+
137
+
138
+ def test_include_no_filters():
139
+ c = _collector()
140
+ ctx = AssetCloud(provider="aws", subscription_id="123")
141
+ assert c._include(ctx) is True
142
+
143
+
144
+ def test_include_provider_match():
145
+ c = _collector(providers={"aws"})
146
+ assert c._include(AssetCloud(provider="aws", subscription_id="x")) is True
147
+
148
+
149
+ def test_include_provider_no_match():
150
+ c = _collector(providers={"azure"})
151
+ assert c._include(AssetCloud(provider="aws", subscription_id="x")) is False
152
+
153
+
154
+ def test_include_subscription_match():
155
+ c = _collector(subscriptions={"123"})
156
+ assert c._include(AssetCloud(provider="aws", subscription_id="123")) is True
157
+
158
+
159
+ def test_include_subscription_no_match():
160
+ c = _collector(subscriptions={"999"})
161
+ assert c._include(AssetCloud(provider="aws", subscription_id="123")) is False
162
+
163
+
164
+ def test_include_both_filters_pass():
165
+ c = _collector(providers={"aws"}, subscriptions={"123"})
166
+ assert c._include(AssetCloud(provider="aws", subscription_id="123")) is True
167
+
168
+
169
+ def test_include_both_filters_provider_fail():
170
+ c = _collector(providers={"azure"}, subscriptions={"123"})
171
+ assert c._include(AssetCloud(provider="aws", subscription_id="123")) is False