
Security
Vulnerability Management Platforms: The Unit They Bill
Vulnerability management platforms meter asset identity and scan events. What that costs on a churning Kubernetes cluster, and what a Trivy pipeline costs.
Comparisons of vulnerability management platforms rank features. Procurement eventually asks something else: what does the invoice count, and how does that count behave on a cluster that replaces its nodes monthly and tags an image on every merge? The usual answer — ephemeral pods multiply into invoice lines — is wrong in an instructive way.
The unit on the invoice is not the pod
Three units appear in the vendors' own licensing and pricing pages. Tenable Vulnerability Management licenses assessed assets, matched on host identity keys. Tenable Enclave Security's Container Security licenses unique image:tag combinations. Amazon Inspector, run standalone, charges per scan event; enable AWS Security Hub and the same ECR scanning becomes a fraction of a per-resource unit. None of these pages documents a per-pod meter.
Pod churn is where the intuition comes from. Datadog's State of Containers and Serverless report — telemetry from Datadog's own customers — finds that almost two-thirds of Kubernetes containers have an uptime under ten minutes and about one-third finish running in under one minute, and it attributes most short-lived containers to Jobs, CronJobs and standalone Pods. The report does not say whether those workloads reuse an existing image. That they mostly do is engineering inference, not Datadog's measurement: an hourly CronJob runs the same image:tag every hour, creating no new identity. Identities come from build cadence — a new tag per merge — and from node rotation, a new host each time a pool is upgraded.
Traditional vulnerability management tools make many assumptions about host durability and app update mechanisms and frequencies that are fundamentally misaligned with a containerized model.
The billing models below are where that mismatch turns into money.
What a platform counts as an asset
Tenable Vulnerability Management — titled Tenable One Vulnerability Management in its current documentation — assigns licences to "assessed resources from the past 90 days, either identified on scans or imported with vulnerabilities". Its licensing guide lists containers among its examples but states no rule for matching them, so do not model running containers as TVM assets. Model hosts.
For hosts, matching is explicit. The algorithm looks at the asset's BIOS UUID, MAC address, NetBIOS name, fully qualified domain name "and more", and, in general, if a new asset does not match a previously discovered one and has been assessed, it counts towards the licence. A fresh VM normally presents a new BIOS UUID and MAC, so a replacement node that matches none of the retired node's keys can count as a new licensed host — while the retired one holds its licence until reclaimed:
- deleted assets — within 24 hours;
- assets from connectors — the day after they are terminated;
- with Asset Age Out enabled — after they go unscanned for a period you specify;
- all other assets — after 90 days without a scan.
Tenable's 2017 post on elastic licensing named this case: machines "spun up, scanned, spun down - all within a few hours - and then never seen again". The reclamation table is the design answer; the consequence is that your asset hygiene, not the vendor, decides whether a retired node leaves the count in a day or in three months.
Tenable Enclave Security's Container Security uses a different unit: "unique image:tag combinations assessed". Those are not on Vulnerability Management's 90-day clock. Current documentation instead offers an Asset Expiration setting that expires assets relative to last scan or image build time, after a maximum number of days you choose. Without a schedule it deletes matching assets immediately; Schedule Expiry deletes them on an interval. The page does not say when a deleted image stops counting against the licence. Get that in writing before you model it.
Per-scan billing puts a price on cadence
Amazon Inspector bills the same ECR scanning two ways; which applies is an account-level fact.
Standalone, without Security Hub, it is usage-priced. Its pricing page, as published in September 2026, prices each newly pushed image's initial scan at $0.09 and each re-scan at $0.01, in US East (N. Virginia) worked examples rather than a rate card; the same page puts EC2 scanning at $1.258 per instance for a full month. Under continuous scanning, Inspector re-scans an image whenever it adds a new CVE to its database that is relevant to that image. Put those together and, on this path, the vendor's database — not your pipeline — generates billable events. A quiet month for your engineers can be a busy one on the invoice.
Which images those re-scans reach depends on the monitoring window. For accounts created on or after 16 May 2025, the default is 14 days from push or last-in-use date; for accounts created before that date, it is 90 days from push or pull — unless a re-scan duration has been configured. Two accounts in one organisation can carry defaults more than six times apart.
Enable Security Hub and the arithmetic changes shape. Its pricing page says existing billing for services such as Inspector transitions to Security Hub pricing with no action required, and lists ECR container image scanning among the capabilities included in the Essentials plan's per-resource-unit price. Eighteen ECR images count as one resource unit. The images counted are those pushed each month plus those in scope for re-scanning, and charges apply only for the time each image is actually monitored. CVE volume drops out of the bill; image count and window length stay in it.
Modelling a cluster that churns
Everything here is modelled from the published rules and rates above, not measured on anyone's invoice. The inputs are assumptions; substitute your own.
Host assets under Tenable Vulnerability Management
Assume a 60-node cluster whose pools are upgraded monthly, replacing every node. If retired nodes are deleted in Tenable, or arrive through a connector that marks them terminated, they leave within a day and the licensed host count stays near 60. If they were discovered by network scan and simply stop answering, each waits out the 90-day default: at steady state, 60 live hosts plus roughly 180 retired ones — about four times the fleet. Same cluster, same scanner, a fourfold difference decided by asset hygiene; Asset Age Out shrinks it to the period you set.
Image tags under Enclave Container Security
Assume 40 services, each building a tagged image on three merges a day across 20 working days: 2,400 new tags a month. Without Asset Expiration the licensed count is cumulative — 28,800 after a year. With expiry keyed to image build time, it stays bounded near the tags built inside your chosen window, subject to the licence-release timing the documentation leaves unstated.
Inspector, standalone or under Security Hub
AWS's own US East example is instructive: 1,000 newly pushed images initially scanned at $0.09 each, and 1,500 images each rescanned an average of 15 times at $0.01 per rescan — $90 of push scans against $225 of re-scans. In the vendor's own example, the line driven by CVE arrivals is two and a half times the line driven by your pushes. Your standalone version is pushes × $0.09, plus images inside the window × relevant CVEs × $0.01. Under Security Hub Essentials it becomes images pushed plus images in re-scan scope, prorated by time monitored, divided by 18 — at a per-unit price to read from the pricing page on the day you model.
For scale, Rapid7's pricing page lists InsightVM from $1.62 per asset per month for 500 assets, as published in September 2026 — a host-platform list price, never an input to the image or Inspector arithmetic. Before modelling, gather:
- node replacements per month, and how retired nodes leave Tenable — deleted, connector-terminated, aged out or left to the default;
- unique tags assessed per month, and whether Asset Expiration is enabled and on which basis;
- for every AWS account: Security Hub on or off, the account's creation date, and any configured re-scan duration.
What the pipeline you own actually costs
The alternative is not exotic. Trivy is Apache-2.0 licensed and scans container images, filesystems, remote Git repositories, virtual machine images and Kubernetes; Grype, also Apache-2.0, scans container images, filesystems and SBOMs. NIST SP 800-190 recommends, for image vulnerabilities, tools that "take the pipeline-based build approach and immutable nature of containers and images into their design". A scanner pointed at your registry, on your schedule, is that.
apiVersion: batch/v1
kind: CronJob
metadata:
name: registry-vuln-scan
namespace: vuln-scan
spec:
schedule: "30 2 * * *" # your cadence; nothing meters it
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 21600
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
initContainers:
- name: list-images
image: gcr.io/go-containerregistry/crane/debug:v0.22.1
command: ["/busybox/sh", "-c"]
args:
- |
set -eu
crane catalog "$REGISTRY" | grep "^$PROJECT/" >/work/repos.txt
: >/work/images.txt
while read -r repo; do
crane ls --omit-digest-tags "$REGISTRY/$repo" |
sed "s|^|$REGISTRY/$repo:|" >>/work/images.txt
done </work/repos.txt
echo "images to scan: $(wc -l </work/images.txt)"
env:
- { name: REGISTRY, value: registry.example.internal }
- { name: PROJECT, value: platform }
- { name: DOCKER_CONFIG, value: /docker }
securityContext: &hardened
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: work, mountPath: /work }
- { name: registry-auth, mountPath: /docker, readOnly: true }
containers:
- name: trivy
image: aquasec/trivy:0.74.0
command: ["/bin/sh", "-c"]
args:
- |
set -eu
mkdir -p "$TMPDIR"
run="/scans/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$run"
trivy image --download-db-only --db-repository "$DB_REPO"
trivy image --download-java-db-only --java-db-repository "$JAVA_DB_REPO"
trivy version --format json >"$run/trivy-version.json"
failed=0
while read -r ref; do
out="$run/$(echo "$ref" | tr '/:' '__').json"
trivy image --image-src remote --scanners vuln --skip-db-update --skip-java-db-update --format json --output "$out" "$ref" || failed=$((failed + 1))
done </work/images.txt
echo "scan failures: $failed"
test "$failed" -eq 0
env:
- { name: TRIVY_CACHE_DIR, value: /work/cache }
- { name: TMPDIR, value: /work/tmp }
- { name: DOCKER_CONFIG, value: /docker }
- { name: DB_REPO, value: registry.example.internal/mirror/aquasec/trivy-db:2 }
- { name: JAVA_DB_REPO, value: registry.example.internal/mirror/aquasec/trivy-java-db:1 }
securityContext: *hardened
volumeMounts:
- { name: work, mountPath: /work }
- { name: registry-auth, mountPath: /docker, readOnly: true }
- { name: scans, mountPath: /scans }
volumes:
- { name: work, emptyDir: {} }
- name: registry-auth
secret:
secretName: registry-pull
items: [{ key: .dockerconfigjson, path: config.json }]
- name: scans
persistentVolumeClaim:
claimName: vuln-scan-resultsCompute runs on nodes you already operate, so one more tag costs compute, not a licence line. The cost open-source write-ups leave out is the database. By default Trivy pulls it from mirror.gcr.io/aquasec first and ghcr.io/aquasecurity second. The trivy-db README says the database is built every 6 hours, with a default update interval of 24 hours in its metadata file. In 2024 the project reported that aggregate load from Trivy users had exceeded GHCR's rate limit — which it gave at the time as 44,000 requests per minute per namespace — and scans failed with TOOMANYREQUESTS; one mitigation then cut database updates from every 6 hours to every 24. That incident is why a serious pipeline mirrors the database into a registry it runs. Trivy's self-hosting guide says to copy the images with any registry tool — it names crane, ORAS and regclient — and to keep the copies updated.
#!/usr/bin/env bash
# mirror-trivy-db.sh: copy Trivy's databases into a registry you run.
set -euo pipefail
DEST="${DEST:-registry.example.internal/mirror/aquasec}"
IMAGES="${IMAGES:-trivy-db:2 trivy-java-db:1}" # schema tags, not :latest
MAX_AGE_HOURS="${MAX_AGE_HOURS:-48}"
for image in $IMAGES; do
src=""
for upstream in mirror.gcr.io/aquasec ghcr.io/aquasecurity; do
if crane digest "$upstream/$image" >/dev/null 2>&1; then
src="$upstream/$image"
break
fi
done
if [[ -z "$src" ]]; then
echo "no upstream answered for $image; mirror unchanged" >&2
exit 1
fi
crane copy "$src" "$DEST/$image"
want="$(crane digest "$src")"
have="$(crane digest "$DEST/$image")"
if [[ "$want" != "$have" ]]; then
echo "digest mismatch for $image: upstream $want, mirror $have" >&2
exit 1
fi
done
# Pull the DB back through the mirror as a scan would, then age-check it.
TRIVY_CACHE_DIR="$(mktemp -d)"
export TRIVY_CACHE_DIR
trap 'rm -rf "$TRIVY_CACHE_DIR"' EXIT
trivy image --download-db-only --db-repository "$DEST/trivy-db:2"
updated="$(trivy version --format json | jq -r '.VulnerabilityDB.UpdatedAt')"
age_hours=$((($(date -u +%s) - $(date -u -d "$updated" +%s)) / 3600))
echo "mirror trivy-db UpdatedAt=$updated (${age_hours}h old)"
if ((age_hours > MAX_AGE_HOURS)); then
echo "mirror DB older than ${MAX_AGE_HOURS}h: scans would pass what they should fail" >&2
exit 2
fiPoint every scan at the mirror with --db-repository, and --java-db-repository for the Java index. The flags accept several values, falling back in order on transient errors, and setting them replaces the defaults: list public locations after yours for a fallback, or omit them for an air gap. The last cost is engineer time to own the mirror, the CronJob, the ignore file and the queries. It is real, it does not scale with build count, and it is what a platform purchase actually buys out.
How the pipeline you own fails
- Stale database, green results. If the mirror stops updating, scans still run, against old knowledge. Control: the age gate above, plus the
trivy-version.jsonstored with every run, tying each report to its database. - Silent coverage loss. If the listing step returns fewer repositories than exist — a permission change, a renamed project — nothing errors. Control: alert on the logged image count, not only on job failure.
- An ignore file nobody revisits. Suppressions without expiry become permanent policy. Control: expiring, reviewed suppressions, as covered in running the vulnerability management life cycle yourself.
- One scanner's blind spots. A scanner is only as good as its package cataloguing. Control: run Grype over a sample of the same digests and diff; Syft's cataloguer blind spots show where to look first.
Evidence, retention and the auditor
Tenable Vulnerability Management retains scan data for 15 months; its documentation says to export anything needed for longer to storage outside the product. Tenable's 2017 post said the same of reclaimed licences: the licence goes at 90 days, but the asset and vulnerability data "remains for fifteen months." Fifteen months covers an annual audit cycle with margin. It does not cover a question, three years on, about an image you shipped.
In a pipeline you own, retention is a storage policy you write: the JSON reports, the database metadata from each run, and the digests they refer to, kept as long as your policy says. Reporting on top of that is arithmetic, not a product:
#!/usr/bin/env bash
# findings.sh: Trivy JSON reports -> one DuckDB table -> two answers.
set -euo pipefail
REPORTS="${1:-/scans}"
DB="${2:-findings.duckdb}"
ROWS="$(mktemp --suffix=.ndjson)"
trap 'rm -f "$ROWS"' EXIT
# One row per scan, image, vulnerability and package.
# shellcheck disable=SC2016
find "$REPORTS" -name '*.json' ! -name 'trivy-version.json' -print0 |
xargs -0 -r jq -c '
.CreatedAt as $at | .ArtifactName as $image
| ((.Metadata.RepoDigests // [])[0] // "unknown") as $digest
| .Results[]? | .Vulnerabilities[]?
| {scanned_at: $at, image: $image, digest: $digest,
id: .VulnerabilityID, pkg: .PkgName, severity: .Severity, status: .Status}
' >"$ROWS"
[[ -s "$ROWS" ]] || { echo "no findings in $REPORTS"; exit 0; }
duckdb "$DB" <<SQL
CREATE OR REPLACE TABLE findings AS
SELECT * REPLACE (CAST(scanned_at AS TIMESTAMPTZ) AS scanned_at)
FROM read_json_auto('$ROWS', format = 'newline_delimited');
-- 1. Images carrying fixable criticals, as of each image's latest scan.
WITH latest AS (SELECT image, max(scanned_at) AS at FROM findings GROUP BY image)
SELECT f.image, count(DISTINCT f.id) AS fixable_criticals
FROM findings f JOIN latest l ON f.image = l.image AND f.scanned_at = l.at
WHERE f.severity = 'CRITICAL' AND f.status = 'fixed'
GROUP BY f.image ORDER BY fixable_criticals DESC;
-- 2. Trend: distinct critical and high findings per day.
SELECT date_trunc('day', scanned_at) AS day, severity,
count(DISTINCT (digest, id, pkg)) AS findings
FROM findings WHERE severity IN ('CRITICAL', 'HIGH')
GROUP BY ALL ORDER BY day, severity;
SQLThose two queries are most of what a dashboard shows. What a platform adds — asset correlation, workflow, SLA tracking — is worth paying for, but price it as that, not as the scanner. Evidence held on your own storage also shortens the third-party risk conversation: one fewer vendor holds the record of your exposure.
The exit ramp runs both ways
- If you buy: schedule exports from day one — the 15-month ceiling means you need them anyway — into a format your own tooling reads, with image digests intact.
- If you build: keep reports in formats other tools accept. Trivy writes JSON, SARIF, CycloneDX and SPDX, so a later platform can ingest your history instead of starting blind.
- Either way: run both in parallel for one renewal cycle against the same digests, and compare finding sets before cancelling anything.
Make either choice reversible at signing, not at renewal. The registry is the other half of the ramp: when images, the scanner database and the reports all sit in a registry you operate, changing scanners is a configuration change. And cost the build side the way a five-year TCO model costs hardware: loaded labour included, nothing assumed free.
The long game
Over ten years, ask which cost curve tracks your growth. Build cadence grows with teams; node rotation grows with fleet size. Per-identity metering grows with both. A pipeline's cost grows with the people who run it — a curve you set, not one a pricing page sets.
The deeper question is capability: when the contract ends, can your team still scan, triage and prove what it runs? Buying a platform can be the right answer. Buying one so completely that nobody in-house can answer that is the lock-in no invoice line shows.
§FAQ/Common questions
Frequently asked
What do vulnerability management platforms actually charge for?
It depends on the product. Tenable Vulnerability Management licenses assessed assets matched on host identity keys such as BIOS UUID, MAC address, NetBIOS name and FQDN. Tenable Enclave Security's Container Security licenses unique image:tag combinations. Standalone Amazon Inspector charges per ECR scan event — $0.09 per initial push scan and $0.01 per re-scan in its US East examples as published in September 2026 — while under AWS Security Hub Essentials, 18 ECR images count as one resource unit. None documents a per-pod charge.
Does Kubernetes pod churn increase vulnerability management licensing costs?
Not through the units these vendors document. Datadog's telemetry places most short-lived Kubernetes containers in Jobs, CronJobs and standalone Pods, which typically re-run an existing image — an engineering inference, not Datadog's finding. What moves the count is build cadence (new image:tags) and node rotation (new host identities), plus, on standalone Inspector only, re-scans triggered when Inspector adds a relevant CVE to its database.
Is a self-hosted Trivy scanning pipeline free?
No. Trivy and Grype are Apache-2.0 licensed, so there is no per-image licence, but the pipeline carries scanner compute, results storage, a mirror of the vulnerability database in a registry you run, and engineer time to keep it honest. The database mirror matters: in 2024 the Trivy project reported scans failing with TOOMANYREQUESTS when aggregate user load exceeded GHCR's rate limit.
How long does Tenable keep vulnerability scan data?
Tenable Vulnerability Management retains scan data for 15 months, and its documentation says to export anything needed for longer to storage outside the product. A self-hosted pipeline keeps reports for as long as your own retention policy specifies.
Does enabling AWS Security Hub change Amazon Inspector ECR scanning costs?
Yes. With Security Hub enabled, existing Inspector billing transitions to Security Hub pricing, and ECR image scanning is included in the Essentials plan's per-resource-unit price. Eighteen images count as one resource unit, counting images pushed plus images in re-scan scope, charged for the time each is monitored. Re-scans stop being a separate usage charge; image count and monitoring window still drive the bill.
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.