Tenable Hardening Dashboard: System Guide
01Purpose and panel order
The dashboard presents monthly hardening and compliance results in a deliberate order:
- Monthly compliance trend
- Hosts, latest compliance, and latest critical findings
- High-risk non-compliant items, always one row per host and control
- Host hardening items, showing every latest compliant and non-compliant result
- Supporting host summary and compliance-by-host chart
Two dashboard tokens drive the filtering: $os_filter$ (selected operating system, or *)
and $host_filter$ (selected hostname, or *). The time window runs
-13mon@mon through now.
02Required data model
The searches read indexed CSV fields via tstats. These exact field names must be preserved when the
synthetic data is replaced with a live feed.
tenable:hardening — one host summary per scan
| Field | Meaning | Example |
|---|---|---|
| scan_date | Scan date, used as _time | 2026-07-15 |
| scan_id | Unique scan identifier | monthly-cis-202607 |
| hostname | Normalised host name | rhel8-web01 |
| ip_address | Host IP at scan time | 10.20.5.14 |
| operating_system | Normalised OS label | RHEL 8 |
| compliance_pct | Passed checks / total checks × 100 | 91.4 |
| critical_findings | Critical failed checks | 2 |
| high_findings | High failed checks | 7 |
| medium_findings | Medium failed checks | 14 |
| passed_checks | Passed audit checks | 137 |
| failed_checks | Failed audit checks | 23 |
| scan_policy | Audit or policy name | CIS RHEL 8 Level 1 |
| data_source | Source system | Tenable VM |
tenable:highrisk — one host/control result per scan
| Field | Meaning | Example |
|---|---|---|
| scan_date | Scan date, used as _time | 2026-07-15 |
| scan_id | Unique scan identifier | monthly-cis-202607 |
| hostname | Normalised host name | rhel8-web01 |
| ip_address | Host IP | 10.20.5.14 |
| operating_system | Normalised OS label | RHEL 8 |
| control_id | Stable audit/plugin/control ID | LNX-PRIV-001 |
| control | Human-readable check title | /bin/bash ownership and permissions |
| severity | Critical or High | Critical |
| status | Exactly Compliant or Non-compliant | Non-compliant |
| expected_setting | Secure target configuration | root:root with mode 0755 |
| risk_reason | Why failure could enable administrative access | Writable shell may allow root code execution |
| data_source | Source system | Tenable VM |
Operating-system values are normalised to: Windows Server 2016, Windows Server 2019,
Windows Server 2022, RHEL 7, RHEL 8, RHEL 9 and
OEL 8.
03Inputs and timestamp extraction
Input configuration lives in tenable_hardening_app/default/inputs.conf, with field extraction in
props.conf:
[tenable:hardening]
INDEXED_EXTRACTIONS = csv
HEADER_FIELD_LINE_NUMBER = 1
TIMESTAMP_FIELDS = scan_date
TIME_FORMAT = %Y-%m-%d
TZ = Australia/Adelaide
SHOULD_LINEMERGE = false
[tenable:highrisk]
INDEXED_EXTRACTIONS = csv
HEADER_FIELD_LINE_NUMBER = 1
TIMESTAMP_FIELDS = scan_date
TIME_FORMAT = %Y-%m-%d
TZ = Australia/Adelaide
SHOULD_LINEMERGE = false
In production, write normalised events to the monitored CSV paths, send them through the Splunk HTTP Event Collector with the same sourcetypes, or adapt the searches to the sourcetypes created by the official Tenable Add-on for Splunk.
04The SPL behind each panel
Operating-system selector
| tstats count where index=main sourcetype="tenable:hardening"
by operating_system
| sort operating_system
Host selector
| tstats count where index=main sourcetype="tenable:highrisk"
operating_system="$os_filter$"
by hostname
| sort hostname
Monthly compliance trend
| tstats avg(compliance_pct) as "Compliance %"
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
by _time span=1mon operating_system
| xyseries _time operating_system "Compliance %"
Host count
| tstats dc(hostname) as hosts
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
Latest compliance
| tstats avg(compliance_pct) as compliance
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
by _time span=1mon
| sort - _time
| head 1
| eval compliance=round(compliance,1)."%"
Latest critical findings
| tstats sum(critical_findings) as critical
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
by _time span=1mon
| sort - _time
| head 1
High-risk non-compliant items
| tstats latest(scan_date) as "Scan Date"
latest(status) as Status
latest(expected_setting) as "Expected Setting"
latest(risk_reason) as "Why This Is High Risk"
latest(severity) as Severity
where index=main sourcetype="tenable:highrisk"
operating_system="$os_filter$" hostname="$host_filter$"
by hostname operating_system control_id control
| where Status="Non-compliant"
| eval severity_order=case(Severity="Critical",1,Severity="High",2,true(),3)
| sort severity_order operating_system hostname control
| fields - severity_order
| rename hostname as Host operating_system as "Operating System"
control_id as "Control ID" control as "High-risk Item"
Host hardening items, latest result per item
| tstats latest(scan_date) as "Scan Date"
latest(status) as "Scan Result"
latest(severity) as Severity
latest(expected_setting) as "Expected Setting"
latest(risk_reason) as Risk
where index=main sourcetype="tenable:highrisk"
operating_system="$os_filter$" hostname="$host_filter$"
by hostname operating_system control_id control
| eval result_order=case('Scan Result'="Non-compliant",1,true(),2),
severity_order=case(Severity="Critical",1,Severity="High",2,true(),3)
| sort result_order severity_order hostname control
| fields - result_order severity_order
| rename hostname as Host operating_system as "Operating System"
control_id as "Control ID" control as "Hardening Item"
Latest host summary
| tstats latest(ip_address) as "IP Address"
latest(compliance_pct) as "Compliance %"
latest(critical_findings) as Critical
latest(high_findings) as High
latest(medium_findings) as Medium
latest(passed_checks) as Passed
latest(failed_checks) as Failed
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
by hostname operating_system
| sort operating_system hostname
| rename hostname as Host operating_system as "Operating System"
Compliance by host
| tstats latest(compliance_pct) as "Compliance %"
where index=main sourcetype="tenable:hardening"
operating_system="$os_filter$"
by hostname
| sort - "Compliance %"
05Moving from synthetic to live Tenable data
- Install and configure the official Tenable Add-on for Splunk, or build an API collector using a restricted Tenable service account.
- Export asset data plus vulnerability and compliance findings. Preserve open, reopened and fixed/passed states so the latest status stays accurate.
- Convert each scan to the two schemas above. Use a stable asset UUID internally where available, even if
hostnameremains the dashboard label. - Calculate the host summary once per completed scan. Do not derive compliance from only critical and high findings; include every applicable audit check.
- Map Tenable audit results consistently:
PASSEDtoCompliant,FAILEDtoNon-compliant. Decide explicitly howWARNING,ERRORand manual-review results should appear. - Mark privilege-escalation controls as High or Critical using an approved control catalogue, not title keyword matching alone.
- Send events to a production index such as
tenable, then changeindex=mainin every search. - Validate event counts, latest scan dates, OS normalisation and one known host before releasing the dashboard to users.
- Store API keys in Splunk's encrypted credential storage or a secrets manager. Never put keys in dashboard XML, scripts or CSV files.
Tenable Vulnerability Management uses asynchronous export jobs for assets, vulnerabilities and compliance data, so the collector has to poll for job completion rather than expecting a synchronous response.
06Production validation searches
Run these after cutting over, before the dashboard is released:
| tstats count min(_time) as first max(_time) as last
where index=tenable sourcetype="tenable:hardening"
by operating_system
| convert ctime(first) ctime(last)
| tstats latest(status) as status
where index=tenable sourcetype="tenable:highrisk"
by hostname operating_system control_id control
| stats count by operating_system status
Hosts whose most recent scan is older than 35 days:
| tstats latest(scan_date) as scan_date
where index=tenable sourcetype="tenable:highrisk"
by hostname
| where strptime(scan_date,"%Y-%m-%d") < relative_time(now(),"-35d")
| rename hostname as "Host with stale scan" scan_date as "Latest scan"