StackPractices
intermediate By Mathias Paulenko

Dependency Vulnerability Report Template

A template for documenting individual dependency vulnerabilities, including exploitability, impact, remediation plans, and risk acceptance.

Overview

Raw scanner output doesn’t make decisions. When Snyk or Trivy dumps two hundred findings into a JSON file, someone still has to answer the questions that matter: is the vulnerable function reachable in our code? Which service is exposed? Who owns the fix, and by when?

A Dependency Vulnerability Report Template turns one finding into a decision-ready document. It pairs the CVE facts (severity, affected versions, patched versions) with your project’s runtime context (reachability, internet exposure, compensating controls), so a developer can start remediation and a security lead can defend the priority without re-running the scan.

This template is designed for one finding per report: a single CVE in a single project. If you need a consolidated view of an infrastructure-wide scan across many hosts, images, and repos, use the Vulnerability Scan Report Template instead. If you only need a fast go/no-go call during triage, the Dependency Vulnerability Triage Template is lighter — use it first and come back here for the documented record.

When to Use

  • After a scheduled or PR-triggered SCA scan produces a finding that needs human review.
  • When a high-profile CVE drops and every team must answer “are we affected, and where?” on a deadline.
  • Before a release gate or security audit, to show that findings are tracked rather than buried in a scanner dashboard.
  • When a customer, vendor, or auditor asks for evidence of how a specific finding was handled.
  • To feed findings into a vulnerability management program with named owners and due dates.

When not to use it: for aggregate reporting across many findings and assets: that’s the scan report’s job. And if the vulnerable component isn’t a third-party dependency but your own code, an incident or SAST finding template fits better. One page per finding is the unit that keeps ownership honest.

Prerequisites

  • A completed SCA scan from Snyk, OWASP Dependency-Check, Trivy, Grype, or GitHub Advanced Security. Keep the JSON output — it’s the report’s source of truth.
  • Access to the affected repository and its dependency manifest (package-lock.json, requirements.txt, pom.xml, go.sum).
  • A scoring baseline. CVSS is the starting point; you will extend it with exploit signals in the prioritization step below.
  • A remediation SLA agreed with engineering, so “Critical” means the same deadline to every reader.
  • An SBOM if the dependency ships in more than one service — it tells you everywhere the package lives, not just where the scanner happened to look.

The Template

Copy the tables into a ticket, an ADR, or a Markdown file committed next to the service it describes. Fill every row: a report with “TBD” in the owner column is a finding that will age out unpatched.

1. Finding Summary

FieldDescriptionExample
Report IDUnique identifierVULN-2026-0042
Date discoveredWhen the finding was identified2026-06-27
ReporterPerson or tool that found itSnyk bot / Security team
ProjectRepository or service affectedpayment-service
EnvironmentWhere the dependency runsProduction, CI, Dev

2. Vulnerability Details

FieldDescriptionExample
CVE IDCommon Vulnerabilities and Exposures identifierCVE-2021-44228
SeverityCVSS base score and vectorCritical (10.0, CVSS:3.1/AV:N/AC:L/…)
EPSS scoreExploit prediction probability0.97 (97th percentile)
In CISA KEV?Confirmed in-the-wild exploitationYes — added 2021-12-10
Affected packageLibrary and ecosystemlog4j-core (Maven)
Installed versionCurrent version in the project2.14.0
Patched versionFirst fixed version2.17.1
Attack vectorHow the vulnerability is exploitedRCE via attacker-controlled log messages
Exploit availabilityIs a public exploit known?Public PoC within 24h of disclosure

3. Impact Assessment

QuestionAnswer
Is the vulnerable function reachable?Yes, via request logging
Is the dependency exposed to the internet?Yes, edge service
Does the dependency process untrusted input?Yes, user-supplied content
Is a compensating control in place?WAF blocks known exploit patterns
Estimated business impactPayment flow interruption

4. Remediation Plan

StepOwnerDue DateStatus
Upgrade dependency to 2.17.1Backend team2026-07-01Not started
Run regression tests in stagingQA team2026-07-02Not started
Deploy to productionDevOps2026-07-03Not started
Verify fix via re-scanSecurity team2026-07-05Not started

5. Risk Acceptance

FieldValue
Can the risk be accepted?No
JustificationInternet-facing RCE with public exploit
Risk ownerEngineering manager
Approval dateN/A
Review dateN/A

Filled Example

The tables below show a completed report for a realistic scenario: an SCA scan finds CVE-2021-44228 (Log4Shell) in log4j-core 2.14.0 inside a payment service. Notice how every row answers a question a decision-maker would ask; the report reads as an argument for acting now, not as scanner output.

1. Finding Summary

FieldValue
Report IDVULN-2026-0042
Date discovered2026-06-27
ReporterWeekly Snyk scan, confirmed by security team
Projectpayment-service (repo: org/payment-service)
EnvironmentProduction (internet-facing), staging

2. Vulnerability Details

FieldValue
CVE IDCVE-2021-44228
SeverityCritical — CVSS 10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
EPSS score~0.97, top percentile
In CISA KEV?Yes — actively exploited in the wild
Affected packageorg.apache.logging.log4j:log4j-core (Maven)
Installed version2.14.0 (direct dependency)
Patched version2.17.1
Attack vectorJNDI lookup triggered by attacker-controlled strings in log messages — remote code execution without authentication
Exploit availabilityPublic exploit code widely available; mass scanning observed

3. Impact Assessment

QuestionAnswer
Is the vulnerable function reachable?Yes — every incoming request is logged with headers and body fields
Is the dependency exposed to the internet?Yes — payment-service sits behind the public API gateway
Does the dependency process untrusted input?Yes — User-Agent, request IDs, and form fields reach log statements
Is a compensating control in place?Partial — WAF has rules for common ${jndi: patterns, but obfuscation bypasses are documented
Estimated business impactFull remote code execution on the payment path; cardholder data exposure, PCI scope breach

4. Remediation Plan

StepOwnerDue DateStatus
Upgrade log4j-core to 2.17.1 in pom.xmlBackend team2026-06-28In progress
Add WAF emergency rule set for JNDI stringsSecurity team2026-06-27Done
Regression-test logging in stagingQA team2026-06-29Not started
Deploy to production under change freeze exceptionDevOps2026-06-30Not started
Re-scan and verify dependency treeSecurity team2026-07-01Not started

5. Risk Acceptance

FieldValue
Can the risk be accepted?No
JustificationKEV-listed RCE on an internet-facing PCI service; no acceptable interim state
Risk ownerVP Engineering
Approval dateN/A
Review dateN/A — escalate daily until closed

The filled example makes the implicit explicit: severity alone would flag hundreds of findings, but “KEV-listed + internet-facing + reachable via request logging + no authentication required” narrows the answer to “fix today, under an exception to the change freeze.”

How It Works

The five sections answer five different questions, and each one earns its place.

Finding Summary locates the report: who found it, where, and when. When the same CVE resurfaces in another service six months later, this block is what makes the old report discoverable.

Vulnerability Details is the objective part. Everything here should be verifiable against the scanner output, NVD, or the package repository — severity, EPSS, KEV status, versions, exploit availability. If a field can’t be verified, say so rather than guessing.

Impact Assessment is where the report earns its cost. A CVSS 9.8 in a build tool that never runs outside CI is a different problem from a CVSS 7.5 in an edge service. Reachability, exposure, untrusted input, and compensating controls convert a generic score into your risk.

Remediation Plan assigns work. Every step gets an owner and a date, and the plan ends with a verification step. A report that skips re-scanning is a report that gets closed on hope.

Risk Acceptance captures the decision not to patch, when that’s the call. If the answer is “no,” the justification explains why the risk can’t wait; if “yes,” a named owner and review date keep the acceptance from becoming permanent amnesia.

flowchart diagram: SCA scan finds CVE

Prioritizing Findings

CVSS measures how bad a vulnerability is in general; it says nothing about how bad it’s for you. Two extra signals fix most of that gap:

  • EPSS (Exploit Prediction Scoring System, maintained by FIRST.org) estimates the probability that a CVE will be exploited in the wild in the next 30 days. A CVSS 9.8 with EPSS 0.01 is a different conversation from a CVSS 7.5 with EPSS 0.95.
  • CISA KEV (Known Exploited Vulnerabilities catalog) lists CVEs with confirmed real-world exploitation. KEV-listed findings on reachable code skip the queue entirely.
  • Reachability analysis in tools like Snyk or Endor Labs checks whether your code actually calls the vulnerable function. Unreachable findings can often be documented and scheduled rather than rushed.

Combining them produces a simple, defensible priority scheme. The SLAs below are common starting points. Adjust to your own vulnerability management policy:

PrioritySignalsExample SLA
P0 — EmergencyKEV-listed, or public exploit + internet-facing + reachableFix within 72 hours
P1 — UrgentHigh EPSS (>0.5) or reachable exploit pathFix within 7 days
P2 — StandardHigh/critical CVSS, no known exploitFix within 30 days
P3 — ScheduledMedium severity, reachableFix within 90 days
P4 — BacklogUnreachable, dev-only, or strong compensating controlsNext maintenance cycle

Record the priority and its justification in the report. Six months later, “why was this CVE fixed in 48 hours while that one waited a quarter?” should be answerable from the document alone.

Variants

  • Executive summary report: one page with counts by severity, trend over time, and the top open risks — for leadership that needs posture, not CVE lists.
  • CI/CD failure report: captures which finding blocked the pipeline, why the gate tripped, and what unblocks it. Lives next to the failed build.
  • Open-source maintainer report: reformatted for upstream disclosure (reproducible trigger, affected versions, suggested fix) following coordinated-disclosure norms.
  • Historical trend report: aggregates findings per quarter to show whether posture is improving and where the same classes of bugs keep reappearing.

What Works

  • Keep one report per finding (or per CVE/project pair): merged reports lose their owner, deadline, and audit trail.
  • Always document exploitability in your context, not just the raw CVSS score.
  • Assign an owner and a due date before the report leaves triage; an unowned finding is an unfixed finding.
  • Link the report to the scan output, the SBOM entry, and the remediation pull request for full traceability in both directions.
  • Re-scan after remediation and record the verification step; close on evidence, not on the merge.
  • Review accepted risks on a fixed cadence (quarterly works); conditions change, and so should the decision.
  • Use reachability analysis to cut false positives, and keep the justification when you deprioritize a finding. Auditors accept “unreachable, documented”; they don’t accept “ignored.”
  • Comment on PRs that introduce vulnerable dependencies: catching a bad dep at review time is cheaper than any report:
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    allow:
      - dependency-type: "all"
    labels:
      - "security"
      - "dependencies"
    open-pull-requests-limit: 10

Common Mistakes

  • Remediating by CVSS alone. Score is one input; reachability, EPSS, KEV, and exposure usually matter more.
  • Forgetting to regression-test the upgraded dependency under real load; version bumps break behavior, not just APIs.
  • Closing the report before a re-scan confirms the fix actually landed in the built artifact.
  • Reporting only direct dependencies. Most SCA findings live in transitive packages; the Dependency Upgrade Runbook covers safe upgrade paths for those.
  • Accepting risk without a written justification, owner, and review date. Undocumented acceptance is indistinguishable from neglect.
  • Ignoring dev dependencies. They run in CI, with your secrets and signing keys, so scan them too:
# npm audit covers the whole tree, dev dependencies included
npm audit
npm audit --omit=dev   # production-only view
snyk test --dev        # include dev dependencies in Snyk
  • Missing OS-level packages in container images. A dependency scan of package-lock.json says nothing about OpenSSL in your base image — scan the image separately:
trivy image --severity HIGH,CRITICAL myapp:latest
grype myapp:latest --fail-on high

Automating the Report

Automated SCA scan in GitHub Actions

This workflow scans on every dependency-change PR and on a weekly schedule, then emits a Markdown table straight into a report file. Note the continue-on-error: Snyk exits non-zero when it finds vulnerabilities, and without it the report step would never run.

name: Dependency Security Scan
on:
  pull_request:
    paths:
      - "package.json"
      - "package-lock.json"
      - "requirements.txt"
      - "pom.xml"
  schedule:
    - cron: "0 6 * * 1"  # Weekly Monday scan

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

      - name: Run Snyk scan
        uses: snyk/actions/node@master  # pin to a commit SHA in production
        continue-on-error: true
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high --json-file-output=snyk-results.json --dev

      - name: Run Trivy filesystem scan
        run: |
          trivy fs --format json --output trivy-results.json .
          trivy fs --format table --exit-code 0 --severity HIGH,CRITICAL .

      - name: Generate vulnerability report
        run: |
          node -e "
            const fs = require('fs');
            const snyk = fs.existsSync('snyk-results.json')
              ? JSON.parse(fs.readFileSync('snyk-results.json'))
              : { vulnerabilities: [] };
            const trivy = fs.existsSync('trivy-results.json')
              ? JSON.parse(fs.readFileSync('trivy-results.json'))
              : { Results: [] };
            const findings = [
              ...snyk.vulnerabilities.map(v => ({
                source: 'snyk', cve: v.identifiers?.CVE?.[0] || 'N/A',
                package: v.name, severity: v.severity,
                fixedVersion: v.fixedIn?.[0] || 'N/A'
              })),
              ...(trivy.Results || []).flatMap(r => r.Vulnerabilities || []).map(v => ({
                source: 'trivy', cve: v.VulnerabilityID,
                package: v.PkgName, severity: v.Severity,
                fixedVersion: v.FixedVersion || 'N/A'
              }))
            ];
            console.log('| Source | CVE | Package | Severity | Fixed Version |');
            console.log('|--------|-----|---------|----------|---------------|');
            findings.forEach(f => {
              console.log('| ' + f.source + ' | ' + f.cve + ' | ' + f.package + ' | ' + f.severity + ' | ' + f.fixedVersion + ' |');
            });
          " > vulnerability-report.md

      - name: Upload report as artifact
        uses: actions/upload-artifact@v4
        with:
          name: vulnerability-report
          path: vulnerability-report.md

Two details worth keeping: --json-file-output writes machine-readable results instead of piping stdout (Snyk prints progress text there), and fixedIn is the field that actually carries the patched version in Snyk JSON; fixInfo doesn’t exist in the output schema.

SBOM generation with CycloneDX

Generate a Software Bill of Materials to track every dependency, including transitive ones:

#!/bin/bash
set -euo pipefail

# SBOM for a Node.js project (npm lockfile required)
npx @cyclonedx/cyclonedx-npm --output-format JSON --output-file sbom.json

# SBOM for a Python project (cyclonedx-bom 4.x)
pip install cyclonedx-bom
cyclonedx-py requirements requirements.txt --of JSON -o sbom-python.json

# Enrich the SBOM with vulnerability data via Trivy
trivy sbom --format json --output sbom-enriched.json sbom.json

# Extract the vulnerable packages from the enriched SBOM
jq '.Results[] | select(.Class=="sbom") | .Vulnerabilities[] | {
  cve: .VulnerabilityID,
  package: .PkgName,
  severity: .Severity,
  fixed_version: .FixedVersion
}' sbom-enriched.json

