Skip to content

feat: typed CVSS score-map constructors - #802

Open
gronke wants to merge 1 commit into
csaf-rs:mainfrom
gronke:feat/cvss-score-map-constructors
Open

feat: typed CVSS score-map constructors#802
gronke wants to merge 1 commit into
csaf-rs:mainfrom
gronke:feat/cvss-score-map-constructors

Conversation

@gronke

@gronke gronke commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The typed CVSS accessors on ContentTrait (#715) cover the read direction for the untyped cvss_v2/cvss_v3/cvss_v4 score maps. The write direction still forces consumers to hand-build serde_json maps. This adds the construction counterpart:

  • cvss_v2_to_score_map / cvss_v3_to_score_map / cvss_v4_to_score_map render a typed cvss-rs model into the JSON object the property carries. (The Cvss enum itself has no Serialize impl; the per-version structs do, and their serde shape is exactly the FIRST schema's.)
  • cvss_v2_score_map_from_vector / cvss_v3_score_map_from_vector / cvss_v4_score_map_from_vector parse a vector string and recompute the base score and severity band from the parsed metrics, so the mandatory consistency tests 6.1.9/6.1.10 hold by construction — an emitted document can never disagree with its own vectors.

The cvss module becomes public for this; the validate_* helpers it already carried become public API with it (happy to scope those pub(crate) instead if you'd rather keep the surface minimal).

Motivation

A downstream SBOM/vulnerability tool emitting CSAF 2.0 currently rebuilds these JSON maps metric-by-metric to keep 6.1.9/6.1.10 green. With the typed models already in the tree, the crate can offer that construction once, for everyone.

Notes

  • Severity bands follow the FIRST specifications: v2 Low/Medium/High at 4.0/7.0; v3 and v4 None/Low/Medium/High/Critical at 0.0/4.0/7.0/9.0.
  • A vector whose base metrics are incomplete keeps its parsed score as-is (calculated_base_score returns None).
  • Error type is the shared cvss_rs::ParseError from the vector FromStr impls.

The typed accessors on ContentTrait (csaf-rs#715) cover the read direction; construction still goes through the untyped serde_json maps the CSAF schemas declare for cvss_v2/cvss_v3/cvss_v4.
Add the write direction:

- cvss_v2_to_score_map / cvss_v3_to_score_map / cvss_v4_to_score_map render a typed cvss-rs model into the JSON object the property carries (the Cvss enum itself has no Serialize impl; the per-version structs serialize to exactly the FIRST schema shape).
- cvss_v2/cvss_v3/cvss_v4_score_map_from_vector parse a vector string and recompute the base score and severity band from the metrics, so the mandatory consistency tests 6.1.9/6.1.10 hold by construction.

This makes the cvss module public; the validate_* helpers it already carried become public API with it.
@gronke
gronke requested a review from a team as a code owner August 12, 2026 10:31
Comment thread csaf-rs/src/cvss/mod.rs

/// The CVSS v3.x severity band of a base score.
fn v3_severity(score: f64) -> cvss_rs::v3::Severity {
use cvss_rs::v3::Severity;

@aschierl-xitaso aschierl-xitaso Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to use map_score_to_severity here, this is the existing implementation used for the tests and also cares about correct rounding.
(Looking at that, I just realized that test 6.1.9 does not yet check the severity for CVSS 2.0, #806, so the corresponding function for 2.0 does not yet exist)

Comment thread csaf-rs/src/cvss/mod.rs

/// The CVSS v2.0 severity band of a base score.
fn v2_severity(score: f64) -> cvss_rs::v2_0::Severity {
use cvss_rs::v2_0::Severity;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently in discussion, but the official first.org JSON schema for CVSS v2.0 does not include a "severity" field, so CSAF will likely reject documents that contain it. Severity will only be allowed starting with CVSS v3.

@gronke

gronke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for both comments, @aschierl-xitaso. Here is where I landed before pushing the next commits.

CVSS v2.0 severity

I checked the schema at https://www.first.org/cvss/cvss-v2.0.json to ground the decision. It defines 19 properties with version, vectorString and baseScore required; severity is not among them. As written the schema is draft-04 without additionalProperties: false, so the field is unspecified rather than strictly rejected, but that is thin ice to build on while the CSAF discussion is ongoing.

I will therefore drop the severity assignment from cvss_v2_score_map_from_vector and keep the recomputed baseScore only. cvss_v2_to_score_map on a caller-built model with severity: None already omits the key, so the crate stays within the FIRST v2 shape by default. If the discussion settles in favor of allowing severity, adding it back is a one-line change.

Severity banding for v3/v4

Agreed on reusing map_score_to_severity; it also gives the constructors the same scaled-integer rounding the tests use. The constructor and the validator must agree on bands, and keeping two implementations invites exactly the drift this PR tries to eliminate. It returns the unified cvss_rs::Severity, so v3/v4 get a small unified-to-version conversion (the inverse of to_unified in cvss/v3.rs). For v2 no mapping is needed once severity emission is dropped; when the v2 severity check for test 6.1.9 (#806) lands, its banding can live next to map_score_to_severity and this constructor can adopt it then.

Missing required version

While grounding the severity question in the schemas I noticed a gap in the current commits: all four FIRST schemas list version as required, but CvssV2 and CvssV4 in cvss-rs 0.4.0 carry no such field - it lives on the internally tagged Cvss enum, which is Deserialize-only. The v2/v4 score maps therefore serialize without version and would fail test 6.1.8 and Cvss::deserialize despite carrying correct scores. The next commits inject the constant into the rendered v2/v4 maps ("2.0"/"4.0"); v3 is unaffected on the vector path because CvssV3::from_str records the version from the mandatory prefix. Longer term the field arguably belongs in cvss-rs itself.

Public surface of mod cvss

One question back to you: the PR currently flips pub(crate) mod cvss to pub mod cvss, which also exposes the existing validation helpers (validate_*, check_*, the create_* error constructors, deserialize_cvss, map_score_to_severity) as public API. Would you rather keep the module fully public, or should I re-scope the validation internals to pub(crate) and export only the six new constructors? I slightly prefer the smaller surface, but the helpers are genuinely useful for custom validators, so I am happy with either.

@tziemek

tziemek commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

I wouldn't expose the CVSS functionality here. Long term, these things should be part of cvss-rs itself.

@gronke

gronke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

I wouldn't expose the CVSS functionality here. Long term, these things should be part of cvss-rs itself.

Opened two (of a series of) PRs, proposing this to cvss-rs:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants