Skip to content

Commit 9abaa13

Browse files
committed
Merge branch 'main' into feature/reorder-base-class-mixins
# Conflicts: # src/ssvc/decision_points/base.py # src/test/test_mixins.py
2 parents eccbe60 + 77baef3 commit 9abaa13

File tree

14 files changed

+284
-27
lines changed

14 files changed

+284
-27
lines changed

.github/workflows/lint_md_changes.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v4
1717
with:
1818
fetch-depth: 0
19-
- uses: tj-actions/changed-files@v45
19+
- uses: tj-actions/changed-files@2f7c5bfce28377bc069a65ba478de0a74aa0ca32
2020
id: changed-files
2121
with:
2222
files: '**/*.md'

.github/workflows/python-app.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ jobs:
2121
- uses: actions/checkout@v4
2222
with:
2323
fetch-tags: true
24-
- name: Set up Python 3.10
24+
- name: Set up Python 3.12
2525
uses: actions/setup-python@v5
2626
with:
27-
python-version: "3.10"
27+
python-version: "3.12"
2828
- name: Install dependencies
2929
run: |
3030
python -m pip install --upgrade pip

data/schema/v1/Decision_Point-1-0-1.schema.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
},
4848
"namespace": {
4949
"type": "string",
50-
"description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.",
51-
"pattern": "^[a-z0-9-]{3,4}[a-z0-9/\\.-]*$",
52-
"examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"]
50+
"description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.",
51+
"pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$",
52+
"examples": ["ssvc", "cvss", "x_custom","x_custom/extension"]
5353
},
5454
"version": {
5555
"type": "string",

docs/reference/code/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ These include:
66
- [CSV Analyzer](analyze_csv.md)
77
- [Policy Generator](policy_generator.md)
88
- [Outcomes](outcomes.md)
9+
- [Namespaces](namespaces.md)
910
- [Doctools](doctools.md)

docs/reference/code/namespaces.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SSVC Namespaces
2+
3+
::: ssvc.namespaces

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ nav:
112112
- CSV Analyzer: 'reference/code/analyze_csv.md'
113113
- Policy Generator: 'reference/code/policy_generator.md'
114114
- Outcomes: 'reference/code/outcomes.md'
115+
- Namespaces: 'reference/code/namespaces.md'
115116
- Doctools: 'reference/code/doctools.md'
116117
- Calculator: 'ssvc-calc/index.md'
117118
- About:

src/ssvc/_mixins.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
from typing import Optional
1919

20-
from pydantic import BaseModel, ConfigDict, field_validator
20+
from pydantic import BaseModel, ConfigDict, Field, field_validator
2121
from semver import Version
2222

23+
from ssvc.namespaces import NS_PATTERN, NameSpace
2324
from . import _schemaVersion
2425

2526

@@ -33,7 +34,7 @@ class _Versioned(BaseModel):
3334

3435
@field_validator("version")
3536
@classmethod
36-
def validate_version(cls, value):
37+
def validate_version(cls, value: str) -> str:
3738
"""
3839
Validate the version field.
3940
Args:
@@ -54,7 +55,29 @@ class _Namespaced(BaseModel):
5455
Mixin class for namespaced SSVC objects.
5556
"""
5657

57-
namespace: str = "ssvc"
58+
# the field definition enforces the pattern for namespaces
59+
# additional validation is performed in the field_validator immediately after the pattern check
60+
namespace: str = Field(pattern=NS_PATTERN, min_length=3, max_length=25)
61+
62+
@field_validator("namespace", mode="before")
63+
@classmethod
64+
def validate_namespace(cls, value: str) -> str:
65+
"""
66+
Validate the namespace field.
67+
The value will have already been checked against the pattern in the field definition.
68+
The value must be one of the official namespaces or start with 'x_'.
69+
70+
Args:
71+
value: a string representing a namespace
72+
73+
Returns:
74+
the validated namespace value
75+
76+
Raises:
77+
ValueError: if the value is not a valid namespace
78+
"""
79+
80+
return NameSpace.validate(value)
5881

5982

6083
class _Keyed(BaseModel):

src/ssvc/decision_points/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pydantic import BaseModel
2222

2323
from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned
24+
from ssvc.namespaces import NameSpace
2425

2526
logger = logging.getLogger(__name__)
2627

@@ -66,8 +67,15 @@ class SsvcDecisionPoint(_Valued, _Keyed, _Versioned, _Namespaced, _Base, BaseMod
6667
Models a single decision point as a list of values.
6768
"""
6869

70+
namespace: str = NameSpace.SSVC
6971
values: tuple[SsvcDecisionPointValue, ...]
7072

73+
def __iter__(self):
74+
"""
75+
Allow iteration over the decision points in the group.
76+
"""
77+
return iter(self.values)
78+
7179
def __init__(self, **data):
7280
super().__init__(**data)
7381
register(self)

src/ssvc/decision_points/cvss/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
from pydantic import BaseModel
1919

2020
from ssvc.decision_points.base import SsvcDecisionPoint
21+
from ssvc.namespaces import NameSpace
2122

2223

2324
class CvssDecisionPoint(SsvcDecisionPoint, BaseModel):
2425
"""
2526
Models a single CVSS decision point as a list of values.
2627
"""
2728

28-
namespace: str = "cvss"
29+
namespace: NameSpace = NameSpace.CVSS

src/ssvc/namespaces.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python
2+
"""
3+
SSVC objects use namespaces to distinguish between objects that arise from different
4+
stakeholders or analytical category sources. This module defines the official namespaces
5+
for SSVC and provides a method to validate namespace values.
6+
"""
7+
# Copyright (c) 2025 Carnegie Mellon University and Contributors.
8+
# - see Contributors.md for a full list of Contributors
9+
# - see ContributionInstructions.md for information on how you can Contribute to this project
10+
# Stakeholder Specific Vulnerability Categorization (SSVC) is
11+
# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed
12+
# with this Software or contact permission@sei.cmu.edu for full terms.
13+
# Created, in part, with funding and support from the United States Government
14+
# (see Acknowledgments file). This program may include and/or can make use of
15+
# certain third party source code, object code, documentation and other files
16+
# (“Third Party Software”). See LICENSE.md for more details.
17+
# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the
18+
# U.S. Patent and Trademark Office by Carnegie Mellon University
19+
20+
import re
21+
from enum import StrEnum, auto
22+
23+
X_PFX = "x_"
24+
"""The prefix for extension namespaces. Extension namespaces must start with this prefix."""
25+
26+
# pattern to match
27+
# `(?=.{3,25}$)`: 3-25 characters long
28+
# `^(x_)`: `x_` prefix is optional
29+
# `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters
30+
# `[/.-]?`: only one punctuation character is allowed between alphanumeric characters
31+
# `[a-z0-9]+`: at least one alphanumeric character is required after the punctuation character
32+
# `([/.-]?[a-z0-9]+){0,22}`: zero to 22 occurrences of the punctuation character followed by at least one alphanumeric character
33+
# (note that the total limit will kick in at or before this point)
34+
# `$`: end of the string
35+
NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$")
36+
"""The regular expression pattern for validating namespaces.
37+
38+
Note:
39+
Namespace values must
40+
41+
- be 3-25 characters long
42+
- contain only lowercase alphanumeric characters and limited punctuation characters (`/`,`.` and `-`)
43+
- have only one punctuation character in a row
44+
- start with 3-4 alphanumeric characters after the optional extension prefix
45+
- end with an alphanumeric character
46+
47+
See examples in the `NameSpace` enum.
48+
"""
49+
50+
51+
class NameSpace(StrEnum):
52+
"""
53+
Defines the official namespaces for SSVC.
54+
55+
The namespace value must be one of the members of this enum or start with the prefix specified in X_PFX.
56+
Namespaces must be 3-25 lowercase characters long and must start with 3-4 alphanumeric characters after the optional prefix.
57+
Limited punctuation characters (/.-) are allowed between alphanumeric characters, but only one at a time.
58+
59+
Example:
60+
Following are examples of valid and invalid namespace values:
61+
62+
- `ssvc` is *valid* because it is present in the enum
63+
- `custom` is *invalid* because it does not start with the experimental prefix and is not in the enum
64+
- `x_custom` is *valid* because it starts with the experimental prefix and meets the pattern requirements
65+
- `x_custom/extension` is *valid* because it starts with the experimental prefix and meets the pattern requirements
66+
- `x_custom/extension/with/multiple/segments` is *invalid* because it exceeds the maximum length
67+
- `x_custom//extension` is *invalid* because it has multiple punctuation characters in a row
68+
- `x_custom.extension.` is *invalid* because it does not end with an alphanumeric character
69+
- `x_custom.extension.9` is *valid* because it meets the pattern requirements
70+
"""
71+
72+
# auto() is used to automatically assign values to the members.
73+
# when used in a StrEnum, auto() assigns the lowercase name of the member as the value
74+
SSVC = auto()
75+
CVSS = auto()
76+
77+
@classmethod
78+
def validate(cls, value: str) -> str:
79+
"""
80+
Validate the namespace value. Valid values are members of the enum or start with the experimental prefix and
81+
meet the specified pattern requirements.
82+
83+
Args:
84+
value: the namespace value to validate
85+
86+
Returns:
87+
the validated namespace value
88+
89+
Raises:
90+
ValueError: if the value is not a valid namespace
91+
92+
"""
93+
if value in cls.__members__.values():
94+
return value
95+
if value.startswith(X_PFX) and NS_PATTERN.match(value):
96+
return value
97+
raise ValueError(
98+
f"Invalid namespace: {value}. Must be one of {[ns.value for ns in cls]} or start with '{X_PFX}'."
99+
)
100+
101+
102+
def main():
103+
for ns in NameSpace:
104+
print(ns)
105+
106+
107+
if __name__ == "__main__":
108+
main()

0 commit comments

Comments
 (0)