Vulnerability Tracking & Triage

Permalink to "Vulnerability Tracking & Triage"

This workflow is part of Supply Chain Auditing & Dependency Verification. Without a structured triage process, automated scanners produce hundreds of daily alerts that overwhelm engineering teams and create alert fatigue — causing genuinely exploitable findings to be buried alongside irrelevant noise. The result is either ignored queues or blanket suppression rules that silence real threats. This page covers the complete pipeline from CVE feed ingestion through CI/CD blocking gates to closed-loop policy refinement, with SRI hash enforcement embedded at the final cryptographic checkpoint before production deployment.

Prerequisites

Permalink to "Prerequisites"

Conceptual Foundation

Permalink to "Conceptual Foundation"

Vulnerability triage is the decision process that converts raw scanner output — a flat list of CVE identifiers paired with affected package versions — into an ordered remediation queue with clear ownership, SLA deadlines, and documented risk-acceptance decisions. The process is grounded in two complementary scoring systems:

CVSS (Common Vulnerability Scoring System) provides a base score (0–10) derived from attack vector, complexity, privileges required, user interaction, and impact metrics. The W3C SRI specification itself does not define CVSS thresholds; each organisation sets its own. NIST SSDF practice RV.1.1 recommends establishing a repeatable response process against which CVSS thresholds are defined.

EPSS (Exploit Prediction Scoring System) adds a probability estimate (0–1) of exploitation in the wild within 30 days, derived from real-world threat intelligence. A CVE with CVSS 9.1 but EPSS 0.003 (0.3% exploitation probability) is typically lower priority than a CVSS 7.4 finding with EPSS 0.91 (91% probability) in an internet-facing script.

For frontend supply-chain risk the asset exposure dimension is critical: a vulnerable package loaded on every payment page through a CDN <script> tag carries fundamentally different risk than the same package used only in a build-time CLI tool. SRI hashes bound the blast radius — they ensure that even if a vulnerable version is referenced, the exact known artifact is what loads, blocking silent substitution with a patched or malicious build.

Vulnerability Triage Pipeline A five-stage pipeline diagram showing CVE feed ingestion, SBOM correlation, severity scoring, CI/CD gate enforcement, and closed-loop policy refinement, with SRI hash validation highlighted at the deployment gate. CVE Feeds NVD · OSV · GitHub SBOM Correlation CycloneDX / SPDX Score & SLA CVSS + EPSS + Exposure CI/CD Gate Block on critical SRI hash check Refine Allowlists Closed-loop feedback Step 1 Step 2 Step 3 Step 4 Step 5 Vulnerability Triage Pipeline from CVE ingestion to SRI-gated deployment

Step 1 — Configure CVE Feed Ingestion

Permalink to "Step 1 — Configure CVE Feed Ingestion"

Reliable triage begins with authoritative, low-latency feed sources. The three canonical sources for frontend supply-chain work are:

  • NVD (National Vulnerability Database)https://services.nvd.nist.gov/rest/json/cves/2.0 — the authoritative CVSS v3.1/v4.0 source; requires a free API key for sustained polling
  • OSV (Open Source Vulnerabilities)https://api.osv.dev/v1/query — ecosystem-aware (npm, PyPI, Maven, Go) with batch query support; no key required
  • GitHub Advisory Databasehttps://api.github.com/graphql — tightly integrated with npm audit and dependabot; useful when your CI already runs on GitHub Actions

Configure a scheduled job that polls each feed and normalises results into a shared schema. The following Node.js snippet queries OSV for all vulnerabilities affecting packages listed in an SBOM artifact:

// scripts/poll-osv.mjs
// Queries OSV batch endpoint for every package in a CycloneDX SBOM JSON.
import fs from 'node:fs/promises';

const sbom = JSON.parse(await fs.readFile('./sbom.cdx.json', 'utf8'));
const queries = sbom.components.map(c => ({
  package: { name: c.name, version: c.version, ecosystem: 'npm' }
}));

const res = await fetch('https://api.osv.dev/v1/querybatch', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ queries })
});
const { results } = await res.json();

const findings = results
  .flatMap((r, i) => (r.vulns ?? []).map(v => ({
    component: queries[i].package.name,
    version: queries[i].package.version,
    id: v.id,
    aliases: v.aliases ?? [],
    severity: v.database_specific?.severity ?? 'UNKNOWN'
  })));

await fs.writeFile('./vuln-findings.json', JSON.stringify(findings, null, 2));
console.log(`Found ${findings.length} findings across ${sbom.components.length} components`);

Expected output:

Found 3 findings across 47 components

Step 2 — Correlate Findings with the Dependency Graph

Permalink to "Step 2 — Correlate Findings with the Dependency Graph"

Raw CVE findings list affected package names and version ranges, but they do not tell you whether a vulnerable package is actually reachable in your runtime. Correlation with the dependency graph is what separates actionable alerts from noise.

Use Lockfile Mapping & Analysis to build a resolved dependency graph from your lockfile, then join it against the findings from Step 1:

// scripts/correlate.mjs
// Joins OSV findings with the resolved dependency graph from package-lock.json
import fs from 'node:fs/promises';

const lock = JSON.parse(await fs.readFile('./package-lock.json', 'utf8'));
const findings = JSON.parse(await fs.readFile('./vuln-findings.json', 'utf8'));

// Build a map: packageName → { version, isDirect, isDevOnly }
const graph = new Map();
for (const [path, meta] of Object.entries(lock.packages ?? {})) {
  if (!path) continue; // root entry
  const name = path.replace(/^node_modules\//, '').replace(/\/node_modules\//, ' > ');
  graph.set(name, {
    version: meta.version,
    isDirect: !path.includes('/node_modules/'),
    isDevOnly: !!meta.dev
  });
}

const correlated = findings.map(f => {
  const node = graph.get(f.component);
  return {
    ...f,
    resolved: !!node,
    installedVersion: node?.version,
    isDirect: node?.isDirect ?? false,
    isDevOnly: node?.isDevOnly ?? true, // unknown packages assumed dev-only
    riskNote: node?.isDevOnly ? 'dev-only — lower runtime exposure' : 'runtime dependency'
  };
}).filter(f => f.resolved);

await fs.writeFile('./correlated-findings.json', JSON.stringify(correlated, null, 2));
console.log(`${correlated.length} findings map to installed packages`);

Findings for packages that exist only in devDependencies still matter for build-time attacks (malicious postinstall scripts) but carry no browser-runtime exposure — a distinction that affects SLA assignment.

Step 3 — Assign Severity Tiers and SLA Deadlines

Permalink to "Step 3 — Assign Severity Tiers and SLA Deadlines"

With correlated findings in hand, apply a contextualised severity model. The following SLA matrix combines CVSS base score, EPSS probability, and asset exposure:

Tier CVSS Range EPSS Threshold Asset Exposure Remediation SLA Escalation Path
Critical 9.0–10.0 any internet-facing 24 hours Security lead → CISO → Compliance officer
High 7.0–8.9 > 0.10 internet-facing 72 hours Security engineer → DevOps manager
High (deprioritised) 7.0–8.9 ≤ 0.10 internal only 14 days Security engineer
Medium 4.0–6.9 > 0.10 internet-facing 7 days Security engineer
Low / Informational 0.1–3.9 any any 30 days Tracked in backlog
Dev-only any any build-time only Next sprint Dev team

Map these tiers to your tooling configuration. For npm audit, the --audit-level flag controls the CI block threshold:

# Fail CI on critical or high; treat medium and below as warnings
npm audit --audit-level=high --json > audit-report.json

For projects using pnpm or yarn, equivalent commands are pnpm audit --audit-level high and yarn npm audit --severity high respectively.

EPSS scores are not surfaced by npm audit natively. To enrich findings with EPSS, query the FIRST API:

# Fetch EPSS score for a single CVE
curl -s "https://api.first.org/data/1.0/epss?cve=CVE-2024-21538" \
  | jq '.data[0] | {cve: .cve, epss: .epss, percentile: .percentile}'
# → {"cve": "CVE-2024-21538", "epss": "0.00097", "percentile": "0.39071"}

Step 4 — Enforce CI/CD Blocking Gates

Permalink to "Step 4 — Enforce CI/CD Blocking Gates"

Policy-as-code enforcement prevents unpatched critical or high-severity vulnerabilities from reaching staging or production. The gate must run after dependency installation and before any build or test step, and must block the merge if thresholds are breached.

# .github/workflows/vulnerability-gate.yml
name: Vulnerability Triage Gate
on:
  pull_request:
    branches: [main, staging]
  push:
    branches: [main]

jobs:
  triage-gate:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write   # for uploading SARIF results

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies (no lifecycle scripts)
        run: npm ci --ignore-scripts

      - name: Run npm audit
        id: audit
        run: npm audit --json --audit-level=none > audit-report.json || true

      - name: Enforce critical threshold
        run: |
          CRITICAL=$(node -e "
            const r = require('./audit-report.json');
            const v = r.metadata?.vulnerabilities ?? {};
            console.log(v.critical ?? 0);
          ")
          HIGH=$(node -e "
            const r = require('./audit-report.json');
            const v = r.metadata?.vulnerabilities ?? {};
            console.log(v.high ?? 0);
          ")
          echo "Critical: $CRITICAL  High: $HIGH"
          if [ "$CRITICAL" -gt 0 ]; then
            echo "::error::CRITICAL CVE detected. Deployment blocked. File a compliance ticket and obtain security-lead sign-off to override."
            exit 1
          fi
          if [ "$HIGH" -gt 2 ]; then
            echo "::error::HIGH CVE count ($HIGH) exceeds threshold of 2. Triage required before merge."
            exit 1
          fi

      - name: Verify SRI hashes have not drifted
        run: |
          # Compare SRI hashes in the built HTML against the approved allowlist
          node scripts/verify-sri-allowlist.mjs

The verify-sri-allowlist.mjs step is what ties vulnerability management to SRI enforcement: any time a dependency update changes the content of a CDN-served script, the hash in the HTML must be updated deliberately, not silently. Hash drift — where a package version changes but the SRI attribute is not updated — is the most common integration failure and surfaces here as a detectable CI signal.

Configuration Reference Table

Permalink to "Configuration Reference Table"
Option Valid Values Default Security Implication
npm audit --audit-level critical, high, moderate, low, info low Only critical or high are suitable as merge-blocking thresholds
--json boolean flag off Required for machine-parseable output; pipe to jq or Node.js for threshold logic
--omit=dev dev, optional, peer none Omit dev dependencies from audit scope to reduce noise in production gate
--ignore-scripts on npm ci boolean flag off Prevents malicious postinstall hooks from running during audit setup
EPSS threshold for escalation 0.0–1.0 team-defined Values above 0.10 indicate meaningful exploitation probability
SLA override TTL (cached feed) hours 24 Maximum time the build may rely on a stale scan result before requiring a fresh fetch

Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

The output of this workflow feeds directly into two adjacent steps:

  1. SBOM update — after a successful triage cycle resolves findings, regenerate the SBOM to capture the clean component inventory. The Generating CycloneDX SBOMs for Frontend Assets guide covers the @cyclonedx/cyclonedx-npm CLI workflow for this.

  2. CSP header refresh — when a dependency update changes the hash of a CDN-loaded script, the Content-Security-Policy header’s script-src directive must be updated to include the new SHA-384 hash. Combine this triage step with a strict Configuring Content-Security-Policy with SRI policy that hard-fails on hash mismatches rather than logging-only.

  3. Third-party risk scoring — vulnerability findings enrich the risk profile maintained in Third-Party Risk Assessment. A vendor whose packages repeatedly carry unpatched CVEs may breach a risk-acceptance threshold, triggering a sourcing review.

Troubleshooting

Permalink to "Troubleshooting"

npm audit exits non-zero but there are no critical findings

The default --audit-level is low, so any severity finding causes a non-zero exit. Add --audit-level=high to your CI command or parse the JSON output explicitly rather than relying on the exit code for threshold logic.

ENOTFOUND registry.npmjs.org during audit in air-gapped CI

The audit command requires network access to the npm registry advisory endpoint. In air-gapped environments, proxy requests through a Verdaccio or Nexus instance that mirrors the npm advisory database, or pre-cache the latest advisory feed and point npm_config_registry at the local mirror.

OSV query returns NOT_FOUND for a known-vulnerable package

OSV uses ecosystem-qualified package identifiers. Ensure ecosystem is set to npm (not node or nodejs) and that version matches the exact string in the lockfile, not a semver range.

CI gate blocks a merge but the CVE is a false positive (no code path reaches the vulnerable function)

Create an explicit .nsprc or npm audit --ignore entry with a linked compliance ticket number and an expiry date. Record the justification in your SBOM’s note field. Blanket suppression without documentation creates a compliance gap under SOC 2 Type II CC7.1.

SRI hash mismatch detected on a CDN asset after a dependency update

The CDN vendor has updated the file content at the same URL. Do not simply remove the integrity attribute — re-generate the SHA-384 hash using openssl dgst -sha384 -binary <file> | openssl base64 -A against the new file and update the allowlist. If the update was not expected, treat it as a potential supply-chain incident and verify the change with the vendor’s release notes before accepting the new hash.

EPSS API rate limit (429) during bulk CVE enrichment

The FIRST EPSS API enforces per-minute rate limits. Batch requests into groups of 30 CVEs per call using the ?cve=CVE-A,CVE-B,... multi-value query parameter, and add an exponential back-off retry loop. Cache results for 24 hours to avoid re-fetching unchanged scores.

Security Boundary Note

Permalink to "Security Boundary Note"

Vulnerability tracking and triage addresses known CVEs in dependencies whose version or content you control. It does not protect against:

  • Zero-day exploits — CVEs that have not yet been published to NVD or OSV. SRI hash enforcement provides partial mitigation by ensuring only the exact approved artifact loads, but a zero-day in a legitimately approved version is not detected by scanning.
  • Malicious packages with no CVE record — typosquatting or dependency confusion attacks where a malicious package has never been reported as vulnerable. Provenance Verification Workflows and Sigstore attestation address this vector.
  • Runtime exploitation after load — once a script with a known CVE has been approved and loaded, SRI does not prevent the exploit code from executing. Triage is a deployment gate, not a runtime sandbox.
  • Build-time attacks through dev dependencies — a malicious postinstall script in a dev dependency can exfiltrate secrets during npm install before any audit completes. Always run npm ci --ignore-scripts in CI and audit before building.

Frequently Asked Questions

Permalink to "Frequently Asked Questions"
What is the difference between vulnerability scanning and vulnerability triage?

Scanning produces a raw list of findings from CVE databases. Triage is the human-and-policy layer that filters those findings by actual exploitability, asset exposure, and business context — turning hundreds of scanner alerts into an ordered remediation queue with SLA deadlines.

How does SRI hash validation fit into a vulnerability management workflow?

SRI hashes act as the cryptographic checkpoint that prevents a known-vulnerable version of a third-party script from being silently substituted with a patched or malicious one. Vulnerability triage identifies which versions are affected; SRI enforcement ensures only the exact approved artifact — not any substitute — loads in the browser.

Which compliance frameworks require formal vulnerability triage workflows?

PCI DSS v4.0.1 requirement 6.4.3 mandates an inventory and integrity mechanism for all payment-page scripts. NIST SSDF practice RV.1 requires a repeatable vulnerability-response process. SOC 2 Type II CC7.1 requires monitoring for software vulnerabilities. ISO 27001 Annex A control 8.8 covers management of technical vulnerabilities.

Can I use base CVSS scores alone to set remediation SLAs?

No. Base CVSS scores omit environmental factors such as network exposure, compensating controls, and whether a public exploit exists. A CVSS 9.1 vulnerability in a library called only server-side from an internal service carries far less risk than a CVSS 7.4 flaw in a script loaded on every payment page. Always adjust with EPSS probability scores and asset exposure before assigning an SLA.

What happens if the CVE feed is unreachable during a CI build?

Build pipelines should maintain a locally cached integrity allowlist so that transient feed outages do not halt every deployment. The recommended pattern is to cache the last successful scan result for up to 24 hours, fail open only if the cache is within its TTL, and require a signed compliance ticket for any override beyond the TTL window.

Permalink to "Related"

Articles in This Topic

Triaging npm audit Findings
Mapping CVEs to PCI DSS 6.4.3
Back to Supply Chain Auditing & Dependency Verification