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.
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.
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.
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
});
}
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:
npm install ran against a locked package-lock.json that still references the old version).trivy image --update to force a re‑fetch.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.
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.
trivy image --severity CRITICAL reports “0 vulnerabilities” even though a critical flaw is present.libssl in Debian vs. openssl in Alpine). A scanner that matches only on name will flag both; you must also verify version and architecture before marking a finding as resolved.trivy fs and grype fs scan the explicit layers you pass them, but a vulnerable function buried three levels deep in a compiled binary may not have a version string to match against. Result: false‑negative reports that disappear only after a full binary audit (e.g., bom analysis or syft SBOM generation).ApplicationSet may auto‑generate image tags from a tagTemplate. If the template increments a patch number (1.2.3 → 1.2.4) but the underlying base image hasn’t been updated, a CVE in the base layer persists across releases. Pinning both app and base versions in a single manifest (e.g., image: myregistry/myapp@sha256:...) eliminates this class of drift.trivy image --skip-unfixed hide vulnerabilities that have no official fix yet. In a high‑velocity environment, this can mask a backlog of “unfixed” CRITICAL items that never get addressed because they’re out of sight. Prefer trivy image --severity UNFIXED and a weekly review ritual rather than permanent suppression.timeout set by the CI runner. The job succeeds with a partial report, and the gate passes unseen. Set TMPDIR or increase the job’s timeout minutes, or pre‑populate the scanner’s cache as a pipeline artifact.development image (e.g., debug symbols, test helpers) may not exist in the production image because the Dockerfile uses multi‑stage builds. Scanning the wrong stage produces a flood of irrelevant findings. Always tag the stage you intend to deploy (--target production in BuildKit) or scan the final FROM image only.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy