In 2025, the CVE ecosystem published 45,777 vulnerabilities — an average of 130.4 per day. That same year, OWASP elevated Software Supply Chain Failures to the third position in its Top 10. Meanwhile, NIST effectively stopped enriching CVEs in the National Vulnerability Database, leaving security teams without severity scores, patching status, and critical descriptions for thousands of entries.

If your vulnerability management program still relies on quarterly scans and spreadsheets, you are already compromised — you just don’t know it yet.

This article lays out a practical path from zero to a mature vulnerability management program: where to start, what to prioritize, and why every step that can be automated must be automated.

The Scale of the Problem

The numbers paint a clear picture:

MetricValue
CVEs published in 202545,777
Average CVEs per day~130
Open-source malware detection increase (2025 vs 2024)+73%
Secrets exposed in repositories (YoY growth)+11%
NVD enrichment status (since Feb 2024)Severely degraded

The NIST NVD enrichment gap is particularly damaging. When CVEs lack severity scores, affected product lists, and remediation guidance, every security team is forced to do its own triage from scratch. The organizations with the resources to do this survive. The rest accumulate risk silently.

Step 1: Asset Inventory — You Can’t Protect What You Don’t Know

Before you scan anything, you need to answer a deceptively simple question: what do we actually have?

Most organizations cannot produce a complete, accurate inventory of their assets. Shadow IT, forgotten development environments, legacy systems that “someone” still maintains, containers spun up in CI pipelines that never got decommissioned — all of these are attack surface that exists outside your vulnerability management program.

A functional asset inventory must cover:

  • Infrastructure: servers, VMs, cloud instances (EC2, GCE, Azure VMs), network devices
  • Containers and orchestration: running container images, Kubernetes clusters, registries
  • Applications: internal and external, including their dependencies
  • Code repositories: every repo is a potential source of vulnerable dependencies
  • Third-party services: SaaS integrations, APIs, managed services
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Example: discover running containers and their images across Docker hosts
docker ps --format '{{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Status}}' | \
  sort -k2 | column -t

# Example: list all EC2 instances across all regions
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "=== $region ==="
  aws ec2 describe-instances --region "$region" \
    --query 'Reservations[].Instances[].[InstanceId,State.Name,Tags[?Key==`Name`].Value|[0]]' \
    --output table
done
Asset inventory is not a one-time project. Environments change daily. Automate discovery with tools like AWS Config, cloud-native asset inventories, or open-source solutions like NetBox and CloudQuery. If your inventory is a spreadsheet updated quarterly, it is fiction.

Step 2: Vulnerability Scanning — Covering Every Layer

Once you know what you have, you can start scanning it. The key is coverage across all layers of your stack.

Infrastructure Scanning

Traditional infrastructure scanners assess hosts, network services, and OS-level packages:

ToolTypeStrengths
NessusCommercialExtensive plugin library, compliance checks, agent-based and agentless
OpenVAS/GreenboneOpen-sourceFull-featured, community-maintained feed, self-hosted
Qualys VMDRCommercial/SaaSCloud-native, asset discovery built in, risk-based prioritization
1
2
3
4
5
6
7
8
9
# OpenVAS: run a scan via the gvm-cli
gvm-cli --gmp-username admin --gmp-password secret socket \
  --socketpath /run/gvmd/gvmd.sock \
  --xml '<create_task>
    <name>Weekly Infrastructure Scan</name>
    <config id="daba56c8-73ec-11df-a475-002264764cea"/>
    <target id="your-target-id"/>
    <scanner id="08b69003-5fc2-4037-a479-93b440211c73"/>
  </create_task>'

Container Image Scanning

Container images introduce a distinct attack surface — vulnerable base images, outdated packages, and embedded secrets. Scan images before they reach production, and continuously scan running images in registries.

1
2
3
4
5
# Trivy: scan a container image for vulnerabilities
trivy image --severity HIGH,CRITICAL --format table nginx:1.27

# Grype: scan with SBOM-aware analysis
grype nginx:1.27 --only-fixed --output table

Both Trivy and Grype integrate natively with CI/CD pipelines and container registries. The goal is clear: no image reaches production without being scanned.

Code and Dependency Scanning

Your application code and its dependencies are a major vulnerability vector. Two categories of tools cover this:

  • SCA (Software Composition Analysis): scans third-party dependencies for known vulnerabilities — tools like Dependabot, Snyk, Trivy (filesystem mode), and Grype
  • SAST (Static Application Security Testing): analyzes your source code for security flaws — tools like Semgrep, SonarQube, and CodeQL
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# GitHub Actions: SCA + container scan in CI pipeline
name: Security Scan
on: [push, pull_request]

jobs:
  vulnerability-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Dependency scanning (SCA)
      - name: Run Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'  # Fail the pipeline on findings

      # Container image scanning
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan container image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'

Step 3: Prioritization — Not All CVEs Are Equal

Here is where most programs fail. Scanning produces hundreds or thousands of findings. Treating them all equally guarantees that your team burns out fixing low-risk issues while critical exploitable vulnerabilities sit in the backlog.

Why CVSS Alone Is Not Enough