The enriched SBOM answers the question a single-repo scan can’t: which of my services ship this package at all? For a worked scanner example, see Python: pip-audit Vulnerability Scan.

Finding the transitive chain

npm audit tells you which package is vulnerable but not how it got in. npm ls <pkg> prints the dependency path. Run it for every vulnerable package to see which direct dependency drags it in:

#!/bin/bash
set -euo pipefail

npm audit --json > audit-output.json 2>/dev/null || true

echo "=== Vulnerable packages and their dependency paths ==="
jq -r '.vulnerabilities | keys[]' audit-output.json | while read -r pkg; do
  echo "── $pkg"
  npm ls "$pkg" 2>/dev/null | head -15 || echo "   not resolved in tree"
done

When the vulnerable package is transitive and the direct dependency hasn’t released a fix, overrides (npm ≥8.3) or resolutions (Yarn) forces a safe version. Pin the override, document it in the report, and set a reminder to remove it once the upstream package catches up.

See Also

Frequently Asked Questions

How do I choose which vulnerabilities to fix first?

Triage on four signals: is the vulnerable function reachable in your code, is there a known exploit (CISA KEV, public PoC, high EPSS), is the affected service internet-facing, and what is the CVSS severity. A medium score in a public service usually beats a critical score in an internal batch job.

What if no patched version exists?

Document a compensating control — a WAF rule, network isolation, input validation — with an owner and a review date. If the residual risk is still unacceptable, replace the dependency or disable the vulnerable feature. Never leave the field blank: "no fix yet" is still a documented decision.

Who should receive this report?

The engineering team that owns the fix, security for oversight and SLA tracking, product owners when remediation displaces roadmap work, and compliance when the report feeds an audit or customer questionnaire.

How do I handle a vulnerable dependency I cannot upgrade?

For a transitive package blocked by an upstream dependency, force a safe version with npm overrides, Yarn resolutions, or pip constraints, and document why. For a direct dependency pinned by an old framework, evaluate a compensating control versus a planned migration, and put both options with dates in the remediation plan.

What is the difference between SCA and SAST?

SCA scans third-party dependencies for known vulnerabilities (CVEs). SAST analyzes your own source code for flaws like injection or unsafe deserialization. They cover different layers — you need both, because SCA won't find the bug you wrote and SAST won't find the CVE you imported.

How does this report relate to the SBOM?

The SBOM is the inventory: every package, every version, every service that ships it. This report is the decision: one finding, its context, its plan. Use the SBOM to find every place a new CVE lands, then open one report per affected project.