Skip to content

CVE Ecosystem Support

HDF v3.3.0 adds first-class, structured slots for the data ecosystem that surrounds a vulnerability finding: CVSS scoring (all four major versions), EPSS exploit-probability data, CISA KEV catalog status, CWE classifications, and affected-package identifiers. Before v3.3.0 this information lived in free-form tags (e.g. tags.cvss_base_score, tags.cve); from v3.3.0 onward it sits in typed fields on Evaluated_Requirement and is recognised end-to-end by the converters, diff engine, parsers, and validators.

This guide explains what is now captured, how the pieces compose, and how downstream consumers can use them.

Why structured CVE data?

CVSS v4 (and v3.x before it) splits scoring into four metric groups, each owned by a different party in the disclosure-to-remediation chain:

GroupOwnerWhat it captures
BaseVendor / CNAIntrinsic technical severity (attack vector, complexity, impact on CIA).
ThreatConsumer (or threat-intel)Real-world exploitation maturity, remediation level, report confidence.
EnvironmentalConsumerDeployment context — security requirements, modified base metrics for my env.
SupplementalConsumer (CVSS 4.0 only)Safety, automatable, recovery, value-density. Informational; never alters score.

The vendor publishes a Base score and that is it. Everything else — whether a CVE is being exploited in the wild today, what compensating controls reduce its impact in your environment, what the package fix path looks like, whether CISA is requiring a federal-deadline patch — is consumer territory. Capturing those metric groups as structured data is what makes "we lowered impact from 9.8 to 4.2 because the affected service is air-gapped" something a reviewer can audit instead of just a paragraph of prose.

The same logic applies to EPSS and KEV. They are decision inputs that influence prioritisation but live outside the original CVSS rubric. Pinning them to a typed field on the finding (with the publication date and catalog membership status) is what lets diff and reporting tools say "this finding's risk profile changed because EPSS jumped from 0.04 to 0.97 overnight."

What HDF now captures

Five fields are now optional on Evaluated_Requirement and one (cvss) is also optional on Standalone_Override.

FieldTypeLocationPopulated byPurpose
cvss[]Cvss[]Evaluated_Requirement + Standalone_OverrideNessus, Twistlock, GrypeCVSS records (Base + optional Threat / Environmental / Supplemental). One entry per CVE.
epssEpssEvaluated_Requirement(consumer enrichment — no converter)FIRST.org exploit-probability score + percentile + publication date.
kevKevEvaluated_Requirement(consumer enrichment — no converter)CISA Known Exploited Vulnerabilities catalog membership and federal due-date.
cwe[]string[]Evaluated_RequirementNessus, Twistlock, GrypeCommon Weakness Enumeration IDs (CWE-79, CWE-89, …) for classification + control mapping.
affectedPackages[]Affected_Package[]Evaluated_RequirementTwistlock, GrypePackage name + version + ecosystem + optional CPE / PURL / fixed-in version.

cvss on Standalone_Override is the override's own scoring rubric — it captures why the override changed the impact, not the original vendor score (which stays on the underlying finding's cvss[]).

Three converters were updated in this release:

  • Nessus — emits cvss[] (CVSS 2.0 / 3.x / 4.0 as the plugin advertises), cwe[] from plugin metadata, and CVE IDs.
  • Twistlock — emits cvss[], cwe[], and affectedPackages[] for every container-image CVE finding.
  • Grype — emits cvss[], cwe[], and affectedPackages[] with CPE + PURL for OS and language-ecosystem packages.

EPSS and KEV have no converter source today because typical scanners do not embed those fields. They are intended to be filled in by enrichment pipelines (FIRST.org EPSS API + CISA KEV JSON feed).

Field reference at a glance

For the full field-by-field reference see the auto-generated Embedded Primitives section on the HDF Results page. The short version:

Cvss carries a required version plus at least one substantive metric or score (the schema's anyOf guard rejects a content-free {version} block). Beyond version, every field is optional so a single primitive can represent vendor-supplied Base data, consumer-supplied Threat / Environmental / Supplemental deltas on an override, or a fully-resolved effective score. Available fields: source (CVE ID or similar), baseVector + baseScore + baseSeverity, threatVector + threatScore, environmentalVector + environmentalScore, supplementalVector (CVSS 4.0 only — never alters score), and a final computedScore + computedSeverity. Some vendor tools emit a final baseScore without the vector that derived it; the schema permits this so the score is captured structurally rather than lost. When baseVector is absent the score cannot be recomputed or decomposed by consumers.

Epss carries three required fields: score (0.0–1.0 exploit probability), percentile (0.0–1.0 rank), and date (ISO 8601 publication date — FIRST.org republishes daily).

Kev carries a required inKev boolean. When inKev=true, dateAdded and dueDate (the CISA BOD 22-01 federal patch deadline) are conditionally required.

Affected_Package requires at least one of: (name + version + ecosystem), purl alone, or cpe alone — PURL and CPE encode name/version/ecosystem implicitly so either may stand on its own. ecosystem is enumerated (npm | pypi | rpm | deb | maven | gem | nuget | go | cargo | generic); cpe is a CPE 2.3 URI; purl is a Package URL; fixedInVersion is optional.

cwe[] is just a list of CWE IDs in CWE-N form (no leading zeros) — ["CWE-79", "CWE-22"].

Example: a full CVE finding

A Log4Shell (CVE-2021-44228) finding emitted by Grype, populated with downstream EPSS + KEV enrichment. Realistic data from public sources (NVD Base score, the CISA KEV entry for CVE-2021-44228, an EPSS snapshot near the disclosure date).

json
{
  "id": "GHSA-jfh8-c2jp-5v3q",
  "title": "Log4Shell: Remote code execution in Apache Log4j 2",
  "descriptions": [
    {
      "label": "default",
      "data": "Apache Log4j2 versions 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker-controlled LDAP and other JNDI related endpoints."
    }
  ],
  "severity": "critical",
  "impact": { "value": 0.98 },
  "result": {
    "status": "failed",
    "codeDesc": "log4j-core 2.14.1 matches CVE-2021-44228",
    "startTime": "2026-05-26T14:22:13Z"
  },
  "cvss": [
    {
      "version": "3.1",
      "source": "CVE-2021-44228",
      "baseVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "baseScore": 10.0,
      "baseSeverity": "critical"
    }
  ],
  "epss": {
    "score": 0.97532,
    "percentile": 0.99987,
    "date": "2026-05-26"
  },
  "kev": {
    "inKev": true,
    "dateAdded": "2021-12-10",
    "dueDate": "2021-12-24",
    "notes": "Apache Log4j2 contains a vulnerability where JNDI features do not protect against attacker-controlled LDAP and other JNDI related endpoints."
  },
  "cwe": ["CWE-20", "CWE-400", "CWE-502"],
  "affectedPackages": [
    {
      "name": "org.apache.logging.log4j:log4j-core",
      "version": "2.14.1",
      "ecosystem": "maven",
      "cpe": "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
      "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
      "fixedInVersion": "2.17.1"
    }
  ],
  "tags": {
    "nist": ["SI-2", "RA-5"]
  }
}

A few things worth noticing:

  • cvss[] is an array because a single finding can match multiple CVEs (Twistlock and Grype both emit this case for OS packages — one binary often vulnerable to several CVEs at once).
  • The vendor-supplied Base data is the only thing in cvss[0] here. No threatVector, no environmentalVector. This is what comes straight off the scanner.
  • epss.date is the date FIRST.org published the score, not when the CVE was disclosed. EPSS scores shift daily as new exploitation evidence is observed; pinning the date lets reporting tools say "EPSS as of 2026-05-26" rather than just "EPSS=0.975."
  • kev.dueDate here is in the past — the federal patching deadline elapsed years ago. The schema does not enforce ordering between dateAdded and dueDate because CISA occasionally adjusts dates after the fact, but a reporting tool can surface overdue items.
  • cwe[] has three entries because Log4Shell genuinely maps to multiple weaknesses (input validation, resource exhaustion via JNDI lookup, deserialisation of untrusted data). This is normal.
  • affectedPackages[0].fixedInVersion is 2.17.1 — the version that closes the issue including the related CVE-2021-45046 / CVE-2021-45105 follow-ups, not the original 2.15.0 patch.

Example: auditable risk adjustment

The Environmental and Threat metric groups exist so that consumers can record their view of a vulnerability without erasing the vendor's. The structured cvss slot on Standalone_Override is the place for that.

Scenario: the CVE-2021-44228 finding above lands on a Log4j install that runs in an isolated build agent — no inbound network, no untrusted input, monitored by EDR. The risk team reduces effective impact from critical to medium but wants an auditable rubric for that decision.

json
{
  "$schema": "https://mitre.github.io/hdf-libs/schemas/hdf-amendments/v3.4.0",
  "name": "Log4Shell environmental risk adjustment",
  "overrides": [
    {
      "type": "riskAdjustment",
      "requirementId": "GHSA-jfh8-c2jp-5v3q",
      "reason": "Log4j component is on an internal build agent with no inbound network exposure and no untrusted input handling. EDR coverage and segmented VLAN provide defence-in-depth. Compensating controls reviewed and accepted by SecOps on 2026-05-26.",
      "appliedBy": {
        "identifier": "secops-review-board@example.org",
        "type": "email"
      },
      "appliedAt": "2026-05-26T15:00:00Z",
      "expiresAt": "2026-08-26T15:00:00Z",
      "impact": {
        "value": 0.42
      },
      "cvss": {
        "version": "3.1",
        "source": "CVE-2021-44228",
        "baseVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
        "baseScore": 10.0,
        "baseSeverity": "critical",
        "environmentalVector": "MAV:A/MAC:H/MPR:H/MUI:R/CR:M/IR:M/AR:M",
        "environmentalScore": 4.2,
        "threatVector": "E:F/RL:O/RC:C",
        "threatScore": 7.4,
        "computedScore": 4.2,
        "computedSeverity": "medium"
      }
    }
  ]
}

What this captures:

  • The vendor Base score of 10.0 is preserved verbatim in baseVector + baseScore. The override does not pretend Log4Shell is intrinsically less severe — it documents that this particular deployment has compensating controls.
  • environmentalVector records the rubric: Modified Attack Vector dropped from Network to Adjacent (the build agent has no internet), Modified Attack Complexity raised to High, Modified Privileges Required raised to High, Modified User Interaction set to Required, and the CIA Requirements set to Medium (this is a build agent, not customer-data infrastructure).
  • threatVector adds the exploit-maturity context: E:F (Functional exploit code exists), RL:O (Official patch available — Log4j 2.17.1 has been released for years), RC:C (Confirmed). Even in isolation, the exploit is mature; that has to be acknowledged in the rubric.
  • computedScore: 4.2 is the post-Environmental score. The override sets impact.value: 0.42cvss.computedScore / 10.0. This is the soft consistency rule validators check.
  • expiresAt is mandatory — there are no permanent overrides. SecOps will revisit this in 90 days.

The soft consistency rule is intentional: validators MAY warn when impact.value and cvss.computedScore / 10.0 diverge by more than ~0.05, but they MUST NOT error. Sometimes operators want to round, sometimes they want to apply additional non-CVSS qualitative judgement. The schema documents the convention without enforcing it.

Workflow: scan → draft → enrich → apply

The hdf amend family of commands implements the end-to-end consumer-enrichment workflow without requiring any bespoke CVE-specific subcommand surface. The cvss block is simply an optional field on any riskAdjustment override entry — the existing draft / create / apply plumbing carries it through.

1. Generate a draft from your scan

hdf amend draft walks the requirements in a results document, emits one incomplete override stub per match, and marks the document _draft: true so hdf amend apply will refuse it until the author completes each entry. For CVE-ecosystem riskAdjustments, filter for failed findings and narrow to CVE-shaped IDs:

bash
hdf amend draft \
  --from scan.hdf.json \
  --type riskAdjustment \
  --status failed \
  --select CVE \
  --expires 90d \
  -o risk-adjustments.draft.json

The draft contains one stub per matching requirement: requirementId, type, appliedAt, expiresAt, and an empty impact: {value: 0.0} field. When the source requirement is a CVE-ecosystem finding (its own cvss[] array is non-empty — emitted by Grype, Twistlock, Nessus), the stub also includes a consumer-owned cvss scaffold with version and source prefilled from the finding and the Threat / Environmental / Supplemental / computed-score fields left blank for the author. Non-CVE riskAdjustments do not get a cvss scaffold — most authoring traffic uses riskAdjustment for non-CVE risk decisions and prefilling there would be misleading.

2. Enrich each override with consumer-owned cvss data

Edit the draft (or pipe through a script / AI agent) so each riskAdjustment carries a cvss block scoped to the consumer-owned fields only. The schema's Cvss primitive is relaxed for amendments: only version is required, and the doc must include at least one substantive metric or score. Base scoring belongs to the finding's own cvss[] and is merged at apply time — do not duplicate baseVector / baseScore here.

A typical CVE-ecosystem override (matching the rubric pattern from the previous section):

json
{
  "type": "riskAdjustment",
  "requirementId": "GHSA-jfh8-c2jp-5v3q",
  "reason": "Log4j component is on an isolated build agent. Compensating controls reviewed 2026-05-26.",
  "appliedBy": { "type": "email", "identifier": "secops@example.org" },
  "appliedAt": "2026-05-26T15:00:00Z",
  "expiresAt": "2026-08-26T15:00:00Z",
  "impact": { "value": 0.42 },
  "cvss": {
    "version": "3.1",
    "source": "CVE-2021-44228",
    "environmentalVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/MAV:A/MAC:H/MPR:H/MUI:R/CR:M/IR:M/AR:M",
    "environmentalScore": 4.2,
    "threatVector": "E:F/RL:O/RC:C",
    "threatScore": 7.4,
    "computedScore": 4.2,
    "computedSeverity": "medium"
  }
}

Vector strings carry the full CVSS vector (Base metrics + modification metrics), not just the deltas. This matches FIRST.org's grammar and is what hdf amend create validates against.

3. Validate and finalise

hdf amend create reads the completed draft (with _draft removed) via --from and runs the full validation pass:

bash
hdf amend create --from risk-adjustments.draft.json -o waivers.json

This step performs three checks beyond the foundation bead's normalisation:

  1. Syntactic vector validation. Each baseVector / threatVector / environmentalVector / supplementalVector is parsed against the FIRST grammar for the declared version. A malformed vector fails the create with a precise error (override for CVE-2021-44228: invalid environmentalVector: ...).
  2. Schema validation. The relaxed Cvss primitive's anyOf substantive-metric-or-score requirement applies — an empty cvss: {version: "3.1"} block fails.
  3. Soft consistency warning. If both impact.value and cvss.computedScore are present and disagree by more than 0.05 on the normalised scale, a warning is printed to stderr. The document still passes — see the soft consistency rule above.

4. Apply across results

The amendments document is reusable across any scan that finds the same CVE. Merging is just hdf amend apply:

bash
hdf amend apply --results scan.hdf.json --amendments waivers.json -o merged.hdf.json

The merged document's affected requirements gain statusOverrides pointing at the amendment entries, and a future read-side resolver will materialise an effectiveCvss on the requirement (Base from the finding + consumer deltas from the override). That resolver work is tracked separately (the effectiveCvss field on Evaluated_Requirement). Until it lands, downstream consumers can read the override's cvss.computedScore directly to surface the post-enrichment view.

AI-suggested enrichment

Environmental enrichment is exactly the kind of repetitive context-application task that AI agents are well-suited to: read the affected component metadata, look at the system's network posture, infer Modified Attack Vector and CIA Requirements, propose an Environmental vector and a computed score. Reviewers then accept, reject, or edit.

The structured cvss slot makes that workflow auditable:

  • The AI agent fills in environmentalVector and computedScore (and optionally threatVector / threatScore).
  • The override carries appliedBy provenance — the agent's identity, the run ID, the prompt version.
  • The reviewer reading the override sees the exact rubric used. Disagreement is concrete: "I would set MAV:N, not MAV:A, because we surface this service over a public bastion" — not abstract.
  • Downstream tools can recompute computedScore from baseVector + environmentalVector + threatVector and flag any mismatch.

The Identity primitive's type enum includes agent for exactly this case: an AI/LLM agent acting with autonomy. It is deliberately distinct from system (deterministic non-interactive automation — CI jobs, cron, scanners) so auditors can apply AI-specific scrutiny — "an LLM proposed this" is a different provenance claim than "a deterministic job emitted this" — and so tooling can enforce policies like mandatory human review for agent-signed riskAdjustments above a severity threshold, or AI-source disclosure under frameworks like the EU AI Act and NIST AI RMF. Put the concrete agent, model, and version in identifier, and elaborate in description.

Recommended pattern for an AI-applied override:

json
{
  "appliedBy": {
    "identifier": "cve-enrichment-bot/v0.4.2",
    "type": "agent",
    "description": "OpenAI GPT-4 via internal CVE-enrichment tool; outputs require human review before activation."
  },
  "appliedAt": "2026-05-26T15:00:00Z",
  "expiresAt": "2026-05-27T15:00:00Z"
}

Note the short expiresAt window — AI-proposed overrides expire quickly until a human approves them, at which point a fresh override with a longer window and a human appliedBy replaces it.

Recomputing scores

hdf-libs does not include a CVSS calculator. We deliberately do not ship one — CVSS scoring has version-specific subtleties, FIRST publishes reference implementations, and validating our own arithmetic against four major spec versions is a tarpit. Instead:

  • Syntactic validation: hdf-utilities exports parseCvssVector (TypeScript) and the equivalent Go helper, which check vector grammar against the version-specific token set without computing a score. Use these in pipelines that ingest external CVSS strings.
  • Score computation: use one of the FIRST-aligned community calculators on the consumer side. Recommended:
    • Go: pandatix/go-cvss — covers v2, v3.0, v3.1, v4.0.
    • TypeScript / JavaScript: cvss on npm for v3.x. For v4.0, the FIRST reference JavaScript implementation is the most reliable choice while the npm ecosystem catches up.
  • Verification: feed the recorded baseVector + threatVector + environmentalVector through the calculator and confirm the result matches computedScore. Mismatches mean either the vector is malformed or the recorded score was edited by hand — both worth flagging.

Vector parsing rejects malformed input early; this is the first thing to call when accepting CVSS data from any source you do not control.

Affected packages: CPE vs PURL

Affected_Package lets you populate either or both of cpe and purl. Both identify the same package; they exist in parallel because no single identifier scheme has won.

IdentifierOriginStrengthsWeaknesses
CPE 2.3NIST / NVDUniversal across operating-system, application, and hardware products. NVD-indexed.Sparse coverage for language-ecosystem packages. Awkward for non-OS scanners.
PURLpurl-spec (Phil Estes)First-class language-ecosystem support (npm, pypi, rpm, deb, maven, gem, nuget, go, cargo).Less coverage for hardware / OS-vendor SKUs. Multiple type-namespacing conventions in flight.

Practical guidance:

  • For OS-vendor packages (RPM, DEB), populate both. Scanners like Grype and Trivy emit both natively.
  • For language-ecosystem packages (npm, pypi, maven, …), populate purl and leave cpe empty.
  • Always populate name + version + ecosystem. cpe and purl are convenient; the three required fields are what diff matching and report rollups depend on.

The diff engine's package-match strategy uses PURL first, then CPE, then (ecosystem, name) as a final fallback. Populating PURL whenever the scanner emits it improves matching across re-scans.

Converter-specific notes

Each of the three updated converters emits slightly different shapes based on what the source tool reports.

Nessus

Tenable plugins emit one or more CVSS records (some plugins still ship CVSS 2.0; modern ones ship 3.x or 4.0). The Nessus converter maps:

  • cvss[] — one entry per CVSS version emitted by the plugin. The same finding may carry CVSS 2.0 + CVSS 3.1 entries side by side; both are preserved with their respective version field. source is set to the CVE ID when the plugin advertises one, otherwise the Nessus plugin ID.
  • cwe[] — extracted from plugin metadata when present. Multiple CWE IDs are common for OWASP-style web findings.
  • affectedPackages[] — not populated; Nessus is a host scanner that reports findings against the host, not specific package names. Use the target block on the surrounding Evaluated_Baseline to identify the host.

If a plugin reports a CVSS score but no vector, the converter constructs the minimal valid Base vector for that version and the score is preserved as baseScore. This is rare in practice — modern Tenable feeds include vectors — but keeps legacy plugin output schema-valid.

Twistlock

Twistlock (Prisma Cloud) container scans emit per-CVE findings with rich package data. The converter maps:

  • cvss[] — one entry per CVE, version is whatever the Twistlock feed publishes (typically 3.x).
  • cwe[] — extracted from the cwe array on the Twistlock vulnerability record.
  • affectedPackages[] — one entry per package the CVE applies to. ecosystem is mapped from the Twistlock package type (nodejsnpm, pythonpypi, etc.) and fixedInVersion is populated when Twistlock identifies a fix.

A single container-image finding can have several CVEs and several affected packages — Twistlock typically reports per-CVE rows, so each Evaluated_Requirement represents one CVE × one image but may list multiple affected packages when the same CVE applies to siblings (think libssl + libcrypto from the same OpenSSL package).

Grype

Anchore Grype is the most prolific emitter of structured CVE-ecosystem data. The converter maps:

  • cvss[] — Grype reports every CVSS record that NVD or the relevant vendor advisory carries for a CVE. It is common to see CVSS 2.0 + 3.1 + 4.0 entries for the same CVE.
  • cwe[] — extracted from NVD weakness data when present.
  • affectedPackages[] — populated for every match. CPE comes from NVD; PURL is constructed by Grype from the ecosystem-native package metadata it scanned.

Grype is the recommended source if you need rich, well-typed CVE-ecosystem data and have no other constraints.

EPSS and KEV enrichment

No converter populates epss or kev today because scanners do not embed those fields in their native output. The typical enrichment pipeline:

  1. Run your converter to produce HDF results.
  2. For each Evaluated_Requirement with a cvss[].source that is a CVE ID, look up the CVE in:
    • FIRST.org EPSS API (https://api.first.org/data/v1/epss?cve=CVE-…) for score, percentile, date.
    • CISA KEV JSON feed (https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json) for inKev, dateAdded, dueDate, notes.
  3. Patch the structured fields into the HDF document.

Both feeds are public, refresh daily, and have stable formats. A simple enrichment pass is on the order of 50–100 lines of code per language.

Diff and parser support

The hdf-diff engine recognises CVE-ecosystem fields:

  • CVE matching: requirements sharing a cvss[].source CVE identifier match across scans even when their requirement IDs differ (common when one scanner uses GHSA IDs and another uses CVE IDs).
  • Package matching: when affectedPackages[] is populated, the matcher uses PURL → CPE → (ecosystem, name) to align findings.
  • Typed diff handlers: changes to cvss, epss, kev, cwe, and affectedPackages produce structured diff entries instead of generic JSON-path diffs.

hdf-parsers flattens these fields into columns for tabular export:

  • cvss_base_score, cvss_computed_score (first entry of cvss[]; cvss_base_score also falls back to legacy tags.cvss_base_score)
  • epss_score, epss_percentile
  • kev_in_kev
  • cwe (semicolon-joined)
  • affected_packages (semicolon-joined name@version pairs)

This keeps spreadsheet workflows (CSV export, Excel pivots) usable without consumers having to parse the nested JSON.

hdf-validators includes acceptance tests covering all five fields including the conditional Kev.dateAdded / dueDate requirement when inKev=true, the Cvss vector-pattern grammar, and the bounded ranges on Epss.score / percentile.

Worked diff example

Consider two successive Grype scans of the same image — 2026-05-01 and 2026-05-26. The same log4j-core 2.14.1 finding appears in both, but EPSS scoring shifted in the interim:

FieldScan A (2026-05-01)Scan B (2026-05-26)
cvss[0].baseScore10.010.0
epss.score0.943120.97532
epss.percentile0.998210.99987
kev.inKevtruetrue
affectedPackages[0].fixedInVersion2.17.12.17.1

hdf-diff matches the requirements via shared cvss[0].source (CVE-2021-44228) and emits a typed diff entry showing epss.score and epss.percentile changed. Reporting tools can surface this as "exploit probability rose 3 percentage points over 25 days" rather than as a generic JSON-path diff, which makes prioritisation conversations concrete.

If the second scan had also included a freshly-published Threat-metric enrichment, the diff entry would carry the added threatVector and reviewers could re-derive why the score moved.

CWE and NIST control mapping

The cwe[] field is independent of the CVSS slot — it is just a list of Common Weakness Enumeration identifiers. CWE matters for two reasons:

  1. Classification: CWE answers "what kind of vulnerability is this?" (input validation, authentication bypass, memory safety, …). CVSS answers "how bad is it?" The two are complementary.
  2. NIST control mapping: hdf-mappings ships a CWE → NIST SP 800-53 control mapping. Converters with cwe[] data feed tags.nist indirectly via that mapping. Log4Shell mapping CWE-20 and CWE-502 lands on SI-10 (information input validation) and SC-8 (transmission confidentiality and integrity), among others.

If you maintain a converter that did not previously emit CWE, populate cwe[] from whatever metadata the source tool provides. Even a single CWE ID materially improves downstream tags.nist coverage.

Format: CWE-N with no leading zeros (CWE-79, not CWE-079). This matches the MITRE catalog convention and the hdf-mappings lookup tables.

Migration from tag-based output

Older HDF documents (pre-v3.3.0) carry CVSS, CVE, CWE, and KEV information in free-form tags:

json
{
  "tags": {
    "cve": "CVE-2021-44228",
    "cvss_base_score": "10.0",
    "cvss_base_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
    "cwe": "CWE-20,CWE-400",
    "kev": "true"
  }
}

Consumers reading these tags will keep working. The three updated converters (Nessus, Twistlock, Grype) emit the structured fields in addition to the legacy tags for one release cycle.

The roadmap:

  • v3.3.0: structured fields added; legacy tags still emitted.
  • v3.4.0: deprecation notice in changelog; legacy tags still emitted.
  • v3.5.0: legacy tag emission removed. Consumers should be reading structured fields by then.

If you maintain a downstream tool that reads tags.cvss_base_score, plan to migrate to cvss[0].baseScore (or aggregate across cvss[] if your tool can handle multi-CVE findings). For the migration window, prefer the structured field when present and fall back to the tag.

Edge cases and gotchas

A handful of behaviours that catch operators off-guard:

  • Multiple CVEs per finding: scanners aggregate. Twistlock might emit one finding for openssl@1.1.1k that lists three CVEs. The HDF representation is cvss[] with three entries, not three separate Evaluated_Requirement objects. Diff tooling matches on the cvss[].source set; reports usually flatten one-row-per-CVE if needed.
  • CVSS 2.0 with no version prefix: CVSS 2.0 vector strings have no CVSS:2.0/ prefix. The schema's regex accepts both prefixed and prefix-less forms; the converter and parseCvssVector helper handle either input.
  • EPSS scores drift daily: do not cache EPSS values indefinitely. The Epss.date field exists specifically so downstream tools can flag stale data ("EPSS from 90 days ago — re-enrich before relying on it").
  • kev.inKev=false is meaningful: it is not the same as omitting the kev field. inKev=false says "we checked, this CVE is not in the KEV catalog as of the enrichment run." Omitting kev says "nobody checked." Reporting tools should distinguish.
  • kev.dueDate before dateAdded: the schema does not enforce ordering. CISA has historically adjusted dates after the fact (regulatory clarifications, policy retraction). Validators do not complain; reports should sanity-check.
  • PURL ecosystem mismatch: PURL types and the Affected_Package.ecosystem enum are related but not identical. pkg:npm/... matches ecosystem: "npm", but pkg:rpm/redhat/... is just rpm in HDF; the namespace (redhat, centos, …) lives inside the PURL itself.
  • CWE rollups: a finding may legitimately have 3+ CWE entries. Do not assume "one finding = one weakness." Log4Shell, the canonical example, maps to CWE-20, CWE-400, and CWE-502.
  • Soft consistency between impact.value and cvss.computedScore: validators warn on mismatches larger than ~0.05. This is intentional — sometimes operators want to apply qualitative judgement beyond the CVSS rubric.

Worked enrichment pipeline output

A minimal end-to-end pipeline run — Grype output → HDF converter → EPSS + KEV enrichment — for an OpenSSL CVE finding:

json
{
  "id": "CVE-2022-3602",
  "title": "X.509 Email Address 4-byte Buffer Overflow",
  "descriptions": [
    {
      "label": "default",
      "data": "A buffer overrun can be triggered in X.509 certificate verification, specifically in name constraint checking."
    }
  ],
  "severity": "high",
  "impact": { "value": 0.75 },
  "result": {
    "status": "failed",
    "codeDesc": "openssl 3.0.4 matches CVE-2022-3602",
    "startTime": "2026-05-26T18:00:00Z"
  },
  "cvss": [
    {
      "version": "3.1",
      "source": "CVE-2022-3602",
      "baseVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H",
      "baseScore": 7.5,
      "baseSeverity": "high"
    }
  ],
  "epss": {
    "score": 0.00124,
    "percentile": 0.45821,
    "date": "2026-05-26"
  },
  "kev": {
    "inKev": false
  },
  "cwe": ["CWE-120"],
  "affectedPackages": [
    {
      "name": "openssl",
      "version": "3.0.4",
      "ecosystem": "rpm",
      "cpe": "cpe:2.3:a:openssl:openssl:3.0.4:*:*:*:*:*:*:*",
      "purl": "pkg:rpm/redhat/openssl@3.0.4?arch=x86_64",
      "fixedInVersion": "3.0.7"
    }
  ]
}

This is a useful contrast to the Log4Shell example earlier:

  • The Base score is 7.5 (high) but EPSS is only 0.00124 — very low exploit probability. The original "Heartbleed 2" panic faded once exploitation proved difficult.
  • kev.inKev: false is an active statement, populated by the enrichment pass. Tooling that sees a missing kev field cannot make this claim.
  • severity is high (mapped from CVSS) but a prioritisation engine that weights EPSS could rank this lower than several CVSSv3 mediums with high EPSS scores. Both values are now in the structured slots; the engine has what it needs.

Field placement cheat-sheet

text
hdf-results.json
└── baselines[].requirements[]                ← Evaluated_Requirement
    ├── cvss[]            ← vendor + optional consumer scoring (one per CVE)
    ├── epss              ← FIRST.org exploit probability
    ├── kev               ← CISA KEV catalog membership
    ├── cwe[]             ← CWE IDs (CWE-N format)
    └── affectedPackages[]    ← name + version + ecosystem (+ CPE / PURL)

hdf-amendments.json
└── amendments[]                              ← Standalone_Override
    └── cvss              ← rubric for THIS override (consumer's Environmental + Threat)

The cvss on the finding documents what the vendor said. The cvss on the override documents what the consumer decided and why. Both belong in the audit trail; neither replaces the other.

See also

Released under the Apache 2.0 License.