CVSS (Common Vulnerability Scoring System) tells you about the theoretical severity of a vulnerability. It does not tell you:

  • Whether it is being actively exploited in the wild
  • Whether the vulnerable code path is reachable in your specific deployment
  • Whether an exploit even exists

A CVSS 9.8 vulnerability in a library function your application never calls is less urgent than a CVSS 7.0 vulnerability with a public exploit targeting your exact configuration.

The Prioritization Stack

Effective prioritization combines multiple signals:

SignalWhat It Tells YouSource
CVSSTheoretical severityNVD, vendor advisories
EPSSProbability of exploitation in the next 30 daysFIRST.org EPSS
KEVConfirmed active exploitationCISA KEV Catalog
Reachability analysisWhether the vulnerable code is actually executedTrivy, Snyk, Endor Labs
Asset criticalityBusiness impact of the affected systemYour asset inventory
1
2
3
4
5
6
7
# Query EPSS scores for specific CVEs
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2024-3094,CVE-2025-55182" | \
  jq '.data[] | {cve: .cve, epss: .epss, percentile: .percentile}'

# Example output:
# {"cve": "CVE-2025-55182", "epss": "0.975", "percentile": "0.999"}
# {"cve": "CVE-2024-3094", "epss": "0.871", "percentile": "0.993"}
Practical rule of thumb: If a CVE is in the CISA KEV catalog, it gets patched immediately — no discussion. If EPSS is above 0.6 and the asset is internet-facing, it goes to the top of the queue. Everything else gets prioritized by reachability and asset criticality.

A Prioritization Decision Matrix

                    ┌─────────────────────────────────────────────┐
                    │              Vulnerability Found             │
                    └──────────────────┬──────────────────────────┘
                                       │
                          ┌────────────▼────────────┐
                          │   In CISA KEV catalog?   │
                          └────┬───────────────┬────┘
                            Yes│               │No
                               ▼               ▼
                        ┌──────────┐   ┌──────────────┐
                        │ PATCH    │   │ EPSS > 0.6?  │
                        │ NOW      │   └──┬────────┬──┘
                        │ (SLA:    │   Yes│        │No
                        │  24-48h) │      ▼        ▼
                        └──────────┘  ┌────────┐ ┌─────────────┐
                                      │Priority│ │Reachable in │
                                      │High    │ │your deploy? │
                                      │(SLA:   │ └──┬───────┬──┘
                                      │ 7 days)│ Yes│       │No
                                      └────────┘    ▼       ▼
                                              ┌────────┐ ┌────────┐
                                              │Normal  │ │Backlog │
                                              │(SLA:   │ │(SLA:   │
                                              │ 30d)   │ │ 90d)   │
                                              └────────┘ └────────┘

Step 4: The Automation Imperative

At 130 CVEs per day, manual processes are not slow — they are impossible. Every step that can be automated must be automated.

Automated Scanning Pipelines

Integrate scanning into every stage of your software delivery lifecycle:

  • IDE: Semgrep, Trivy IDE plugins — catch issues before commit
  • Pre-commit / PR: CI pipeline scans (SCA, SAST, container image) — gate merges on findings
  • Registry: continuous scanning of stored container images — detect newly disclosed CVEs in already-built images
  • Runtime: periodic infrastructure scans — catch configuration drift and unpatched systems

Automated Patch Management

For OS-level packages, automated patching is mature and battle-tested:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Debian/Ubuntu: configure unattended-upgrades
cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'CONF'
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
CONF

# Enable the timer
systemctl enable --now unattended-upgrades
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Ansible: patch all hosts and reboot if needed
- name: Apply security patches
  hosts: all
  become: true
  tasks:
    - name: Update all packages (RHEL/CentOS)
      ansible.builtin.dnf:
        name: "*"
        state: latest
        security: true
      when: ansible_os_family == "RedHat"

    - name: Update all packages (Debian/Ubuntu)
      ansible.builtin.apt:
        upgrade: safe
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Check if reboot is required
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_required

    - name: Reboot if needed
      ansible.builtin.reboot:
        reboot_timeout: 300
      when: reboot_required.stat.exists | default(false)

For container images, automate base image updates and rebuilds:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Renovate: auto-update base images in Dockerfiles
# renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "docker": {
    "enabled": true,
    "pinDigests": true
  },
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "matchUpdateTypes": ["patch", "minor"],
      "automerge": true
    }
  ]
}

Automated Ticket Creation and SLA Tracking

When a scan produces findings, they need to reach the right team with the right priority — automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Pseudocode: scan results → Jira tickets with SLA
def process_scan_results(findings):
    for finding in findings:
        priority = calculate_priority(
            cvss=finding.cvss,
            epss=finding.epss,
            in_kev=finding.cve in kev_catalog,
            asset_criticality=finding.asset.criticality,
            reachable=finding.reachable
        )

        sla_days = {
            "critical": 2,    # KEV or EPSS > 0.9
            "high": 7,        # EPSS > 0.6 or CVSS >= 9.0
            "medium": 30,     # Reachable, moderate score
            "low": 90          # Not reachable, low score
        }

        create_ticket(
            project="VULN",
            summary=f"[{finding.cve}] {finding.package}{finding.asset.name}",
            description=finding.detail,
            priority=priority,
            due_date=now() + timedelta(days=sla_days[priority]),
            labels=["vulnerability", "automated"],
            assignee=finding.asset.team_owner
        )

