Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a formal semver 2.0.0 version type #371

Open
wants to merge 16 commits into
base: feature-PR371-semver2.0
Choose a base branch
from

Conversation

darakian
Copy link

@darakian darakian commented Dec 9, 2024

First crack at adding a formal version type in response to #362 (comment) Any others which are agreed upon should be spun up in their own PRs so that conversations in the PRs can be kept on topic

Happy to expand this if people think the full semver spec should be in this repo as well. I went back and forth on that.

Another thought is that maybe this should be a retroactive definition of the semver type. That would likely be breaking for some of the current records though.

The goal here is to have strict validation provided by cve services

First crack at adding a formal version type in response to
CVEProject#362 (comment)
Any others which are agreed upon should be spun up in their own PRs so that conversations in the PRs can be kept on topic

Happy to expand this if people think the full semver spec should be in this repo as well. I went back and forth on that.
@sei-vsarvepalli
Copy link
Contributor

I recommend you resubmit the PR with a change in both schema/docs/CVE_Record_Format_bundled_adpContainer.json and schema/docs/CVE_Record_Format_bundled_cnaContainer.json focusing on the version field. This PR with change to just example.md will not be useful without a schema based validation, as example.md is only a human friendly markdown.

It will be best to target a JSON schema validation instead of programmatically verifying versions when they are specific like this scenario with a clear semver-2.0.0 compliance being tested.

Secondly, we should follow/extend the current schema model and extend it to satisfy this need instead of a completely new JSON schema fields like exclusiveUpperBound - it is not really as initiative as lessThan

See the current versions.md document which has some examples

https://github.com/CVEProject/cve-schema/blob/main/schema/docs/versions.md

{
  "version": "2.0.0",
  "versionType": "semver",
  "lessThanOrEqual": "2.5.1",
  "status": "affected"
}

The one we don't current have is the exclusiveLowerBound that you mention. However the other examples can be mapped according to the current schema. Potentially we can add as greaterThan boolean field which when present the version field should be treated as ">" instead of ">=" which is the current default "version" field.

So your Example will actually look like

            {
               "versionType": "semver-2.0.0",
               "version": "1.2.3-alpha",
               "lessThan": "2.3.4+build17"
             }
             {
               "versionType": "semver-2.0.0",
               "version": "3.4.5-beta",
               "greaterThan": true,
               "lessThanOrEqual": "4.5.6+assembly88"
             }
             {
               "versionType": "semver-2.0.0",
               "version": "5.6.7-gamma",
             }
             {
               "versionType": "semver-2.0.0",
               "version": "6.7.8-delta",
             }

You need to build a JSON schema validator to work with such data, with versionType frozen with enum as semver-2.0.0 and valid regex to "version", "lessThanOrEqual" and "lessThan" fields require regex validator
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
Finally provide the additional "greaterThan" boolean field perhaps that will treat version as ">" instead of ">=".

@darakian
Copy link
Author

darakian commented Feb 13, 2025

Thank for the comment and I can update the json in this PR once we get to consensus 👍

With respect to the range fields themselves, after seeing you rewrite my example I think it makes sense to simplify and create new fields so that a parser doesn't need to implement conditional logic based on the combination of fields present. I think this will make for simpler and more maintainable code long term. Maybe more people can chime in on this point.

As for the regex it looks like the one you're suggesting is the second of the two provided on semver.org. Albeit with a leading and trailing /.

For documentation's sake here are the two

One with named groups for those systems that support them (PCRE [Perl Compatible Regular Expressions, i.e. Perl, PHP and R], Python and Go).

^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
and

one with numbered capture groups instead (so cg1 = major, cg2 = minor, cg3 = patch, cg4 = prerelease and cg5 = buildmetadata) that is compatible with ECMA Script (JavaScript), PCRE (Perl Compatible Regular Expressions, i.e. Perl, PHP and R), Python and Go.

^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

…for the expressions of "everything under X" or "everything over Y"
@darakian
Copy link
Author

Had a thought hit me about one sided ranges, so I added two more examples

            {
              "versionType": "semver-2.0.0",
              "exclusiveUpperBound": "1.0.0",
            }
            {
              "versionType": "semver-2.0.0",
              "inclusiveLowerBound": "9.0.0",
            }

Which allow someone to express the idea of everything under X or everything over Y. The former of those two is reasonably common.

…-02-20. The status conversation will happen another day
@darakian
Copy link
Author

So your Example will actually look like
...

           {
             "versionType": "semver-2.0.0",
             "version": "3.4.5-beta",
             "greaterThan": true,
             "lessThanOrEqual": "4.5.6+assembly88"
           }

@sei-vsarvepalli where does the greaterThan parameter come from? I'm prepping my comparison of the two representations for thursday's QWG and I can't find a reference to this parameter in the docs. Searching the repo for the string brings back only this PR
https://github.com/search?q=repo%3ACVEProject%2Fcve-schema%20greaterThan&type=code
Am I missing something?

@sei-vsarvepalli
Copy link
Contributor

So your Example will actually look like
...

           {
             "versionType": "semver-2.0.0",
             "version": "3.4.5-beta",
             "greaterThan": true,
             "lessThanOrEqual": "4.5.6+assembly88"
           }

@sei-vsarvepalli where does the greaterThan parameter come from? I'm prepping my comparison of the two representations for thursday's QWG and I can't find a reference to this parameter in the docs. Searching the repo for the string brings back only this PR https://github.com/search?q=repo%3ACVEProject%2Fcve-schema%20greaterThan&type=code Am I missing something?

The field greaterThan does not exit today. It could be an option if you want to maintain the other fields as-is and then add something without having to recreate a new field. Appending a not-required field greaterThan is a non-breaking change, allowing other versionType fields to adopt something similar as we move towards enforcing stricter schema checks.

@darakian
Copy link
Author

darakian commented Feb 27, 2025

Gotcha. Then I guess the difference between the two approaches in schema terms is to add a greaterThan parameter vs adding the inclusiveLowerBound, exclusiveLowerBound, inclusiveUpperBound, exclusiveUpperBound, and exactly parameters.

I've written a pretty simple parser in python for my proposal. It assumes perfect data (validated) and that the data is semver-2.0.0, but I think it gets the point across on the simplicity of parsing. Feel free to play around with it as well by changing the specific parameters in the test. I think I covered all the cases and it can probably be simplified further.

import json

test_json_string = """
{
    "versionType": "semver-2.0.0", 
    "status": "affected", 
    "exclusiveLowerBound": "1.2.3-alpha",
    "inclusiveUpperBound": "2.3.4+build17"
    }
"""

def parse_decoded_json(json):
	if json.get("exactly"):
		return f'= {json.get("exactly")}'

	if json.get("inclusiveLowerBound"):
		lower = f'{">= "+json.get("inclusiveLowerBound")}'
	elif json.get("exclusiveLowerBound"):
		lower = f'{"> "+json.get("exclusiveLowerBound")}'
	else:
		lower = ""

	if json.get("inclusiveUpperBound"):
		upper = f'{"<= "+json.get("inclusiveUpperBound")}'
	elif json.get("exclusiveUpperBound"):
		upper = f'{"< "+json.get("exclusiveUpperBound")}'
	else:
		upper = ""

	return f'{lower}, {upper}'

the_json = json.loads(test_json_string)
print(parse_decoded_json(the_json))

I initially had

lower = f'{">= "+json.get("inclusiveLowerBound") if json.get("inclusiveLowerBound") else "> "+ json.get("exclusiveLowerBound")}'
upper = f'{"<= "+json.get("inclusiveUpperBound") if json.get("inclusiveUpperBound") else "< "+ json.get("exclusiveUpperBound")}'

However that doesn't handled one sided ranges and I wanted to get some code up before today's qwg meeting. I also haven't had time to make a complete comparison parser, but translating the section

if json.get("exactly"):
	return f'= {json.get("exactly")}'

results in something that needs to look like

if json.get("version") and (not json.get("lessThan") or not json.get("greaterThan") or not json.get("lessThanOrEqual")):
		return f'= {json.get("version")}'

as the code needs to be sure that the parameter version stands alone. Having a new parameter with a single function simplifies that logic.

@darakian darakian changed the base branch from main to feature-PR371-semver2.0 February 28, 2025 17:17
@darakian
Copy link
Author

darakian commented Mar 5, 2025

@sei-vsarvepalli the new properties are in as of commit 62db169, however I'm not sure how to express the valid combinations of parameters for the semver 2.0.0 version type. Do I need to do something like a oneOf for the versions block itself? eg.

"versions": {
                    "oneOf": [
                    "type": "array",
                    "description": "Set of product versions or version ranges related to the vulnerability. The versions satisfy the CNA Rules [8.1.2 requirement](https://cve.mitre.org/cve/cna/rules.html#section_8-1_cve_entry_information_requirements). Versions or defaultStatus may be omitted, but not both.",
                    "minItems": 1,
                    "uniqueItems": true,
                    "items": {
                        "type": "object",
                        ...

Where the first option in the one of is the entire current payload and the other is the semver 2.0.0? Maybe you know a simpler approach?

If this is valid then still need to ensure version type is set to semver-2.0.0 for these combinations
@darakian
Copy link
Author

darakian commented Mar 6, 2025

I let this stew for a bit and I think 046dadd is in the right direction. I think its possible to only allow those parameter combinations when the version type is semver 2.0.0, but not sure how to encode that yet.

@darakian
Copy link
Author

darakian commented Mar 12, 2025

@sei-vsarvepalli Ok, so I'm trying to run the tests locally and it seems I need to rebuild dist/cve5validator.js. When attempting to do so though I get Error: Cannot find module '../../docs/CVE_JSON_bundled.json'. It looks like that file got renamed here
a3babe8

However that file doesn't seem to reference the CVE schema file that I've been making edits to, so I'm a little confused how this all works for local testing. Am I missing something basic here? Am I editing the wrong file?

@sei-vsarvepalli
Copy link
Contributor

sei-vsarvepalli commented Mar 12, 2025

@sei-vsarvepalli Ok, som I'm trying to run the tests locally and it seems I need to rebuild dist/cve5validator.js. When attempting to do so though I get Error: Cannot find module '../../docs/CVE_JSON_bundled.json'. It looks like that file got renamed here a3babe8

However that file doesn't seem to reference the CVE schema file that I've been making edits to, so I'm a little confused how this all works for local testing. Am I missing something basic here? Am I editing the wrong file?

What tests are you running? It looks like the starting point of your repo is main which has diverged quite a bit too. Perhaps start with either the develop branch or feature-144-SSVC seem more that target for 5.2.0 where this semver update is expected to be bundled.

Your JSON file is also mangled, the line 323 is missing a comma. When I run test against your branch I get this error

ParserError: Error parsing ./cve-schema/schema/cve-schema.json: missed comma between flow collection entries (324:29)

 321 |                                     {"required": ["exclusiv ...
 322 |                                 ]
 323 |                             }
 324 |                             {
-----------------------------------^

@darakian
Copy link
Author

Thanks for pointing out the comma. Added that in.

I'm trying to run the node validation suite with node validate.js ../tests/valid/semver2-0-0.json. The test is running fwiw. I'm getting the following

jon~/g/!/c/s/s/Node_Validator:add-semver-2.0.0-versionType❯❯❯ node validate.js ../tests/valid/semver2-0-0.json
../tests/valid/semver2-0-0.json is invalid:
[
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/0/maxProperties',
    keyword: 'maxProperties',
    params: { limit: 2 },
    message: 'must NOT have more than 2 properties'
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/1/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/2/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/3/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf',
    keyword: 'oneOf',
    params: { passingSchemas: null },
    message: 'must match exactly one schema in oneOf'
  },
  {
    instancePath: '/cveMetadata/state',
    schemaPath: '#/properties/state/enum',
    keyword: 'enum',
    params: { allowedValues: [Array] },
    message: 'must be equal to one of the allowed values'
  },
  {
    instancePath: '',
    schemaPath: '#/oneOf',
    keyword: 'oneOf',
    params: { passingSchemas: null },
    message: 'must match exactly one schema in oneOf'
  }
]
Summary: Validation FAILED for 1 out of 1 files!

Which made me think that the validation is failing to match a case on the versions section and hence looking into build.js. I could rebase this branch but it doesn't feel like that's an issue here.

@sei-vsarvepalli
Copy link
Contributor

sei-vsarvepalli commented Mar 13, 2025

Thanks for pointing out the comma. Added that in.

I'm trying to run the node validation suite with node validate.js ../tests/valid/semver2-0-0.json. The test is running fwiw. I'm getting the following

jon~/g/!/c/s/s/Node_Validator:add-semver-2.0.0-versionType❯❯❯ node validate.js ../tests/valid/semver2-0-0.json
../tests/valid/semver2-0-0.json is invalid:
[
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/0/maxProperties',
    keyword: 'maxProperties',
    params: { limit: 2 },
    message: 'must NOT have more than 2 properties'
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/1/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/2/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf/3/required',
    keyword: 'required',
    params: { missingProperty: 'version' },
    message: "must have required property 'version'"
  },
  {
    instancePath: '/containers/cna/affected/0/versions/0',
    schemaPath: '#/properties/versions/items/oneOf',
    keyword: 'oneOf',
    params: { passingSchemas: null },
    message: 'must match exactly one schema in oneOf'
  },
  {
    instancePath: '/cveMetadata/state',
    schemaPath: '#/properties/state/enum',
    keyword: 'enum',
    params: { allowedValues: [Array] },
    message: 'must be equal to one of the allowed values'
  },
  {
    instancePath: '',
    schemaPath: '#/oneOf',
    keyword: 'oneOf',
    params: { passingSchemas: null },
    message: 'must match exactly one schema in oneOf'
  }
]
Summary: Validation FAILED for 1 out of 1 files!

Which made me think that the validation is failing to match a case on the versions section and hence looking into build.js. I could rebase this branch but it doesn't feel like that's an issue here.

I think that validator is not right one or right way to use. Use the ajv commands - see bellow shell script as example, you need to first bundle the schema and compile it use ajv - The schema is in multiple parts, once you edit CVE_Record_Format.json, you need to generate the cve-schema.json file below script will do that.

I recommend you start with the "feature-144-SSVC" or "develop" branch and port your changes to that, and run the GitHub workflow scripts or the one below. Don't use the raw Validator.js which is going bro behave different.

REPO_DIR=`pwd`
CVE_SCHEMA_DIR=$REPO_DIR/schema
CVE_SCHEMA_FILENAME=CVE_Record_Format.json

npm --prefix "${CVE_SCHEMA_DIR}/support/schema2markmap" install "${CVE_SCHEMA_DIR}/support/schema2markmap"

python3.12 "${REPO_DIR}/tools/merge_schema.py" "${CVE_SCHEMA_DIR}/imports/ssvc/deep-ssvc-v1.0.1.json" > "${CVE_SCHEMA_DIR}/imports/ssvc/ssvc-v1.0.1.json"

sed 's/file\://g' "${CVE_SCHEMA_DIR}/${CVE_SCHEMA_FILENAME}" > "${CVE_SCHEMA_DIR}/cve-schema.json"

node  "${CVE_SCHEMA_DIR}/support/schema2markmap/schema-bundle.js" "${CVE_SCHEMA_DIR}/cve-schema.json" "${CVE_SCHEMA_DIR}/docs/"

ajv compile -c ajv-formats -s "${CVE_SCHEMA_DIR}/docs/CVE_Record_Format_bundled.json"
ajv validate -c ajv-formats -s "${CVE_SCHEMA_DIR}/docs/CVE_Record_Format_bundled.json" -d "${CVE_SCHEMA_DIR}/docs/full-record-basic-example.json"
ajv validate -c ajv-formats -s "${CVE_SCHEMA_DIR}/docs/CVE_Record_Format_bundled.json" -d "${CVE_SCHEMA_DIR}/docs/full-record-advanced-example.json"
ajv validate -c ajv-formats -s "${CVE_SCHEMA_DIR}/docs/CVE_Record_Format_bundled_cnaPublishedContainer.json" -d "${CVE_SCHEMA_DIR}/docs/cnaContainer-advanced-example.json"
ajv validate -c ajv-formats -s "${CVE_SCHEMA_DIR}/docs/CVE_Record_Format_bundled_cnaPublishedContainer.json" -d "${CVE_SCHEMA_DIR}/docs/cnaContainer-basic-example.json"

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.

2 participants