diff --git a/data/json/decision_points/basic/probability_scale_in_5_weighted_levels_ascending_1_0_0.json b/data/json/decision_points/basic/probability_scale_in_5_weighted_levels_ascending_1_0_0.json index 2b5df7aa..bcefa963 100644 --- a/data/json/decision_points/basic/probability_scale_in_5_weighted_levels_ascending_1_0_0.json +++ b/data/json/decision_points/basic/probability_scale_in_5_weighted_levels_ascending_1_0_0.json @@ -1,35 +1,35 @@ { "namespace": "basic", - "key": "P_5X", + "key": "P_5W", "version": "1.0.0", "name": "Probability Scale in 5 weighted levels, ascending", - "definition": "A probability scale with finer resolution at both extremes, based on NIST SP 800-30 Rev. 1 Appendix G", + "definition": "A probability scale with higher resolution as probability increases", "schemaVersion": "2.0.0", "values": [ { - "key": "VL", - "name": "Very Low", - "definition": "0% <= Probability < 5%. Highly unlikely." + "key": "P0_30", + "name": "Less than 30%", + "definition": "Probability < 0.3" }, { - "key": "L", - "name": "Low", - "definition": "5% <= Probability < 21%. Unlikely." + "key": "P30_55", + "name": "30% to 55%", + "definition": "0.3 <= Probability < 0.55" }, { - "key": "M", - "name": "Moderate", - "definition": "21% <= Probability < 80%. Somewhat likely." + "key": "P55_75", + "name": "55% to 75%", + "definition": "0.55 <= Probability < 0.75" }, { - "key": "H", - "name": "High", - "definition": "80% <= Probability < 96%. Highly likely." + "key": "P75_90", + "name": "75% to 90%", + "definition": "0.75 <= Probability < 0.9" }, { - "key": "VH", - "name": "Very High", - "definition": "96% <= Probability <= 100%. Almost certain." + "key": "P90_100", + "name": "Greater than 90%", + "definition": "0.9 <= Probability <= 1.0" } ] } diff --git a/data/json/decision_points/nist_800_30/probability_scale_in_5_weighted_levels_ascending_1_0_0.json b/data/json/decision_points/nist_800_30/probability_scale_in_5_weighted_levels_ascending_1_0_0.json new file mode 100644 index 00000000..2422981f --- /dev/null +++ b/data/json/decision_points/nist_800_30/probability_scale_in_5_weighted_levels_ascending_1_0_0.json @@ -0,0 +1,35 @@ +{ + "namespace": "nist#800-30", + "key": "P_5X", + "version": "1.0.0", + "name": "Probability Scale in 5 weighted levels, ascending", + "definition": "A probability scale with finer resolution at both extremes, based on NIST SP 800-30 Rev. 1 Appendix G", + "schemaVersion": "2.0.0", + "values": [ + { + "key": "VL", + "name": "Very Low", + "definition": "0% <= Probability < 5%. Highly unlikely." + }, + { + "key": "L", + "name": "Low", + "definition": "5% <= Probability < 21%. Unlikely." + }, + { + "key": "M", + "name": "Moderate", + "definition": "21% <= Probability < 80%. Somewhat likely." + }, + { + "key": "H", + "name": "High", + "definition": "80% <= Probability < 96%. Highly likely." + }, + { + "key": "VH", + "name": "Very High", + "definition": "96% <= Probability <= 100%. Almost certain." + } + ] +} diff --git a/data/json/ssvc_object_registry.json b/data/json/ssvc_object_registry.json index 151e6dd2..7ded94f2 100644 --- a/data/json/ssvc_object_registry.json +++ b/data/json/ssvc_object_registry.json @@ -289,76 +289,6 @@ } } }, - "P_5X": { - "key": "P_5X", - "versions": { - "1.0.0": { - "version": "1.0.0", - "obj": { - "namespace": "basic", - "key": "P_5X", - "version": "1.0.0", - "name": "Probability Scale in 5 weighted levels, ascending", - "definition": "A probability scale with finer resolution at both extremes, based on NIST SP 800-30 Rev. 1 Appendix G", - "schemaVersion": "2.0.0", - "values": [ - { - "key": "VL", - "name": "Very Low", - "definition": "0% <= Probability < 5%. Highly unlikely." - }, - { - "key": "L", - "name": "Low", - "definition": "5% <= Probability < 21%. Unlikely." - }, - { - "key": "M", - "name": "Moderate", - "definition": "21% <= Probability < 80%. Somewhat likely." - }, - { - "key": "H", - "name": "High", - "definition": "80% <= Probability < 96%. Highly likely." - }, - { - "key": "VH", - "name": "Very High", - "definition": "96% <= Probability <= 100%. Almost certain." - } - ] - }, - "values": { - "VL": { - "key": "VL", - "name": "Very Low", - "definition": "0% <= Probability < 5%. Highly unlikely." - }, - "L": { - "key": "L", - "name": "Low", - "definition": "5% <= Probability < 21%. Unlikely." - }, - "M": { - "key": "M", - "name": "Moderate", - "definition": "21% <= Probability < 80%. Somewhat likely." - }, - "H": { - "key": "H", - "name": "High", - "definition": "80% <= Probability < 96%. Highly likely." - }, - "VH": { - "key": "VH", - "name": "Very High", - "definition": "96% <= Probability <= 100%. Almost certain." - } - } - } - } - }, "P_2A": { "key": "P_2A", "versions": { @@ -5986,6 +5916,81 @@ } } }, + "nist#800-30": { + "namespace": "nist#800-30", + "keys": { + "P_5X": { + "key": "P_5X", + "versions": { + "1.0.0": { + "version": "1.0.0", + "obj": { + "namespace": "nist#800-30", + "key": "P_5X", + "version": "1.0.0", + "name": "Probability Scale in 5 weighted levels, ascending", + "definition": "A probability scale with finer resolution at both extremes, based on NIST SP 800-30 Rev. 1 Appendix G", + "schemaVersion": "2.0.0", + "values": [ + { + "key": "VL", + "name": "Very Low", + "definition": "0% <= Probability < 5%. Highly unlikely." + }, + { + "key": "L", + "name": "Low", + "definition": "5% <= Probability < 21%. Unlikely." + }, + { + "key": "M", + "name": "Moderate", + "definition": "21% <= Probability < 80%. Somewhat likely." + }, + { + "key": "H", + "name": "High", + "definition": "80% <= Probability < 96%. Highly likely." + }, + { + "key": "VH", + "name": "Very High", + "definition": "96% <= Probability <= 100%. Almost certain." + } + ] + }, + "values": { + "VL": { + "key": "VL", + "name": "Very Low", + "definition": "0% <= Probability < 5%. Highly unlikely." + }, + "L": { + "key": "L", + "name": "Low", + "definition": "5% <= Probability < 21%. Unlikely." + }, + "M": { + "key": "M", + "name": "Moderate", + "definition": "21% <= Probability < 80%. Somewhat likely." + }, + "H": { + "key": "H", + "name": "High", + "definition": "80% <= Probability < 96%. Highly likely." + }, + "VH": { + "key": "VH", + "name": "Very High", + "definition": "96% <= Probability <= 100%. Almost certain." + } + } + } + } + } + } + }, "ssvc": { "namespace": "ssvc", "keys": { diff --git a/docs/reference/code/namespaces.md b/docs/reference/code/namespaces.md index 87397326..d0118a76 100644 --- a/docs/reference/code/namespaces.md +++ b/docs/reference/code/namespaces.md @@ -39,7 +39,7 @@ title: Base Namespace Structure flowchart LR subgraph base_ns[Base Namespace] - direction LR + direction TB subgraph unregistered[Unregistered Namespace] direction LR xpfx[x_] @@ -50,11 +50,31 @@ subgraph base_ns[Base Namespace] subgraph registered[Registered Namespace] direction LR base_registered[Registered Base Namespace] + base_fragment[Fragment] + base_registered -.->|optional| base_fragment end registered ~~~|OR| unregistered end ``` +!!! warning "Reserved Namespace Strings" + + The SSVC project has _reserved_ the following namespace strings: + + - `example` and `x_example`are _reserved_ for use in documentation or as + examples where a registered or unregistered namespace is needed, respectively. + No production use of the `example` or `x_example` namespace is intended. + + - `test` and `x_test` are _reserved_ for use in testing of current + or new SSVC related code. + No production use of the `test` namespace is intended. + + - `invalid` and `x_invalid` are _reserved_ and must not be used as + registered or unregistered namespaces, respectively. Attempting to create an + object using either of these strings will result in an error in the Python + implementation of SSVC. + + !!! info inline end "Current Registered Namespaces" The SSVC project currently has a set of registered namespaces that are @@ -69,6 +89,8 @@ end print(f"- {ns.value}") ``` + + #### Registered Namespace Registered namespaces are those that are explicitly defined in the SSVC project. @@ -114,10 +136,24 @@ Registered namespaces are intended to be used as follows: Suggestions for changes to the CVSS specifications should be directed to the [FIRST CVSS Special Interest Group](https://www.first.org/cvss/) (SIG). -!!! example "Potential Standards-based namespaces" +!!! note "Use of (Optional) Fragments in Registered Namespaces" + + The optional fragment string following the `#` character in a registered + namespace is used to indicate a grouping or subset of decision points within + the same namespace. + This allows us to organize decision points that are related or share a common + context within the same namespace. + +!!! example "Fragment Usage in Registered Namespaces" + + We use the `nist` namespace for decision points based on NIST standards, and + fragments to differentiate between decision points based on different NIST publications, + e.g., `nist#800-30` for decision points based on NIST Special Publication 800-30. We may in the future add namespaces when needed to reflect different standards - bodies like `nist`, `iso-iec`, `ietf`, `oasis`, etc. + bodies like `iso-iec`, `ietf`, `oasis`, etc. We would expect to use fragments + in a similar way to differentiate between different standards or publications + from the same standards body. !!! question "How do I request a new registered namespace?" @@ -142,11 +178,16 @@ we expect that this will rarely lead to conflicts in practice. notation of a domain under their control to ensure uniqueness. - After the reverse domain name, a fragment separated by `#` must follow. - The construct of reverse domain name followed by `#` and a fragment is called `x-name`. + - For any domain using other characters, DNS Punycode must be used - Aside from the required `x_` prefix and the fragment separator `#`, unregistered namespaces must contain only alphanumeric characters, dots (`.`), and dashes (`-`). -- For any domain using other characters, DNS Punycode must be used +!!! note "Fragment Usage is Required in Unregistered Namespaces" + In unregistered namespaces, the fragment is _required_. This is different from + registered namespaces, where the fragment is _optional_. + + !!! warning "Namespace Conflicts" Conflicts are possible in the `x_` prefix space - especially as the control over a @@ -155,7 +196,8 @@ we expect that this will rarely lead to conflicts in practice. `x_example.test#test`, and there are no guarantees of global uniqueness for the decision points in the `x_example.test#test` namespace. - !!! info "Documentation and Test Namespaces" + +!!! info "Documentation and Test Namespaces" Any namespace starting with `x_example` can be used in documentation or as examples, where a unregistered namespace is needed. @@ -164,7 +206,7 @@ we expect that this will rarely lead to conflicts in practice. The namespace `x_example.test#documentation` is recommended for use in documentation or as examples. - !!! tip "Test Namespace" +!!! tip "Test Namespace" The `x_example.test#test` namespace is commonly used for testing purposes and is not intended for production use. It is used to test the SSVC framework and its components, @@ -178,7 +220,8 @@ Extensions are optional and may be used to refine or clarify existing decision p Extensions allow SSVC users to create decision points that are specific to their constituencies or to provide translations of existing decision points. -!!! info "Namespace Extension Requirements" + +!!! info "Namespace Extension Semantic Requirements" Extensions must follow the following requirements: @@ -189,22 +232,28 @@ constituencies or to provide translations of existing decision points. - Extensions may reduce the set of values for a decision point in the parent namespace, but must not add new values. + See also the "Namespace Extension Structural Requirements" below. + +!!! question "Why are the Namespace Extension Semantic Requirements important?" + + The way extensions are built enables tools to process the decision points even if + they do not know the defined extension. As long as the tool knows the base + namespace, it can process the decision point. + !!! question "What if I want to create a new decision point?" - If you want to create a new decision point, please use a private/experimental namespace + If you want to create a new decision point, please use an unregistered namespace as described above instead of an extension. Extensions are not intended to be used to create new decision points. -!!! question "Why is that important?" - - The way extensions are build enables tools to process the decision points even if - they do not know the defined extension. As long as the tool knows the base - namespace, it can process the decision point. #### Namespace Extension Structure The first extension segment is reserved for an optional BCP-47 language tag, which may be left empty. -When empty, the default language (`en-US`) is implied. + +!!! info "Default language is `en-US`" + + If the first extension segment is left empty, the default language (`en-US`) is implied. Subsequent extension segments must either @@ -230,7 +279,7 @@ The following diagram illustrates the structure of namespace extensions: ```mermaid --- -title: Namespace Extensions +title: Namespace Extension Structure --- flowchart LR @@ -243,22 +292,34 @@ subgraph exts[Extensions] lang ~~~|OR| empty_lang end subgraph ext[Subsequent Extension Segments] - direction LR - e_lang[Language Tag] - dot[.] - reverse_ns_ext[Reverse Domain Name Notation] - fragment[#Fragment] - subgraph translation[Translation] + direction TB + + subgraph lang_seg[Language Only Segment] + e_lang[Language Tag] + end + + subgraph e_dot[Extension Segment] + direction LR + dot[.] + reverse_ns_ext[Reverse Domain Name Notation] + fragment[#Fragment] + + dot --> reverse_ns_ext --> fragment + end + + subgraph translation[Translation Segment] direction LR t_dot[.] t_reverse_ns_ext[Reverse Domain Name Notation] - t_fragment[#Optional Fragment] + t_fragment[#Fragment] t_dollar[$] t_lang[Language Tag] - t_dot --> t_reverse_ns_ext --> t_fragment --> t_dollar --> t_lang + t_dot --> t_reverse_ns_ext -.->|optional| t_fragment -.-> t_dollar --> t_lang + t_reverse_ns_ext --> t_dollar end - lang ~~~|OR| dot --> reverse_ns_ext --> fragment ~~~|OR| translation + + lang_seg ~~~|OR| e_dot ~~~|OR| translation end first -->|/| ext ext -->|/| ext @@ -269,7 +330,7 @@ base_ns -->|/| first ``` -!!! info "Namespace Extension Requirements" +!!! info "Namespace Extension Structural Requirements" Extensions must follow the following structure: @@ -286,17 +347,15 @@ base_ns -->|/| first The structure of the namespace string is intended to show inheritance for variations on SSVC objects. -!!! tip "Extension Order Matters" - Extension order matters. `ssvc/de-DE/.example.organization#ref-arch-1` - denotes that (a) an official German (Germany) translation of the SSVC decision points - is available, and (b) that this translation has been extended with an extension - by `organization.example` to fit their specific needs for `ref-arch-1`. +!!! tip "Extension Order Matters" - On the other hand, `ssvc//.example.organization#ref-arch-1/de-DE` - denotes that (a) the `.example.organization#ref-arch-1` extension is - available in the default language (`en-US`), and (b) that this extension has - been translated into German (Germany). + SSVC namespace extension order carries semantic meaning. + + | Example | Meaning | + |---------|---------| + | `ssvc/de-DE/.example.organization#ref-arch-1` | Denotes (a) an official German (Germany) translation of the SSVC decision points is available, and (b) that this translation has been extended with an extension by `organization.example` to fit their specific needs for `ref-arch-1`.| + | `ssvc//.example.organization#ref-arch-1/de-DE` | Denotes that (a) the `.example.organization#ref-arch-1` extension is available in the default language (`en-US`), and (b) that this extension has been translated into German (Germany). | !!! example "Use of fragment identifiers and language tags" @@ -370,7 +429,7 @@ and the ABNF specification in - Namespaces must be between 3 and 1000 characters long. -(The ABNF is used to generated the regular expressions in +(The ABNF is used to generate the regular expressions in `src/ssvc/utils/patterns.py`, see the comment in the source code there.) ## The `ssvc.namespaces` module diff --git a/src/ssvc/decision_points/basic/probability/__init__.py b/src/ssvc/decision_points/basic/probability/__init__.py index 9dae2bf1..da01e3e7 100644 --- a/src/ssvc/decision_points/basic/probability/__init__.py +++ b/src/ssvc/decision_points/basic/probability/__init__.py @@ -22,7 +22,8 @@ from .cis_wep import LATEST as CIS_WEP from .five_equal import LATEST as FIVE_EQUAL from .five_weighted import LATEST as FIVE_WEIGHTED -from .nist5 import LATEST as NIST_5 from .two_equal import LATEST as TWO_EQUAL -DECISION_POINTS = {dp.id:dp for dp in (TWO_EQUAL,FIVE_EQUAL,FIVE_WEIGHTED,NIST_5,CIS_WEP)} \ No newline at end of file +DECISION_POINTS = { + dp.id: dp for dp in (TWO_EQUAL, FIVE_EQUAL, FIVE_WEIGHTED, CIS_WEP) +} diff --git a/src/ssvc/decision_points/nist/__init__.py b/src/ssvc/decision_points/nist/__init__.py new file mode 100644 index 00000000..9a5d8558 --- /dev/null +++ b/src/ssvc/decision_points/nist/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 + +"""Provides decision points based on NIST standards""" + +from .sp800_30_r1_g import LATEST as PROB_W5_SP800_30_R1_G + +DECISION_POINTS = {dp.id: dp for dp in (PROB_W5_SP800_30_R1_G,)} diff --git a/src/ssvc/decision_points/nist/base.py b/src/ssvc/decision_points/nist/base.py new file mode 100644 index 00000000..210d6153 --- /dev/null +++ b/src/ssvc/decision_points/nist/base.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +""" +Provides base classes for NIST-based decision points. +""" +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +from pydantic import BaseModel + +from ssvc.decision_points.base import DecisionPoint +from ssvc.namespaces import NameSpace + + +class NISTDecisionPoint(DecisionPoint, BaseModel): + """Base class for NIST-based decision points.""" + + namespace: str = NameSpace.NIST diff --git a/src/ssvc/decision_points/basic/probability/nist5.py b/src/ssvc/decision_points/nist/sp800_30_r1_g.py similarity index 92% rename from src/ssvc/decision_points/basic/probability/nist5.py rename to src/ssvc/decision_points/nist/sp800_30_r1_g.py index 159825c8..24378b5f 100644 --- a/src/ssvc/decision_points/basic/probability/nist5.py +++ b/src/ssvc/decision_points/nist/sp800_30_r1_g.py @@ -21,8 +21,9 @@ # subject to its own license. # DM24-0278 from ssvc.decision_points.base import DecisionPointValue -from ssvc.decision_points.basic.base import BasicDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.nist.base import NISTDecisionPoint +from ssvc.namespaces import FRAG_SEP, NameSpace # These ranges are based on NIST SP 800-30 Rev. 1 Appendix G @@ -53,7 +54,8 @@ ) -P5X = BasicDecisionPoint( +P5X = NISTDecisionPoint( + namespace=FRAG_SEP.join((NameSpace.NIST, "800-30")), key="P_5X", version="1.0.0", name="Probability Scale in 5 weighted levels, ascending", diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 128ee648..01f1834e 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -23,11 +23,19 @@ # subject to its own license. # DM24-0278 -from enum import StrEnum, auto +from enum import StrEnum from ssvc.utils.defaults import MAX_NS_LENGTH, MIN_NS_LENGTH, X_PFX from ssvc.utils.patterns import NS_PATTERN +EXT_SEP = "/" +FRAG_SEP = "#" + +# The following namespace strings are RESERVED and cannot be used +# as the base of a namespace (i.e., before any fragment or extension), +# even if they otherwise meet the pattern requirements. +RESERVED_NS = ("invalid", "x_invalid") + class NameSpace(StrEnum): f"""Define the official namespaces for SSVC. @@ -54,12 +62,13 @@ class NameSpace(StrEnum): # auto() is used to automatically assign values to the members. # when used in a StrEnum, auto() assigns the lowercase name of the member as the value - SSVC = auto() - CVSS = auto() - CISA = auto() - BASIC = auto() - EXAMPLE = auto() - TEST = auto() + SSVC = "ssvc" + CVSS = "cvss" + CISA = "cisa" + BASIC = "basic" + EXAMPLE = "example" + TEST = "test" + NIST = "nist" @classmethod def validate(cls, value: str) -> str: @@ -79,20 +88,22 @@ def validate(cls, value: str) -> str: """ valid = NS_PATTERN.match(value) - ext_sep = "/" - frag_sep = "#" - if valid: # pattern matches, so we can proceed with further checks # partition always returns three parts: the part before the separator, the separator itself, and the part after the separator - (base_ns, _, extension) = value.partition(ext_sep) + (base_ns, _, extension) = value.partition(EXT_SEP) # and we don't care about the extension beyond the pattern match above # so base_ns is either the full value or the part before the first slash # but base_ns might have a fragment # so we need to split that off if present + # because partition always returns three parts, we can ignore the second and third parts here if "#" in base_ns: - (base_ns, _, fragment) = base_ns.partition(frag_sep) + (base_ns, _, _) = base_ns.partition(FRAG_SEP) + + # reject reserved namespaces + if base_ns in RESERVED_NS: + raise ValueError(f"Invalid namespace: '{value}' is reserved.") if base_ns in cls.__members__.values(): # base_ns is a registered namespaces diff --git a/src/test/test_namespaces.py b/src/test/test_namespaces.py index 1469d813..11ef6d78 100644 --- a/src/test/test_namespaces.py +++ b/src/test/test_namespaces.py @@ -19,7 +19,7 @@ import unittest -from ssvc.namespaces import NameSpace +from ssvc.namespaces import NameSpace, RESERVED_NS from ssvc.utils.patterns import NS_PATTERN @@ -81,6 +81,10 @@ def test_namespace_validator(self): with self.assertRaises(ValueError): NameSpace.validate(ns) + for ns in RESERVED_NS: + with self.assertRaises(ValueError): + NameSpace.validate(ns) + for ns in [ "x_example.test#test", "x_example.test#foo",