SBOM: Knowing What Is in Your Software

A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in a piece of software — libraries, frameworks, transitive dependencies, and their versions.

When the next Log4Shell drops, the first question every security team asks is: “Are we affected?” Without an SBOM, the answer is “we don’t know, give us a week.” With an SBOM, the answer is a database query.

1
2
3
4
5
6
7
8
# Generate SBOM for a container image (SPDX format)
trivy image --format spdx-json --output sbom.spdx.json nginx:1.27

# Generate SBOM for a project directory (CycloneDX format)
syft dir:./myapp -o cyclonedx-json > sbom.cdx.json

# Query an SBOM: "do we use log4j anywhere?"
cat sbom.cdx.json | jq '.components[] | select(.name | test("log4j")) | {name, version}'

SBOMs are becoming a regulatory requirement. The EU Cyber Resilience Act (CRA) and updates to US Executive Order 14028 both mandate SBOM generation and disclosure for software sold to government entities. This is not optional for much longer.

SBOM Integration in CI/CD

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Generate and store SBOM on every build
- name: Generate SBOM
  run: |
    syft dir:. -o cyclonedx-json > sbom.cdx.json
    trivy sbom sbom.cdx.json --severity HIGH,CRITICAL --exit-code 1

- name: Upload SBOM to dependency track
  run: |
    curl -X POST "https://deptrack.internal/api/v1/bom" \
      -H "X-Api-Key: ${DEPTRACK_API_KEY}" \
      -F "project=${PROJECT_UUID}" \
      -F "bom=@sbom.cdx.json"

Maturity Model: Where Are You Today?

Vulnerability management programs evolve through three stages. Knowing where you are tells you what to invest in next.

StageCharacteristicsTypical Outcome
ReactiveQuarterly scans, manual triage, spreadsheets, no SLAsCVEs accumulate, breaches are discovered externally
ProactiveContinuous scanning, automated prioritization, SLAs enforced, SBOM generatedKnown vulnerabilities are managed, MTTR is measured
PredictiveEPSS-driven prioritization, reachability analysis, automated patching, threat intelligence integrationTeam focuses only on exploitable risk, MTTR is minimized

Most organizations are somewhere between reactive and proactive. The gap between proactive and predictive is where automation and data-driven prioritization make the difference.

Moving Up the Maturity Curve

Reactive to Proactive:

  • Deploy continuous scanning (not quarterly)
  • Implement an SBOM pipeline
  • Define and enforce SLAs per severity
  • Automate ticket creation from scan results

Proactive to Predictive:

  • Integrate EPSS and KEV into prioritization logic
  • Implement reachability analysis to filter noise
  • Automate patching for OS-level and container base images
  • Correlate vulnerability data with threat intelligence feeds

Metrics That Matter

You cannot improve what you do not measure. These four metrics define the health of a vulnerability management program:

MetricWhat It MeasuresTarget
MTTR (Mean Time to Remediate)Average time from detection to fixCritical: <48h, High: <7d, Medium: <30d
Scan CoveragePercentage of assets scanned regularly>95% of production assets
SLA CompliancePercentage of vulnerabilities fixed within SLA>90%
Vulnerability DensityOpen critical/high CVEs per assetTrending downward quarter-over-quarter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Example: query your vulnerability database for MTTR
psql -d vulndb -c "
  SELECT
    severity,
    ROUND(AVG(EXTRACT(EPOCH FROM (remediated_at - detected_at)) / 86400), 1) AS avg_mttr_days,
    COUNT(*) AS total_vulns,
    ROUND(100.0 * SUM(CASE WHEN remediated_at - detected_at <= sla_interval THEN 1 ELSE 0 END) / COUNT(*), 1) AS sla_compliance_pct
  FROM vulnerabilities
  WHERE detected_at >= NOW() - INTERVAL '90 days'
    AND remediated_at IS NOT NULL
  GROUP BY severity
  ORDER BY severity;
"

Track these metrics on a dashboard visible to both security and engineering leadership. When MTTR creeps up, it is a signal that either prioritization is broken or patching automation needs investment.

Conclusion

Vulnerability management in 2026 is a data engineering problem disguised as a security problem. The volume of CVEs, the degradation of NVD enrichment, and the expanding attack surface of modern software supply chains make manual approaches fundamentally unviable.

The path forward is clear:

  1. Start with asset inventory — discovery must be automated and continuous
  2. Scan every layer — infrastructure, containers, code, dependencies
  3. Prioritize ruthlessly — EPSS, KEV, and reachability analysis over raw CVSS scores
  4. Automate everything — scanning, patching, ticketing, and SLA tracking
  5. Generate SBOMs — know what is inside your software before the next zero-day forces the question
  6. Measure what matters — MTTR, coverage, SLA compliance, vulnerability density

The organizations that treat vulnerability management as an engineering discipline — automated, measured, and continuously improved — will survive. The rest will be case studies.


References: