Vulnerability Management

Production engineering guide for vulnerability management covering patterns, implementation strategies, and operational best practices.

Vulnerability management is the disciplined, repeatable process of discovering security weaknesses across the software supply chain, assessing their potential impact, prioritizing remediation based on risk, and verifying that fixes take effect before code reaches production. It matters whenever code, containers, or infrastructure change—on every commit, before a release, after a dependency update, or in response to a CVE disclosure. Unlike a one‑time audit, effective vulnerability management is continuous; it integrates into CI/CD pipelines, tracks remediation state across environments, and feeds metrics to compliance and risk teams.

1. Identify what’s vulnerable

The first step is inventory: run a scanner against the assets you own—container images, filesystem trees, git repos, or cloud resources—and capture a list of installed packages and their versions. Real tools ship with commands you can call directly from a shell.

Container images

trivy image --format json --severity HIGH,CRITICAL myregistry/myapp:release-12
docker scan myregistry/myapp:release-12 --json
grype fs --output json

All three contact CVE and OSV feeds, download the matching database, and emit a JSON document describing each matched vulnerability. If the image tag is missing or the registry returns a non‑2xx response, you’ll see an error like:

trivy: error during image resolution: manifest for myregistry/myapp:release-12 not found in any registry
docker: error during connect: Head "https://registry-1.docker.io/v2/myregistry/myapp/manifests/release-12": denied: no matching manifest for linux/amd64
grype: failed to fetch remote package database: HTTP 403 fetch attempt denied

Filter the output to the fields you care about:

trivy image --format json --severity HIGH,CRITICAL myregistry/myapp:release-12 \
  | jq '.Results[0].Vulnerabilities[] | select(.Severity == "CRITICAL") | {id: .Vulnerability.ID, pkg: .PkgName, fixed: .InstalledVersion}'

Filesystem or git repos

trivy fs --format json --severity MEDIUM,LOW . \
  | jq '.Results[0].Vulnerabilities[] | {id: .Vulnerability.ID, pkg: .PkgName, vers: .InstalledVersion}'
grype . --format json \
  | jq '.matches[] | select(.vulnerabilities[0].severity == "high") | {cve: .vulnerabilities[0].id, pkg: .artifact.name, vers: .artifact.version}'

Local package databases

psql -d asset_inventory -c "SELECT cve_id, package_name, installed_version FROM pkg_cves WHERE severity = 'critical';"
psql: error: table pkg_cves does not exist

Create and populate that table once from an osv-scanner or trivy export, then query it fast in CI or dashboards.

2. Prioritize what to fix

Not every CVE demands immediate action. Prioritization combines three data points: severity (CVSS or vendor‑assigned), exploit availability (EPSS, vendor‑reported active exploitation), and asset context (is this image running in production? does it handle PII?).

Map a CVE to its metrics

curl -s "https://api.osv.dev/v1/vulns/detail/CVE-2023-1234" \
  | jq '{id: .vuln.id, cvss: .vuln.cvssScore, epss: .vuln.epssScore, desc: .vuln.description}'

If you keep a local copy of the OSV database, a psql query can replace the HTTP round‑trip:

psql -d vuln_db -c "SELECT cve_id, cvss_score, epss_score FROM osv_cves WHERE cve_id = 'CVE-2023-1234';"
psql: error: column epss_score does not exist

—add the column from a recent OSV snapshot, then re‑run.

Combine scanner output with priority data Pipe the trivy/grype/docker scan JSON into a small filter that joins on CVE ID and package name, then tags each finding with a priority flag:

trivy image --format json --severity LOW,HIGH,CRITICAL myregistry/myapp:release-12 \
  | jq '
      .Results[0].Vulnerabilities
      | map({
          id: .Vulnerability.ID,
          pkg: .PkgName,
          installed: .InstalledVersion,
          fixed: .FixedVersion,
          severity: .Severity,
          cve: .Vulnerability.CVE_ID
        })
      | map(. + {priority: (if .severity == "CRITICAL" then "P0"
                         elif .Vulnerability.CVE_ID as $cid
                         | (curl -s "https://api.osv.dev/v1/vulns/detail/$cid" | .vuln.epssScore) > 0.1
                         then "P1" else "P2" end)})
    '

The result is a ranked list you can dump to a CSV, post to a ticketing system, or gate a CI job.

3. Remediate

Remediation falls into three patterns: bump a dependency version, rebuild a container from a clean base, or apply a compensating control (runtime WAF, network segmentation). The goal is to eliminate the matched package/version pair so the next scan no longer reports it.

Dependency version bump (Node example)

npm install [email protected] --save-exact

If the update introduces a regression, npm audit will re‑scan and either confirm the vacancy or surface a new finding:

added 43 packages, and audited 143 packages in 2s
found 1 high severity vulnerability
  run `npm audit fix` to fix them

npm audit fix --force will auto‑upgrade to the newest compatible version, but it may pull in a different vulnerability set—always re‑scan.

Dependency version bump (Python example)

pip install "pkg-fix>=2.4.1,<3.0"

Or, if you use a requirements.txt and Dependabot‑style automation, the file entry changes to:

pkg-fix @ https://files.pythonhosted.org/packages/…/pkg_fix-2.4.1-py3-none-any.whl

Container image rebuild

docker build --pull -t myregistry/myapp:release-12 .

The --pull flag forces re‑retrieval of the base image, ensuring you’re not pulling a stale layer cached from a previous build. If the Dockerfile uses FROM alpine:3.19 and a CVE is discovered in alpine:3.19, the rebuild will pull alpine:3.20 (or whatever the current tagged release is) and the subsequent trivy image run will no longer flag the base‑image CVE.

Git‑based remediation workflow Most teams automate this via a CI step that opens a PR when a scanner finds a new vulnerability. A minimal GitHub Actions snippet looks like:

name: vulnerability-scan
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * *'   # daily at 06:00 UTC
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: trivy image scan
        run: |
          trivy image --format json --severity HIGH,CRITICAL myregistry/myapp:$(git describe --tags)
          | tee trivy-report.json
      - name: comment PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const report = JSON.parse(fs.readFileSync('trivy-report.json', 'utf8'));
            const critical = report?.Results?.[0]?.Vulnerabilities?.filter(v => v.Severity === 'CRITICAL') ?? [];
            if (critical.length) {
              const body = `⚠️ ${critical.length} critical vulnerability(ies) found. See report.`;
              github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body
              });
            }

4. Verify

After a fix lands, re‑run the same scanner against the same asset. The vulnerability entry should either disappear entirely (if the version bump removed the vulnerable code) or change status to “fixed” / “unfixed” depending on scanner configuration.

trivy image --format json --severity HIGH,CRITICAL myregistry/myapp:release-12 \
  | jq '.Results[0].Vulnerabilities | length'

Output 0 means the scan no longer sees a match. If the count stays positive, check why:

Handling drift In CI/CD pipelines, images are often rebuilt from cache. A docker build without --pull may reuse an old base‑image layer, re‑introducing a CVE that was previously cleared. Guard against this with a pipeline lint that enforces --pull on every build, or store a build‑metadata hash (e.g., BUILD_ID=$(date +%s)) and verify it matches the scan’s image tag after deployment.

5. Report and integrate

Visibility outside the terminal is essential for stakeholders, auditors, and executive dashboards. Most scanners can output formats that feed downstream tools.

SARIF for GitHub Code Scanning

trivy image --format sarif --output trivy-report.sarif myregistry/myapp:release-12

Upload the file in the GitHub UI: gh codeql upload-sarif -q trivy-report.sarif. The findings appear in the “Security” tab of the repository, where maintainers can triage without leaving the PR flow.

JSON feed for external dashboards

trivy image --format json --severity ALL myregistry/myapp:release-12 \
  > /var/www/vuln-dashboard/data/current.json

A lightweight web server can serve this file, and the dashboard can aggregate across many clusters by scp‑ing or rsync-ing the output nightly.

CI gate that blocks merges

if [ "$(trivy image --severity CRITICAL --format json myregistry/myapp:release-12 \
    | jq '.Results[0].Vulnerabilities | length')" -gt 0 ]; then
  echo "🛑 Critical vulnerabilities present; failing CI."
  exit 1
fi

Place this at the bottom of any job that builds or deploys the image. The gate will stay green only when the vulnerability count for the severity threshold is zero.

Ticket sync (generic webhook) When a new high‑severity CVE is detected, POST a minimal payload to your issue tracker:

curl -X POST -H "Content-Type: application/json" \
  -d "{\"title\": \"CVE-2023-1234 critical in myapp\", \"severity\": \"critical\", \"cve\": \"CVE-2023-1234\", \"component\": \"myregistry/myapp:release-12\"}" \
  https://issues.example.com/api/v1/create

The receiving system can then auto‑assign, set SLA, and track resolution state. Keep the payload schema stable; adding fields later is easier than renaming them across integrations.

Sharp edges — what silently fails, what you only learn after it breaks

These edges aren’t theoretical—they’re the difference between a vulnerability program that looks green on a dashboard and one that actually reduces risk. Treat the scanner output as a signal, not a verdict, and build processes that surface context, verify fixes, and enforce consistency across every commit.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.