From a096d15d5f968cbf8bd87fe22bdc35f6260af018 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Feb 2025 10:43:37 -0500 Subject: [PATCH 01/99] add `__str__()` method to `SsvcDecisionPointValue` --- src/ssvc/decision_points/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 869e3263..1dd4b2ce 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -60,6 +60,9 @@ class SsvcDecisionPointValue(_Base, _Keyed, BaseModel): Models a single value option for a decision point. """ + def __str__(self): + return self.name + class SsvcDecisionPoint(_Base, _Keyed, _Versioned, _Namespaced, BaseModel): """ From b753aedc4142e2cd5abc7c988d4cf43865cced31 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Feb 2025 10:45:54 -0500 Subject: [PATCH 02/99] add `combinations` and `combo_strings` methods to `SsvcDecisionPointGroup` make it easier to build decision frameworks --- src/ssvc/dp_groups/base.py | 28 ++++++++++++- src/test/test_dp_groups.py | 86 ++++++++++++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index d198a0df..47ca6b68 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -17,10 +17,13 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +from itertools import product +from typing import Generator + from pydantic import BaseModel from ssvc._mixins import _Base, _Versioned -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue class SsvcDecisionPointGroup(_Base, _Versioned, BaseModel): @@ -44,6 +47,29 @@ def __len__(self): l = len(dplist) return l + def combinations( + self, + ) -> Generator[tuple[SsvcDecisionPointValue, ...], None, None]: + # Generator[yield_type, send_type, return_type] + """ + Produce all possible combinations of decision point values in the group. + """ + # for each decision point, get the values + # then take the product of all the values + # and yield each combination + values_list: list[list[SsvcDecisionPointValue]] = [ + dp.values for dp in self.decision_points + ] + for combination in product(*values_list): + yield combination + + def combo_strings(self) -> Generator[tuple[str, ...], None, None]: + """ + Produce all possible combinations of decision point values in the group as strings. + """ + for combo in self.combinations(): + yield tuple(str(v) for v in combo) + def get_all_decision_points_from( *groups: list[SsvcDecisionPointGroup], diff --git a/src/test/test_dp_groups.py b/src/test/test_dp_groups.py index e4c2397e..46fec87f 100644 --- a/src/test/test_dp_groups.py +++ b/src/test/test_dp_groups.py @@ -27,15 +27,9 @@ def setUp(self) -> None: description=f"Description of Decision Point {i}", version="1.0.0", values=( - SsvcDecisionPointValue( - name="foo", key="FOO", description="foo" - ), - SsvcDecisionPointValue( - name="bar", key="BAR", description="bar" - ), - SsvcDecisionPointValue( - name="baz", key="BAZ", description="baz" - ), + SsvcDecisionPointValue(name="foo", key="FOO", description="foo"), + SsvcDecisionPointValue(name="bar", key="BAR", description="bar"), + SsvcDecisionPointValue(name="baz", key="BAZ", description="baz"), ), ) self.dps.append(dp) @@ -69,6 +63,80 @@ def test_len(self): self.assertEqual(len(self.dps), len(list(g.decision_points))) self.assertEqual(len(self.dps), len(g)) + def test_combinations(self): + # add them to a decision point group + g = dpg.SsvcDecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # get all the combinations + combos = list(g.combinations()) + + # assert that the number of combinations is the product of the number of values + # for each decision point + n_combos = 1 + for dp in self.dps: + n_combos *= len(dp.values) + self.assertEqual(n_combos, len(combos)) + + # assert that each combination is a tuple + for combo in combos: + self.assertIsInstance(combo, tuple) + # assert that each value in the combination is a decision point value + for value in combo: + self.assertIsInstance(value, SsvcDecisionPointValue) + + # foo, bar, and baz should be in each combination to some degree + foo_count = sum(1 for v in combo if v.name == "foo") + bar_count = sum(1 for v in combo if v.name == "bar") + baz_count = sum(1 for v in combo if v.name == "baz") + for count in (foo_count, bar_count, baz_count): + # each count should be greater than or equal to 0 + self.assertGreaterEqual(count, 0) + # the total count of foo, bar, and baz should be the same as the length of the combination + # indicating that no other values are present + total = sum((foo_count, bar_count, baz_count)) + self.assertEqual(len(combo), total) + + def test_combo_strings(self): + # add them to a decision point group + g = dpg.SsvcDecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # get all the combinations + combos = list(g.combo_strings()) + + # assert that the number of combinations is the product of the number of values + # for each decision point + n_combos = 1 + for dp in self.dps: + n_combos *= len(dp.values) + self.assertEqual(n_combos, len(combos)) + + # assert that each combination is a tuple + for combo in combos: + self.assertEqual(len(self.dps), len(combo)) + self.assertIsInstance(combo, tuple) + # assert that each value in the combination is a string + for value in combo: + self.assertIsInstance(value, str) + # foo, bar, and baz should be in each combination to some degree + foo_count = sum(1 for v in combo if v == "foo") + bar_count = sum(1 for v in combo if v == "bar") + baz_count = sum(1 for v in combo if v == "baz") + for count in (foo_count, bar_count, baz_count): + # each count should be greater than or equal to 0 + self.assertGreaterEqual(count, 0) + # the total count of foo, bar, and baz should be the same as the length of the combination + # indicating that no other values are present + total = sum((foo_count, bar_count, baz_count)) + self.assertEqual(len(combo), total) + def test_json_roundtrip(self): # add them to a decision point group g = dpg.SsvcDecisionPointGroup( From 5a50e8a2ac304117ddbdf579f1f2e55f4f199638 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Feb 2025 11:48:16 -0500 Subject: [PATCH 03/99] wip commit --- src/ssvc/framework/__init__.py | 28 ++++++ src/ssvc/framework/decision_framework.py | 105 +++++++++++++++++++++++ src/ssvc/policy_generator.py | 20 ++--- src/test/test_decision_framework.py | 73 ++++++++++++++++ 4 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 src/ssvc/framework/__init__.py create mode 100644 src/ssvc/framework/decision_framework.py create mode 100644 src/test/test_decision_framework.py diff --git a/src/ssvc/framework/__init__.py b/src/ssvc/framework/__init__.py new file mode 100644 index 00000000..ae6a8836 --- /dev/null +++ b/src/ssvc/framework/__init__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +""" +file: __init__.py +author: adh +created_at: 2/25/25 10:00 AM +""" + + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/framework/decision_framework.py b/src/ssvc/framework/decision_framework.py new file mode 100644 index 00000000..4d4ffa18 --- /dev/null +++ b/src/ssvc/framework/decision_framework.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a Decision Framework class that can be used to model decisions in SSVC +""" +import pandas as pd +from pydantic import BaseModel + +from ssvc._mixins import _Base, _Namespaced, _Versioned +from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.outcomes.base import OutcomeGroup, OutcomeValue +from ssvc.policy_generator import PolicyGenerator + + +class DecisionFramework(_Versioned, _Namespaced, _Base, BaseModel): + """ + The DecisionFramework class is a model for decisions in SSVC. + + It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. + + The mapping is generated by the PolicyGenerator class, and stored as a dictionary. + The mapping dict keys are tuples of decision points and decision point values. + The mapping dict values are outcomes. + """ + + decision_point_group: SsvcDecisionPointGroup + outcome_group: OutcomeGroup + mapping: dict[tuple[tuple[str, str], ...], OutcomeValue] + + def populate_mapping(self): + """ + Populate the mapping with all possible combinations of decision points. + """ + dp_lookup = { + dp.name.lower(): dp for dp in self.decision_point_group.decision_points + } + outcome_lookup = { + outcome.name.lower(): outcome for outcome in self.outcome_group.outcomes + } + + dp_value_lookup = {} + for dp in self.decision_point_group.decision_points: + key1 = dp.name.lower() + dp_value_lookup[key1] = {} + for dp_value in dp.values: + key2 = dp_value.name.lower() + dp_value_lookup[key1][key2] = dp_value + + with PolicyGenerator( + dp_group=self.decision_point_group, + outcomes=self.outcome_group, + ) as policy: + table: pd.DataFrame = policy.clean_policy() + + # the table is a pandas DataFrame + # the columns are the decision points, with the last column being the outcome + # the rows are the possible combinations of decision points + # we need to convert this back to specific decision points and outcomes + for row in table.itertuples(): + outcome_name = row[-1].lower() + outcome = outcome_lookup[outcome_name] + + dp_value_names = row[1:-1] + dp_value_names = [dp_name.lower() for dp_name in dp_value_names] + + columns = [col.lower() for col in table.columns] + + # construct the key for the mapping + dp_values = [] + for col, val in zip(columns, dp_value_names): + value_lookup = dp_value_lookup[col] + dp = dp_lookup[col] + val = value_lookup[val] + + k = (f"{dp.name}:{dp.version}", f"{val.name}") + dp_values.append(k) + + key = tuple(dp_values) + self.mapping[key] = outcome + + return self.mapping + + +# convenience alias +Policy = DecisionFramework + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 38e75f01..6cce2921 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -45,8 +45,8 @@ class PolicyGenerator: def __init__( self, - dp_group: SsvcDecisionPointGroup = None, - outcomes: OutcomeGroup = None, + dp_group: SsvcDecisionPointGroup, + outcomes: OutcomeGroup, outcome_weights: list[float] = None, validate: bool = False, ): @@ -86,9 +86,7 @@ def __init__( # validate that the outcome weights sum to 1.0 total = sum(outcome_weights) if not math.isclose(total, 1.0): - raise ValueError( - f"Outcome weights must sum to 1.0, but sum to {total}" - ) + raise ValueError(f"Outcome weights must sum to 1.0, but sum to {total}") self.outcome_weights = outcome_weights logger.debug(f"Outcome weights: {self.outcome_weights}") @@ -204,9 +202,7 @@ def _assign_outcomes(self): logger.debug(f"Layer count: {len(layers)}") logger.debug(f"Layer sizes: {[len(layer) for layer in layers]}") - outcome_counts = [ - round(node_count * weight) for weight in self.outcome_weights - ] + outcome_counts = [round(node_count * weight) for weight in self.outcome_weights] toposort = list(nx.topological_sort(self.G)) logger.debug(f"Toposort: {toposort[:4]}...{toposort[-4:]}") @@ -295,15 +291,11 @@ def _confirm_topological_order(self, node_order: list) -> None: # all nodes must be in the graph for node in node_order: if node not in self.G.nodes: - raise ValueError( - f"Node order contains node {node} not in the graph" - ) + raise ValueError(f"Node order contains node {node} not in the graph") for node in self.G.nodes: if node not in node_order: - raise ValueError( - f"Graph contains node {node} not in the node order" - ) + raise ValueError(f"Graph contains node {node} not in the node order") node_idx = {node: i for i, node in enumerate(node_order)} diff --git a/src/test/test_decision_framework.py b/src/test/test_decision_framework.py new file mode 100644 index 00000000..1551d6b2 --- /dev/null +++ b/src/test/test_decision_framework.py @@ -0,0 +1,73 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +from ssvc.decision_points.exploitation import LATEST as exploitation_dp +from ssvc.decision_points.safety_impact import LATEST as safety_dp +from ssvc.decision_points.system_exposure import LATEST as exposure_dp +from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.framework.decision_framework import DecisionFramework +from ssvc.outcomes.base import OutcomeGroup +from ssvc.outcomes.groups import DSOI as dsoi_og + + +class MyTestCase(unittest.TestCase): + def setUp(self): + self.framework = DecisionFramework( + name="Test Decision Framework", + description="Test Decision Framework Description", + version="1.0.0", + decision_point_group=SsvcDecisionPointGroup( + name="Test Decision Point Group", + description="Test Decision Point Group Description", + decision_points=[exploitation_dp, exposure_dp, safety_dp], + ), + outcome_group=OutcomeGroup( + name="Test Outcome Group", + description="Test Outcome Group Description", + outcomes=dsoi_og, + ), + mapping={}, + ) + + pass + + def tearDown(self): + pass + + def test_create(self): + self.assertEqual(self.framework.name, "Test Decision Framework") + self.assertEqual(3, len(self.framework.decision_point_group)) + + def test_populate_mapping(self): + result = self.framework.populate_mapping() + + # there should be one row in result for each combination of decision points + combo_count = len(list(self.framework.decision_point_group.combinations())) + self.assertEqual(len(result), combo_count) + + # the length of each key should be the number of decision points + for key in result.keys(): + self.assertEqual(len(key), 3) + for i, (dp_name_version, dp_value_name) in enumerate(key): + dp = self.framework.decision_point_group.decision_points[i] + name_version = f"{dp.name}:{dp.version}" + self.assertEqual(name_version, dp_name_version) + + value_names = [v.name for v in dp.values] + self.assertIn(dp_value_name, value_names) + + +if __name__ == "__main__": + unittest.main() From e1de818b238baa8a3cc0d441bc6af0a39cf4f86a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 14:37:10 -0500 Subject: [PATCH 04/99] fix type hint on SsvcDecisionPointGroup object --- src/ssvc/dp_groups/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index 47ca6b68..ffbbec8b 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -31,7 +31,7 @@ class SsvcDecisionPointGroup(_Base, _Versioned, BaseModel): Models a group of decision points. """ - decision_points: list[SsvcDecisionPoint] + decision_points: tuple[SsvcDecisionPoint, ...] def __iter__(self): """ From 6f9fae7e708d4256399c13526d15a0d181924a1b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 14:37:43 -0500 Subject: [PATCH 05/99] add `VERSIONS` and `LATEST` pattern to decision point groups --- src/ssvc/dp_groups/ssvc/collections.py | 6 ++++-- src/ssvc/dp_groups/ssvc/coordinator_publication.py | 8 +++++++- src/ssvc/dp_groups/ssvc/coordinator_triage.py | 7 ++++++- src/ssvc/dp_groups/ssvc/deployer.py | 7 ++++--- src/ssvc/dp_groups/ssvc/supplier.py | 6 ++++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/ssvc/dp_groups/ssvc/collections.py b/src/ssvc/dp_groups/ssvc/collections.py index c7b2b527..e106ff5a 100644 --- a/src/ssvc/dp_groups/ssvc/collections.py +++ b/src/ssvc/dp_groups/ssvc/collections.py @@ -57,10 +57,12 @@ ), ) +VERSIONS = (SSVCv1, SSVCv2, SSVCv2_1) +LATEST = VERSIONS[-1] def main(): - for dpg in [SSVCv1, SSVCv2, SSVCv2_1]: - print(dpg.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index 35423fd9..cd731cd6 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -43,9 +43,15 @@ - Public Value Added v1.0.0 """ +VERSIONS = ( + COORDINATOR_PUBLICATION_1, +) +LATEST = VERSIONS[-1] + def main(): - print(COORDINATOR_PUBLICATION_1.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index 2fedb785..2d08fe7a 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -64,9 +64,14 @@ - Safety Impact v1.0.0 """ +VERSIONS = ( + COORDINATOR_TRIAGE_1, +) +LATEST = VERSIONS[-1] def main(): - print(COORDINATOR_TRIAGE_1.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index 76218acd..2c5b111a 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -122,11 +122,12 @@ - Mission Impact v1.0.0 -> v2.0.0 """ +VERSIONS = (PATCH_APPLIER_1, DEPLOYER_2, DEPLOYER_3) +LATEST = VERSIONS[-1] def main(): - print(PATCH_APPLIER_1.model_dump_json(indent=2)) - print(DEPLOYER_2.model_dump_json(indent=2)) - print(DEPLOYER_3.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index 05fb092c..18dca35f 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -89,10 +89,12 @@ - Public Safety Impact v1.0.0 added, which subsumes Safety Impact v1.0.0 """ +VERSIONS = (PATCH_DEVELOPER_1, SUPPLIER_2) +LATEST = VERSIONS[-1] def main(): - print(PATCH_DEVELOPER_1.model_dump_json(indent=2)) - print(SUPPLIER_2.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": From d953c6a9c48662035a1913afda3700ef0117591f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 14:38:17 -0500 Subject: [PATCH 06/99] add keys to outcome groups --- src/ssvc/outcomes/base.py | 4 ++-- src/ssvc/outcomes/groups.py | 22 +++++++++++++--------- src/test/test_outcomes.py | 8 ++++++-- src/test/test_policy_generator.py | 12 ++++-------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index 11eaf873..414b1364 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -26,12 +26,12 @@ class OutcomeValue(_Base, _Keyed, BaseModel): """ -class OutcomeGroup(_Base, _Versioned, BaseModel): +class OutcomeGroup(_Base, _Keyed, _Versioned, BaseModel): """ Models an outcome group. """ - outcomes: list[OutcomeValue] + outcomes: tuple[OutcomeValue, ...] def __iter__(self): """ diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 5326b6d9..0d2aa387 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -22,6 +22,7 @@ DSOI = OutcomeGroup( name="Defer, Scheduled, Out-of-Cycle, Immediate", + key="DSOI", description="The original SSVC outcome group.", version="1.0.0", outcomes=( @@ -37,12 +38,11 @@ PUBLISH = OutcomeGroup( name="Publish, Do Not Publish", + key="PUBLISH", description="The publish outcome group.", version="1.0.0", outcomes=( - OutcomeValue( - name="Do Not Publish", key="N", description="Do Not Publish" - ), + OutcomeValue(name="Do Not Publish", key="N", description="Do Not Publish"), OutcomeValue(name="Publish", key="P", description="Publish"), ), ) @@ -52,6 +52,7 @@ COORDINATE = OutcomeGroup( name="Decline, Track, Coordinate", + key="COORDINATE", description="The coordinate outcome group.", version="1.0.0", outcomes=( @@ -66,6 +67,7 @@ MOSCOW = OutcomeGroup( name="Must, Should, Could, Won't", + key="MOSCOW", description="The Moscow outcome group.", version="1.0.0", outcomes=( @@ -81,6 +83,7 @@ EISENHOWER = OutcomeGroup( name="Do, Schedule, Delegate, Delete", + key="EISENHOWER", description="The Eisenhower outcome group.", version="1.0.0", outcomes=( @@ -96,6 +99,7 @@ CVSS = OutcomeGroup( name="CVSS Levels", + key="CVSS", description="The CVSS outcome group.", version="1.0.0", outcomes=( @@ -111,6 +115,7 @@ CISA = OutcomeGroup( name="CISA Levels", + key="CISA", description="The CISA outcome group. " "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", version="1.0.0", @@ -152,6 +157,7 @@ YES_NO = OutcomeGroup( name="Yes, No", + key="YES_NO", description="The Yes/No outcome group.", version="1.0.0", outcomes=( @@ -165,14 +171,13 @@ VALUE_COMPLEXITY = OutcomeGroup( name="Value, Complexity", + key="VALUE_COMPLEXITY", description="The Value/Complexity outcome group.", version="1.0.0", outcomes=( # drop, reconsider later, easy win, do first OutcomeValue(name="Drop", key="D", description="Drop"), - OutcomeValue( - name="Reconsider Later", key="R", description="Reconsider Later" - ), + OutcomeValue(name="Reconsider Later", key="R", description="Reconsider Later"), OutcomeValue(name="Easy Win", key="E", description="Easy Win"), OutcomeValue(name="Do First", key="F", description="Do First"), ), @@ -183,13 +188,12 @@ THE_PARANOIDS = OutcomeGroup( name="theParanoids", + key="PARANOIDS", description="PrioritizedRiskRemediation outcome group based on TheParanoids.", version="1.0.0", outcomes=( OutcomeValue(name="Track 5", key="5", description="Track"), - OutcomeValue( - name="Track Closely 4", key="4", description="Track Closely" - ), + OutcomeValue(name="Track Closely 4", key="4", description="Track Closely"), OutcomeValue(name="Attend 3", key="3", description="Attend"), OutcomeValue(name="Attend 2", key="2", description="Attend"), OutcomeValue(name="Act 1", key="1", description="Act"), diff --git a/src/test/test_outcomes.py b/src/test/test_outcomes.py index 4f5738e9..0095c676 100644 --- a/src/test/test_outcomes.py +++ b/src/test/test_outcomes.py @@ -32,10 +32,14 @@ def test_outcome_group(self): values.append(OutcomeValue(key=x, name=x, description=x)) og = OutcomeGroup( - name="og", description="an outcome group", outcomes=tuple(values) + name="Outcome Group", + key="OG", + description="an outcome group", + outcomes=tuple(values), ) - self.assertEqual(og.name, "og") + self.assertEqual(og.name, "Outcome Group") + self.assertEqual(og.key, "OG") self.assertEqual(og.description, "an outcome group") self.assertEqual(len(og), len(ALPHABET)) diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 18162550..65132436 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -33,9 +33,9 @@ def setUp(self) -> None: self.og = OutcomeGroup( name="test", description="test", + key="TEST", outcomes=[ - OutcomeValue(key=c, name=c, description=c) - for c in self.og_names + OutcomeValue(key=c, name=c, description=c) for c in self.og_names ], ) self.dpg = SsvcDecisionPointGroup( @@ -318,12 +318,8 @@ def test_confirm_topological_order(self): self.assertIsNone(pg._confirm_topological_order([0, 1, 2, 3, 4, 5])) self.assertIsNone(pg._confirm_topological_order([0, 1, 3, 2, 4, 5])) - self.assertRaises( - ValueError, pg._confirm_topological_order, [0, 1, 2, 4, 3, 5] - ) - self.assertRaises( - ValueError, pg._confirm_topological_order, [0, 1, 2, 3, 5] - ) + self.assertRaises(ValueError, pg._confirm_topological_order, [0, 1, 2, 4, 3, 5]) + self.assertRaises(ValueError, pg._confirm_topological_order, [0, 1, 2, 3, 5]) if __name__ == "__main__": From e8c94dd70131e2ee9aa528e082abcca7887c24d1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 14:39:41 -0500 Subject: [PATCH 07/99] refactor DecisionFramework for testability --- src/ssvc/framework/decision_framework.py | 39 +++++++++++++++++++----- src/test/test_decision_framework.py | 27 ++++++++-------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/ssvc/framework/decision_framework.py b/src/ssvc/framework/decision_framework.py index 4d4ffa18..2a318f05 100644 --- a/src/ssvc/framework/decision_framework.py +++ b/src/ssvc/framework/decision_framework.py @@ -20,7 +20,7 @@ from ssvc._mixins import _Base, _Namespaced, _Versioned from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue +from ssvc.outcomes.base import OutcomeGroup from ssvc.policy_generator import PolicyGenerator @@ -37,12 +37,19 @@ class DecisionFramework(_Versioned, _Namespaced, _Base, BaseModel): decision_point_group: SsvcDecisionPointGroup outcome_group: OutcomeGroup - mapping: dict[tuple[tuple[str, str], ...], OutcomeValue] + mapping: dict[str, str] - def populate_mapping(self): + def __init__(self, **data): + super().__init__(**data) + + if not self.mapping: + self.mapping = self.generate_mapping() + + def generate_mapping(self) -> dict[str, str]: """ Populate the mapping with all possible combinations of decision points. """ + mapping = {} dp_lookup = { dp.name.lower(): dp for dp in self.decision_point_group.decision_points } @@ -84,13 +91,18 @@ def populate_mapping(self): dp = dp_lookup[col] val = value_lookup[val] - k = (f"{dp.name}:{dp.version}", f"{val.name}") + key_delim = ":" + k = key_delim.join([dp.namespace, dp.key, val.key]) dp_values.append(k) - key = tuple(dp_values) - self.mapping[key] = outcome + key = ",".join([str(k) for k in dp_values]) + + outcome_group = self.outcome_group + outcome_str = ":".join([outcome_group.key, outcome.key]) + + mapping[key] = outcome_str - return self.mapping + return mapping # convenience alias @@ -98,7 +110,18 @@ def populate_mapping(self): def main(): - pass + from ssvc.dp_groups.ssvc.supplier import LATEST as dpg + from ssvc.outcomes.groups import MOSCOW as og + + dfw = DecisionFramework( + name="Example Decision Framework", + description="The description for an Example Decision Framework", + version="1.0.0", + decision_point_group=dpg, + outcome_group=og, + mapping={}, + ) + print(dfw.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/test/test_decision_framework.py b/src/test/test_decision_framework.py index 1551d6b2..23e336c0 100644 --- a/src/test/test_decision_framework.py +++ b/src/test/test_decision_framework.py @@ -18,7 +18,6 @@ from ssvc.decision_points.system_exposure import LATEST as exposure_dp from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.framework.decision_framework import DecisionFramework -from ssvc.outcomes.base import OutcomeGroup from ssvc.outcomes.groups import DSOI as dsoi_og @@ -33,11 +32,7 @@ def setUp(self): description="Test Decision Point Group Description", decision_points=[exploitation_dp, exposure_dp, safety_dp], ), - outcome_group=OutcomeGroup( - name="Test Outcome Group", - description="Test Outcome Group Description", - outcomes=dsoi_og, - ), + outcome_group=dsoi_og, mapping={}, ) @@ -51,7 +46,7 @@ def test_create(self): self.assertEqual(3, len(self.framework.decision_point_group)) def test_populate_mapping(self): - result = self.framework.populate_mapping() + result = self.framework.generate_mapping() # there should be one row in result for each combination of decision points combo_count = len(list(self.framework.decision_point_group.combinations())) @@ -59,14 +54,20 @@ def test_populate_mapping(self): # the length of each key should be the number of decision points for key in result.keys(): - self.assertEqual(len(key), 3) - for i, (dp_name_version, dp_value_name) in enumerate(key): + parts = key.split(",") + self.assertEqual(len(parts), 3) + for i, keypart in enumerate(parts): + dp_namespace, dp_key, dp_value_key = keypart.split(":") + dp = self.framework.decision_point_group.decision_points[i] - name_version = f"{dp.name}:{dp.version}" - self.assertEqual(name_version, dp_name_version) + self.assertEqual(dp_namespace, dp.namespace) + self.assertEqual(dp_key, dp.key) + value_keys = [v.key for v in dp.values] + self.assertIn(dp_value_key, value_keys) - value_names = [v.name for v in dp.values] - self.assertIn(dp_value_name, value_names) + print() + print() + print(self.framework.model_dump_json(indent=2)) if __name__ == "__main__": From e0af9c27fb604947d7d273905781c5e108e69a43 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:15:10 -0500 Subject: [PATCH 08/99] replace list with tuple to match type hint --- src/ssvc/policy_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 6cce2921..c7f58b77 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -343,12 +343,12 @@ def main(): name="Dummy Decision Point Group", description="Dummy decision point group", version="1.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1_0_1, AUTOMATABLE_2, HUMAN_IMPACT_2, - ], + ), ) with PolicyGenerator( From 85af2096c6d9eb8f0c158773bc9beaf2cd82b7c2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:15:30 -0500 Subject: [PATCH 09/99] black reformat --- src/ssvc/csv_analyzer.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/ssvc/csv_analyzer.py b/src/ssvc/csv_analyzer.py index 159d43dc..239309ab 100644 --- a/src/ssvc/csv_analyzer.py +++ b/src/ssvc/csv_analyzer.py @@ -67,6 +67,9 @@ logger = logging.getLogger(__name__) +# set an option to avoid a deprecation warning +pd.set_option("future.no_silent_downcasting", True) + def _col_norm(c: str) -> str: """ @@ -95,9 +98,7 @@ def _imp_df(column_names: list, importances: list) -> pd.DataFrame: a dataframe of feature importances """ df = ( - pd.DataFrame( - {"feature": column_names, "feature_importance": importances} - ) + pd.DataFrame({"feature": column_names, "feature_importance": importances}) .sort_values("feature_importance", ascending=False) .reset_index(drop=True) ) @@ -186,9 +187,7 @@ def _perm_feat_imp(model, x, y): def _parse_args(args) -> argparse.Namespace: # parse command line - parser = argparse.ArgumentParser( - description="Analyze an SSVC tree csv file" - ) + parser = argparse.ArgumentParser(description="Analyze an SSVC tree csv file") parser.add_argument( "csvfile", metavar="csvfile", type=str, help="the csv file to analyze" ) @@ -375,12 +374,8 @@ def check_topological_order(df, target): for u in H.nodes: H.nodes[u]["outcome"] = G.nodes[u]["outcome"] - logger.debug( - f"Original graph: {len(G.nodes)} nodes with {len(G.edges)} edges" - ) - logger.debug( - f"Reduced graph: {len(H.nodes)} nodes with {len(H.edges)} edges" - ) + logger.debug(f"Original graph: {len(G.nodes)} nodes with {len(G.edges)} edges") + logger.debug(f"Reduced graph: {len(H.nodes)} nodes with {len(H.edges)} edges") problems = [] # check if the outcome is topologically sorted From 1139ca0651b26bcf60720526266012831887c926 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:15:59 -0500 Subject: [PATCH 10/99] add validator method to check mapping --- src/ssvc/framework/decision_framework.py | 59 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/ssvc/framework/decision_framework.py b/src/ssvc/framework/decision_framework.py index 2a318f05..7c974e88 100644 --- a/src/ssvc/framework/decision_framework.py +++ b/src/ssvc/framework/decision_framework.py @@ -15,14 +15,19 @@ """ Provides a Decision Framework class that can be used to model decisions in SSVC """ +import logging + import pandas as pd -from pydantic import BaseModel +from pydantic import BaseModel, field_validator from ssvc._mixins import _Base, _Namespaced, _Versioned +from ssvc.csv_analyzer import check_topological_order from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.base import OutcomeGroup from ssvc.policy_generator import PolicyGenerator +logger = logging.getLogger(__name__) + class DecisionFramework(_Versioned, _Namespaced, _Base, BaseModel): """ @@ -43,7 +48,53 @@ def __init__(self, **data): super().__init__(**data) if not self.mapping: - self.mapping = self.generate_mapping() + mapping = self.generate_mapping() + self.__class__.validate_mapping(mapping) + self.mapping = mapping + + # stub for validating mapping + @field_validator("mapping", mode="before") + @classmethod + def validate_mapping(cls, data): + """ + Placeholder for validating the mapping. + """ + if len(data) == 0: + return data + + # extract column names from keys + values = {} + target = None + + for key, value in data.items(): + key = key.lower() + value = value.lower() + + parts = key.split(",") + for part in parts: + (ns, dp, val) = part.split(":") + if dp not in values: + values[dp] = [] + values[dp].append(val) + + (og_key, og_valkey) = value.split(":") + if og_key not in values: + values[og_key] = [] + + values[og_key].append(og_valkey) + target = og_key + + # now values is a dict of columnar data + df = pd.DataFrame(values) + + problems: list = check_topological_order(df, target) + + if problems: + raise ValueError(f"Mapping has problems: {problems}") + else: + logger.debug("Mapping passes topological order check") + + return data def generate_mapping(self) -> dict[str, str]: """ @@ -113,6 +164,10 @@ def main(): from ssvc.dp_groups.ssvc.supplier import LATEST as dpg from ssvc.outcomes.groups import MOSCOW as og + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + dfw = DecisionFramework( name="Example Decision Framework", description="The description for an Example Decision Framework", From f51b3f291d89b865e5d990084d3f5b8d62146bb1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:29:07 -0500 Subject: [PATCH 11/99] refactor --- src/ssvc/framework/decision_framework.py | 48 +++++++++++++++++------- src/test/test_decision_framework.py | 8 ++-- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/ssvc/framework/decision_framework.py b/src/ssvc/framework/decision_framework.py index 7c974e88..d3fedb4b 100644 --- a/src/ssvc/framework/decision_framework.py +++ b/src/ssvc/framework/decision_framework.py @@ -52,19 +52,30 @@ def __init__(self, **data): self.__class__.validate_mapping(mapping) self.mapping = mapping - # stub for validating mapping - @field_validator("mapping", mode="before") @classmethod - def validate_mapping(cls, data): + def mapping_to_table(cls, data: dict) -> pd.DataFrame: """ - Placeholder for validating the mapping. + Convert the mapping to a pandas DataFrame. """ - if len(data) == 0: - return data - # extract column names from keys values = {} - target = None + + cols = [] + for key in data.keys(): + parts = key.split(",") + for part in parts: + (_, dp, _) = part.split(":") + cols.append(dp) + + # add the outcome column + first_value = list(data.values())[0] + (okey, _) = first_value.split(":") + cols.append(okey) + + # set up the lists for the columns + for col in cols: + col = col.lower() + values[col] = [] for key, value in data.items(): key = key.lower() @@ -73,19 +84,28 @@ def validate_mapping(cls, data): parts = key.split(",") for part in parts: (ns, dp, val) = part.split(":") - if dp not in values: - values[dp] = [] values[dp].append(val) (og_key, og_valkey) = value.split(":") - if og_key not in values: - values[og_key] = [] - values[og_key].append(og_valkey) - target = og_key # now values is a dict of columnar data df = pd.DataFrame(values) + # the last column is the outcome + return df + + # stub for validating mapping + @field_validator("mapping", mode="before") + @classmethod + def validate_mapping(cls, data): + """ + Placeholder for validating the mapping. + """ + if len(data) == 0: + return data + + df = cls.mapping_to_table(data) + target = df.columns[-1] problems: list = check_topological_order(df, target) diff --git a/src/test/test_decision_framework.py b/src/test/test_decision_framework.py index 23e336c0..b11b00b7 100644 --- a/src/test/test_decision_framework.py +++ b/src/test/test_decision_framework.py @@ -44,8 +44,10 @@ def tearDown(self): def test_create(self): self.assertEqual(self.framework.name, "Test Decision Framework") self.assertEqual(3, len(self.framework.decision_point_group)) + # mapping should not be empty + self.assertGreater(len(self.framework.mapping), 0) - def test_populate_mapping(self): + def test_generate_mapping(self): result = self.framework.generate_mapping() # there should be one row in result for each combination of decision points @@ -65,10 +67,6 @@ def test_populate_mapping(self): value_keys = [v.key for v in dp.values] self.assertIn(dp_value_key, value_keys) - print() - print() - print(self.framework.model_dump_json(indent=2)) - if __name__ == "__main__": unittest.main() From 3cc382c514e1e8785578fbd826638e5f5b485cbc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:36:25 -0500 Subject: [PATCH 12/99] fixup type hints --- src/ssvc/dp_groups/base.py | 4 ++-- src/ssvc/dp_groups/ssvc/deployer.py | 9 +++++---- src/ssvc/dp_groups/ssvc/supplier.py | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index ffbbec8b..a2546635 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -73,7 +73,7 @@ def combo_strings(self) -> Generator[tuple[str, ...], None, None]: def get_all_decision_points_from( *groups: list[SsvcDecisionPointGroup], -) -> list[SsvcDecisionPoint]: +) -> tuple[SsvcDecisionPoint, ...]: """ Given a list of SsvcDecisionPointGroup objects, return a list of all the unique SsvcDecisionPoint objects contained in those groups. @@ -100,7 +100,7 @@ def get_all_decision_points_from( dps.append(dp) seen.add(key) - return list(dps) + return tuple(dps) def main(): diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index 2c5b111a..7f937479 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -38,12 +38,12 @@ name="SSVC Patch Applier", description="The decision points used by the patch applier.", version="1.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1, MISSION_IMPACT_1, SAFETY_IMPACT_1, - ], + ), ) """ In SSVC v1, Patch Applier v1 represents the decision points used by the patch applier. @@ -64,7 +64,7 @@ name="SSVC Deployer", description="The decision points used by the deployer.", version="2.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1_0_1, MISSION_IMPACT_1, @@ -73,7 +73,7 @@ AUTOMATABLE_2, VALUE_DENSITY_1, HUMAN_IMPACT_2, - ], + ), ) """ Deployer v2.0.0 is renamed from Patch Applier v1.0.0. @@ -125,6 +125,7 @@ VERSIONS = (PATCH_APPLIER_1, DEPLOYER_2, DEPLOYER_3) LATEST = VERSIONS[-1] + def main(): for version in VERSIONS: print(version.model_dump_json(indent=2)) diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index 18dca35f..b9e42ebb 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -60,14 +60,14 @@ name="SSVC Supplier", description="The decision points used by the supplier.", version="2.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, UTILITY_1_0_1, TECHNICAL_IMPACT_1, AUTOMATABLE_2, VALUE_DENSITY_1, SAFETY_IMPACT_1, - ], + ), ) """ In SSVC v2, Supplier v2 represents the decision points used by the supplier. @@ -92,6 +92,7 @@ VERSIONS = (PATCH_DEVELOPER_1, SUPPLIER_2) LATEST = VERSIONS[-1] + def main(): for version in VERSIONS: print(version.model_dump_json(indent=2)) From af28a08772f917c4f66dc3cd3a88e26378f70eb1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 3 Mar 2025 15:59:38 -0500 Subject: [PATCH 13/99] add test --- src/test/test_decision_framework.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/test_decision_framework.py b/src/test/test_decision_framework.py index b11b00b7..531b40d9 100644 --- a/src/test/test_decision_framework.py +++ b/src/test/test_decision_framework.py @@ -13,6 +13,8 @@ import unittest +import pandas as pd + from ssvc.decision_points.exploitation import LATEST as exploitation_dp from ssvc.decision_points.safety_impact import LATEST as safety_dp from ssvc.decision_points.system_exposure import LATEST as exposure_dp @@ -67,6 +69,30 @@ def test_generate_mapping(self): value_keys = [v.key for v in dp.values] self.assertIn(dp_value_key, value_keys) + def test_mapping_to_table(self): + d = { + "ssvc:One:A,ssvc:Two:B,ssvc:Three:C": "og:One", + "ssvc:One:A,ssvc:Two:B,ssvc:Three:D": "og:Two", + } + table = self.framework.mapping_to_table(d) + + # is it a DataFrame? + self.assertIsInstance(table, pd.DataFrame) + self.assertEqual(2, len(table)) + self.assertEqual(4, len(table.columns)) + + # does it have the right columns? + self.assertEqual(["one", "two", "three", "og"], list(table.columns)) + # does it have the right values? + for i, (k, v) in enumerate(d.items()): + k = k.lower() + v = v.lower() + + parts = k.split(",") + for part in parts: + (ns, dp, val) = part.split(":") + self.assertEqual(val, table.iloc[i][dp]) + if __name__ == "__main__": unittest.main() From 34632e8f42f1ccd7fa48fb72eaab97fbd525540b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 6 Mar 2025 12:00:31 -0500 Subject: [PATCH 14/99] rename DecisionFramework to PrioritizationFramework --- ...amework.py => prioritization_framework.py} | 20 ++++++++++++------- ...rk.py => test_prioritization_framework.py} | 10 +++++----- 2 files changed, 18 insertions(+), 12 deletions(-) rename src/ssvc/framework/{decision_framework.py => prioritization_framework.py} (91%) rename src/test/{test_decision_framework.py => test_prioritization_framework.py} (91%) diff --git a/src/ssvc/framework/decision_framework.py b/src/ssvc/framework/prioritization_framework.py similarity index 91% rename from src/ssvc/framework/decision_framework.py rename to src/ssvc/framework/prioritization_framework.py index d3fedb4b..c5e8ab5f 100644 --- a/src/ssvc/framework/decision_framework.py +++ b/src/ssvc/framework/prioritization_framework.py @@ -13,7 +13,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University """ -Provides a Decision Framework class that can be used to model decisions in SSVC +Provides a Prioritization Framework class that can be used to model decisions in SSVC """ import logging @@ -29,9 +29,9 @@ logger = logging.getLogger(__name__) -class DecisionFramework(_Versioned, _Namespaced, _Base, BaseModel): +class PrioritizationFramework(_Versioned, _Namespaced, _Base, BaseModel): """ - The DecisionFramework class is a model for decisions in SSVC. + The PrioritizationFramework class is a model for decisions in SSVC. It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. @@ -177,7 +177,7 @@ def generate_mapping(self) -> dict[str, str]: # convenience alias -Policy = DecisionFramework +Policy = PrioritizationFramework def main(): @@ -188,15 +188,21 @@ def main(): logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) - dfw = DecisionFramework( - name="Example Decision Framework", - description="The description for an Example Decision Framework", + dfw = PrioritizationFramework( + name="Example Prioritization Framework", + description="The description for an Example Prioritization Framework", version="1.0.0", decision_point_group=dpg, outcome_group=og, mapping={}, ) print(dfw.model_dump_json(indent=2)) + print() + print() + print("### JSON SCHEMA ###") + import json + + print(json.dumps(PrioritizationFramework.model_json_schema(), indent=2)) if __name__ == "__main__": diff --git a/src/test/test_decision_framework.py b/src/test/test_prioritization_framework.py similarity index 91% rename from src/test/test_decision_framework.py rename to src/test/test_prioritization_framework.py index 531b40d9..d041f795 100644 --- a/src/test/test_decision_framework.py +++ b/src/test/test_prioritization_framework.py @@ -19,15 +19,15 @@ from ssvc.decision_points.safety_impact import LATEST as safety_dp from ssvc.decision_points.system_exposure import LATEST as exposure_dp from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.framework.decision_framework import DecisionFramework +from ssvc.framework.prioritization_framework import PrioritizationFramework from ssvc.outcomes.groups import DSOI as dsoi_og class MyTestCase(unittest.TestCase): def setUp(self): - self.framework = DecisionFramework( - name="Test Decision Framework", - description="Test Decision Framework Description", + self.framework = PrioritizationFramework( + name="Test Prioritization Framework", + description="Test Prioritization Framework Description", version="1.0.0", decision_point_group=SsvcDecisionPointGroup( name="Test Decision Point Group", @@ -44,7 +44,7 @@ def tearDown(self): pass def test_create(self): - self.assertEqual(self.framework.name, "Test Decision Framework") + self.assertEqual(self.framework.name, "Test Prioritization Framework") self.assertEqual(3, len(self.framework.decision_point_group)) # mapping should not be empty self.assertGreater(len(self.framework.mapping), 0) From 57fb8a666418a54922b5061384ce1b97bc2d8da5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 10 Mar 2025 16:11:35 -0400 Subject: [PATCH 15/99] rename new class to DecisionTable --- src/ssvc/{framework => decision_tables}/__init__.py | 0 .../base.py} | 12 ++++++------ src/test/test_prioritization_framework.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/ssvc/{framework => decision_tables}/__init__.py (100%) rename src/ssvc/{framework/prioritization_framework.py => decision_tables/base.py} (94%) diff --git a/src/ssvc/framework/__init__.py b/src/ssvc/decision_tables/__init__.py similarity index 100% rename from src/ssvc/framework/__init__.py rename to src/ssvc/decision_tables/__init__.py diff --git a/src/ssvc/framework/prioritization_framework.py b/src/ssvc/decision_tables/base.py similarity index 94% rename from src/ssvc/framework/prioritization_framework.py rename to src/ssvc/decision_tables/base.py index c5e8ab5f..edfc5ad8 100644 --- a/src/ssvc/framework/prioritization_framework.py +++ b/src/ssvc/decision_tables/base.py @@ -13,7 +13,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University """ -Provides a Prioritization Framework class that can be used to model decisions in SSVC +Provides a DecisionTable class that can be used to model decisions in SSVC """ import logging @@ -29,9 +29,9 @@ logger = logging.getLogger(__name__) -class PrioritizationFramework(_Versioned, _Namespaced, _Base, BaseModel): +class DecisionTable(_Versioned, _Namespaced, _Base, BaseModel): """ - The PrioritizationFramework class is a model for decisions in SSVC. + The DecisionTable class is a model for decisions in SSVC. It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. @@ -177,7 +177,7 @@ def generate_mapping(self) -> dict[str, str]: # convenience alias -Policy = PrioritizationFramework +Policy = DecisionTable def main(): @@ -188,7 +188,7 @@ def main(): logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) - dfw = PrioritizationFramework( + dfw = DecisionTable( name="Example Prioritization Framework", description="The description for an Example Prioritization Framework", version="1.0.0", @@ -202,7 +202,7 @@ def main(): print("### JSON SCHEMA ###") import json - print(json.dumps(PrioritizationFramework.model_json_schema(), indent=2)) + print(json.dumps(DecisionTable.model_json_schema(), indent=2)) if __name__ == "__main__": diff --git a/src/test/test_prioritization_framework.py b/src/test/test_prioritization_framework.py index d041f795..83f7f046 100644 --- a/src/test/test_prioritization_framework.py +++ b/src/test/test_prioritization_framework.py @@ -18,14 +18,14 @@ from ssvc.decision_points.exploitation import LATEST as exploitation_dp from ssvc.decision_points.safety_impact import LATEST as safety_dp from ssvc.decision_points.system_exposure import LATEST as exposure_dp +from ssvc.decision_tables.base import DecisionTable from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.framework.prioritization_framework import PrioritizationFramework from ssvc.outcomes.groups import DSOI as dsoi_og class MyTestCase(unittest.TestCase): def setUp(self): - self.framework = PrioritizationFramework( + self.framework = DecisionTable( name="Test Prioritization Framework", description="Test Prioritization Framework Description", version="1.0.0", From aa5040fc74caaa03493f6b5226d68d73d125ab35 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 09:46:08 -0400 Subject: [PATCH 16/99] create a `_Valued` mixin --- src/ssvc/_mixins.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 414c99e1..10b19973 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -65,6 +65,20 @@ class _Keyed(BaseModel): key: str +class _Valued(BaseModel): + """ + Mixin class for valued SSVC objects. + """ + + values: tuple + + def __iter__(self): + """ + Allow iteration over the values in the object. + """ + return iter(self.values) + + def exclude_if_none(value): return value is None From f46b42010bc5a44cf91c714dc8c52cef381da567 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 09:47:25 -0400 Subject: [PATCH 17/99] add `_Valued` mixin to base decision point class. Also reorder mixins to adjust default json output key order --- src/ssvc/decision_points/base.py | 12 +++--------- src/test/test_doctools.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 869e3263..af511c91 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -20,7 +20,7 @@ from pydantic import BaseModel -from ssvc._mixins import _Base, _Keyed, _Namespaced, _Versioned +from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned logger = logging.getLogger(__name__) @@ -61,18 +61,12 @@ class SsvcDecisionPointValue(_Base, _Keyed, BaseModel): """ -class SsvcDecisionPoint(_Base, _Keyed, _Versioned, _Namespaced, BaseModel): +class SsvcDecisionPoint(_Valued, _Keyed, _Versioned, _Namespaced, _Base, BaseModel): """ Models a single decision point as a list of values. """ - values: list[SsvcDecisionPointValue] = [] - - def __iter__(self): - """ - Allow iteration over the decision points in the group. - """ - return iter(self.values) + values: tuple[SsvcDecisionPointValue, ...] def __init__(self, **data): super().__init__(**data) diff --git a/src/test/test_doctools.py b/src/test/test_doctools.py index c59226a5..70fba2f9 100644 --- a/src/test/test_doctools.py +++ b/src/test/test_doctools.py @@ -31,10 +31,10 @@ "key": "DPT", "name": "Decision Point Test", "description": "This is a test decision point.", - "values": [ + "values": ( {"key": "N", "name": "No", "description": "No means no"}, {"key": "Y", "name": "Yes", "description": "Yes means yes"}, - ], + ), } @@ -122,7 +122,12 @@ def test_dump_json(self): # file is loadable json d = json.load(open(json_file)) for k, v in dp.model_dump().items(): - self.assertEqual(v, d[k]) + # on reload, the tuples are lists, but they should be the same + reloaded_value = d[k] + if isinstance(reloaded_value, list): + reloaded_value = tuple(reloaded_value) + + self.assertEqual(v, reloaded_value) # should not overwrite the file overwrite = False From 03d103a32fe75c58ccbe612bb98b5dbb1835cbc8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 09:47:51 -0400 Subject: [PATCH 18/99] update json examples to reflect new base class mixin ordering --- data/json/decision_points/automatable_2_0_0.json | 4 ++-- data/json/decision_points/cvss/access_complexity_1_0_0.json | 4 ++-- data/json/decision_points/cvss/access_complexity_2_0_0.json | 4 ++-- data/json/decision_points/cvss/access_vector_1_0_0.json | 4 ++-- data/json/decision_points/cvss/access_vector_2_0_0.json | 4 ++-- data/json/decision_points/cvss/attack_complexity_3_0_0.json | 4 ++-- data/json/decision_points/cvss/attack_complexity_3_0_1.json | 4 ++-- data/json/decision_points/cvss/attack_requirements_1_0_0.json | 4 ++-- data/json/decision_points/cvss/attack_vector_3_0_0.json | 4 ++-- data/json/decision_points/cvss/attack_vector_3_0_1.json | 4 ++-- data/json/decision_points/cvss/authentication_1_0_0.json | 4 ++-- data/json/decision_points/cvss/authentication_2_0_0.json | 4 ++-- data/json/decision_points/cvss/automatable_1_0_0.json | 4 ++-- data/json/decision_points/cvss/availability_impact_1_0_0.json | 4 ++-- data/json/decision_points/cvss/availability_impact_2_0_0.json | 4 ++-- .../availability_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- .../availability_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../decision_points/cvss/availability_requirement_1_0_0.json | 4 ++-- .../decision_points/cvss/availability_requirement_1_1_0.json | 4 ++-- .../decision_points/cvss/availability_requirement_1_1_1.json | 4 ++-- .../cvss/collateral_damage_potential_1_0_0.json | 4 ++-- .../cvss/collateral_damage_potential_2_0_0.json | 4 ++-- .../decision_points/cvss/confidentiality_impact_1_0_0.json | 4 ++-- .../decision_points/cvss/confidentiality_impact_2_0_0.json | 4 ++-- ...confidentiality_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- ...confidentiality_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../cvss/confidentiality_requirement_1_0_0.json | 4 ++-- .../cvss/confidentiality_requirement_1_1_0.json | 4 ++-- .../cvss/confidentiality_requirement_1_1_1.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_1_1_0_0.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_2_1_0_0.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_3_1_0_0.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_4_1_0_0.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_5_1_0_0.json | 4 ++-- data/json/decision_points/cvss/equivalence_set_6_1_0_0.json | 4 ++-- .../decision_points/cvss/exploit_code_maturity_1_2_0.json | 4 ++-- data/json/decision_points/cvss/exploit_maturity_2_0_0.json | 4 ++-- data/json/decision_points/cvss/exploitability_1_0_0.json | 4 ++-- data/json/decision_points/cvss/exploitability_1_1_0.json | 4 ++-- data/json/decision_points/cvss/impact_bias_1_0_0.json | 4 ++-- data/json/decision_points/cvss/integrity_impact_1_0_0.json | 4 ++-- data/json/decision_points/cvss/integrity_impact_2_0_0.json | 4 ++-- .../cvss/integrity_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- .../cvss/integrity_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../decision_points/cvss/integrity_requirement_1_0_0.json | 4 ++-- .../decision_points/cvss/integrity_requirement_1_1_0.json | 4 ++-- .../decision_points/cvss/integrity_requirement_1_1_1.json | 4 ++-- .../cvss/modified_attack_complexity_3_0_0.json | 4 ++-- .../cvss/modified_attack_complexity_3_0_1.json | 4 ++-- .../cvss/modified_attack_requirements_1_0_0.json | 4 ++-- .../decision_points/cvss/modified_attack_vector_3_0_0.json | 4 ++-- .../decision_points/cvss/modified_attack_vector_3_0_1.json | 4 ++-- .../cvss/modified_availability_impact_2_0_0.json | 4 ++-- ...ed_availability_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- ...ed_availability_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../cvss/modified_confidentiality_impact_2_0_0.json | 4 ++-- ...confidentiality_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- ...confidentiality_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../decision_points/cvss/modified_integrity_impact_2_0_0.json | 4 ++-- ...ified_integrity_impact_to_the_subsequent_system_1_0_0.json | 4 ++-- ...ified_integrity_impact_to_the_vulnerable_system_3_0_0.json | 4 ++-- .../cvss/modified_privileges_required_1_0_0.json | 4 ++-- .../cvss/modified_privileges_required_1_0_1.json | 4 ++-- data/json/decision_points/cvss/modified_scope_1_0_0.json | 4 ++-- .../decision_points/cvss/modified_user_interaction_1_0_0.json | 4 ++-- .../decision_points/cvss/modified_user_interaction_2_0_0.json | 4 ++-- data/json/decision_points/cvss/privileges_required_1_0_0.json | 4 ++-- data/json/decision_points/cvss/privileges_required_1_0_1.json | 4 ++-- data/json/decision_points/cvss/provider_urgency_1_0_0.json | 4 ++-- data/json/decision_points/cvss/recovery_1_0_0.json | 4 ++-- data/json/decision_points/cvss/remediation_level_1_0_0.json | 4 ++-- data/json/decision_points/cvss/remediation_level_1_1_0.json | 4 ++-- data/json/decision_points/cvss/report_confidence_1_0_0.json | 4 ++-- data/json/decision_points/cvss/report_confidence_1_1_0.json | 4 ++-- data/json/decision_points/cvss/report_confidence_2_0_0.json | 4 ++-- data/json/decision_points/cvss/safety_1_0_0.json | 4 ++-- data/json/decision_points/cvss/scope_1_0_0.json | 4 ++-- data/json/decision_points/cvss/target_distribution_1_0_0.json | 4 ++-- data/json/decision_points/cvss/target_distribution_1_1_0.json | 4 ++-- data/json/decision_points/cvss/user_interaction_1_0_0.json | 4 ++-- data/json/decision_points/cvss/user_interaction_2_0_0.json | 4 ++-- data/json/decision_points/cvss/value_density_1_0_0.json | 4 ++-- .../cvss/vulnerability_response_effort_1_0_0.json | 4 ++-- data/json/decision_points/exploitation_1_0_0.json | 4 ++-- data/json/decision_points/exploitation_1_1_0.json | 4 ++-- data/json/decision_points/human_impact_2_0_0.json | 4 ++-- data/json/decision_points/human_impact_2_0_1.json | 4 ++-- .../decision_points/mission_and_well-being_impact_1_0_0.json | 4 ++-- data/json/decision_points/mission_impact_1_0_0.json | 4 ++-- data/json/decision_points/mission_impact_2_0_0.json | 4 ++-- data/json/decision_points/public_safety_impact_2_0_0.json | 4 ++-- data/json/decision_points/public_safety_impact_2_0_1.json | 4 ++-- data/json/decision_points/public_value_added_1_0_0.json | 4 ++-- data/json/decision_points/public_well-being_impact_1_0_0.json | 4 ++-- data/json/decision_points/report_credibility_1_0_0.json | 4 ++-- data/json/decision_points/report_public_1_0_0.json | 4 ++-- data/json/decision_points/safety_impact_1_0_0.json | 4 ++-- data/json/decision_points/safety_impact_2_0_0.json | 4 ++-- data/json/decision_points/supplier_cardinality_1_0_0.json | 4 ++-- data/json/decision_points/supplier_contacted_1_0_0.json | 4 ++-- data/json/decision_points/supplier_engagement_1_0_0.json | 4 ++-- data/json/decision_points/supplier_involvement_1_0_0.json | 4 ++-- data/json/decision_points/system_exposure_1_0_0.json | 4 ++-- data/json/decision_points/system_exposure_1_0_1.json | 4 ++-- data/json/decision_points/technical_impact_1_0_0.json | 4 ++-- data/json/decision_points/utility_1_0_0.json | 4 ++-- data/json/decision_points/utility_1_0_1.json | 4 ++-- data/json/decision_points/value_density_1_0_0.json | 4 ++-- data/json/decision_points/virulence_1_0_0.json | 4 ++-- 109 files changed, 218 insertions(+), 218 deletions(-) diff --git a/data/json/decision_points/automatable_2_0_0.json b/data/json/decision_points/automatable_2_0_0.json index a44086f9..5a0528d8 100644 --- a/data/json/decision_points/automatable_2_0_0.json +++ b/data/json/decision_points/automatable_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Automatable", + "description": "Can an attacker reliably automate creating exploitation events for this vulnerability?", "namespace": "ssvc", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "A", - "name": "Automatable", - "description": "Can an attacker reliably automate creating exploitation events for this vulnerability?", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/access_complexity_1_0_0.json b/data/json/decision_points/cvss/access_complexity_1_0_0.json index 30e88f11..b07e7595 100644 --- a/data/json/decision_points/cvss/access_complexity_1_0_0.json +++ b/data/json/decision_points/cvss/access_complexity_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Access Complexity", + "description": "This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "AC", - "name": "Access Complexity", - "description": "This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/access_complexity_2_0_0.json b/data/json/decision_points/cvss/access_complexity_2_0_0.json index 09c795fc..15fec7b8 100644 --- a/data/json/decision_points/cvss/access_complexity_2_0_0.json +++ b/data/json/decision_points/cvss/access_complexity_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Access Complexity", + "description": "This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "AC", - "name": "Access Complexity", - "description": "This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/access_vector_1_0_0.json b/data/json/decision_points/cvss/access_vector_1_0_0.json index beee709d..55d6d8c6 100644 --- a/data/json/decision_points/cvss/access_vector_1_0_0.json +++ b/data/json/decision_points/cvss/access_vector_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Access Vector", + "description": "This metric measures whether or not the vulnerability is exploitable locally or remotely.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "AV", - "name": "Access Vector", - "description": "This metric measures whether or not the vulnerability is exploitable locally or remotely.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/access_vector_2_0_0.json b/data/json/decision_points/cvss/access_vector_2_0_0.json index 9f68fb5a..14918e5c 100644 --- a/data/json/decision_points/cvss/access_vector_2_0_0.json +++ b/data/json/decision_points/cvss/access_vector_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Access Vector", + "description": "This metric reflects the context by which vulnerability exploitation is possible.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "AV", - "name": "Access Vector", - "description": "This metric reflects the context by which vulnerability exploitation is possible.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/attack_complexity_3_0_0.json b/data/json/decision_points/cvss/attack_complexity_3_0_0.json index b9dd8584..e2ef4655 100644 --- a/data/json/decision_points/cvss/attack_complexity_3_0_0.json +++ b/data/json/decision_points/cvss/attack_complexity_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Attack Complexity", + "description": "This metric describes the conditions beyond the attacker's control that must exist in order to exploit the vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "AC", - "name": "Attack Complexity", - "description": "This metric describes the conditions beyond the attacker's control that must exist in order to exploit the vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/attack_complexity_3_0_1.json b/data/json/decision_points/cvss/attack_complexity_3_0_1.json index 7f49cf1d..a3469f1b 100644 --- a/data/json/decision_points/cvss/attack_complexity_3_0_1.json +++ b/data/json/decision_points/cvss/attack_complexity_3_0_1.json @@ -1,10 +1,10 @@ { + "name": "Attack Complexity", + "description": "This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. ", "namespace": "cvss", "version": "3.0.1", "schemaVersion": "1-0-1", "key": "AC", - "name": "Attack Complexity", - "description": "This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. ", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/attack_requirements_1_0_0.json b/data/json/decision_points/cvss/attack_requirements_1_0_0.json index 4232fa7b..eaff05de 100644 --- a/data/json/decision_points/cvss/attack_requirements_1_0_0.json +++ b/data/json/decision_points/cvss/attack_requirements_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Attack Requirements", + "description": "This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "AT", - "name": "Attack Requirements", - "description": "This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/attack_vector_3_0_0.json b/data/json/decision_points/cvss/attack_vector_3_0_0.json index 612e5c72..3db17af6 100644 --- a/data/json/decision_points/cvss/attack_vector_3_0_0.json +++ b/data/json/decision_points/cvss/attack_vector_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Attack Vector", + "description": "This metric reflects the context by which vulnerability exploitation is possible. ", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "AV", - "name": "Attack Vector", - "description": "This metric reflects the context by which vulnerability exploitation is possible. ", "values": [ { "key": "P", diff --git a/data/json/decision_points/cvss/attack_vector_3_0_1.json b/data/json/decision_points/cvss/attack_vector_3_0_1.json index fbf31693..fe2baea6 100644 --- a/data/json/decision_points/cvss/attack_vector_3_0_1.json +++ b/data/json/decision_points/cvss/attack_vector_3_0_1.json @@ -1,10 +1,10 @@ { + "name": "Attack Vector", + "description": "This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.", "namespace": "cvss", "version": "3.0.1", "schemaVersion": "1-0-1", "key": "AV", - "name": "Attack Vector", - "description": "This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.", "values": [ { "key": "P", diff --git a/data/json/decision_points/cvss/authentication_1_0_0.json b/data/json/decision_points/cvss/authentication_1_0_0.json index 0e2f41e7..a2bedd42 100644 --- a/data/json/decision_points/cvss/authentication_1_0_0.json +++ b/data/json/decision_points/cvss/authentication_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Authentication", + "description": "This metric measures whether or not an attacker needs to be authenticated to the target system in order to exploit the vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "Au", - "name": "Authentication", - "description": "This metric measures whether or not an attacker needs to be authenticated to the target system in order to exploit the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/authentication_2_0_0.json b/data/json/decision_points/cvss/authentication_2_0_0.json index 98a1037b..f618747f 100644 --- a/data/json/decision_points/cvss/authentication_2_0_0.json +++ b/data/json/decision_points/cvss/authentication_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Authentication", + "description": "This metric measures the number of times an attacker must authenticate to a target in order to exploit a vulnerability. This metric does not gauge the strength or complexity of the authentication process, only that an attacker is required to provide credentials before an exploit may occur. The possible values for this metric are listed in Table 3. The fewer authentication instances that are required, the higher the vulnerability score.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "Au", - "name": "Authentication", - "description": "This metric measures the number of times an attacker must authenticate to a target in order to exploit a vulnerability. This metric does not gauge the strength or complexity of the authentication process, only that an attacker is required to provide credentials before an exploit may occur. The possible values for this metric are listed in Table 3. The fewer authentication instances that are required, the higher the vulnerability score.", "values": [ { "key": "M", diff --git a/data/json/decision_points/cvss/automatable_1_0_0.json b/data/json/decision_points/cvss/automatable_1_0_0.json index 1963318c..03956092 100644 --- a/data/json/decision_points/cvss/automatable_1_0_0.json +++ b/data/json/decision_points/cvss/automatable_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Automatable", + "description": "The \"Automatable\" metric captures the answer to the question \"Can an attacker automate exploitation events for this vulnerability across multiple targets?\" based on steps 1-4 of the kill chain.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "AU", - "name": "Automatable", - "description": "The \"Automatable\" metric captures the answer to the question \"Can an attacker automate exploitation events for this vulnerability across multiple targets?\" based on steps 1-4 of the kill chain.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/availability_impact_1_0_0.json b/data/json/decision_points/cvss/availability_impact_1_0_0.json index 4c2b59e3..ad667d01 100644 --- a/data/json/decision_points/cvss/availability_impact_1_0_0.json +++ b/data/json/decision_points/cvss/availability_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Impact", + "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the target system.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "A", - "name": "Availability Impact", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the target system.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/availability_impact_2_0_0.json b/data/json/decision_points/cvss/availability_impact_2_0_0.json index f3b37b02..7fd162ed 100644 --- a/data/json/decision_points/cvss/availability_impact_2_0_0.json +++ b/data/json/decision_points/cvss/availability_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Impact", + "description": "This metric measures the impact to availability of a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "A", - "name": "Availability Impact", - "description": "This metric measures the impact to availability of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/availability_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/availability_impact_to_the_subsequent_system_1_0_0.json index be7cedbe..79369891 100644 --- a/data/json/decision_points/cvss/availability_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/availability_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Impact to the Subsequent System", + "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SA", - "name": "Availability Impact to the Subsequent System", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/availability_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/availability_impact_to_the_vulnerable_system_3_0_0.json index ebef410c..4e999e21 100644 --- a/data/json/decision_points/cvss/availability_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/availability_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Impact to the Vulnerable System", + "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "VA", - "name": "Availability Impact to the Vulnerable System", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/availability_requirement_1_0_0.json b/data/json/decision_points/cvss/availability_requirement_1_0_0.json index cbffe72a..01bd1da6 100644 --- a/data/json/decision_points/cvss/availability_requirement_1_0_0.json +++ b/data/json/decision_points/cvss/availability_requirement_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Requirement", + "description": "This metric measures the impact to the availability of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "AR", - "name": "Availability Requirement", - "description": "This metric measures the impact to the availability of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/availability_requirement_1_1_0.json b/data/json/decision_points/cvss/availability_requirement_1_1_0.json index 66dec4d4..28045aa0 100644 --- a/data/json/decision_points/cvss/availability_requirement_1_1_0.json +++ b/data/json/decision_points/cvss/availability_requirement_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Availability Requirement", + "description": "This metric measures the impact to the availability of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "AR", - "name": "Availability Requirement", - "description": "This metric measures the impact to the availability of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/availability_requirement_1_1_1.json b/data/json/decision_points/cvss/availability_requirement_1_1_1.json index 9e4a94fe..cb041336 100644 --- a/data/json/decision_points/cvss/availability_requirement_1_1_1.json +++ b/data/json/decision_points/cvss/availability_requirement_1_1_1.json @@ -1,10 +1,10 @@ { + "name": "Availability Requirement", + "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Availability.", "namespace": "cvss", "version": "1.1.1", "schemaVersion": "1-0-1", "key": "AR", - "name": "Availability Requirement", - "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Availability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/collateral_damage_potential_1_0_0.json b/data/json/decision_points/cvss/collateral_damage_potential_1_0_0.json index b650ad2f..19666f0f 100644 --- a/data/json/decision_points/cvss/collateral_damage_potential_1_0_0.json +++ b/data/json/decision_points/cvss/collateral_damage_potential_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Collateral Damage Potential", + "description": "This metric measures the potential for a loss in physical equipment, property damage or loss of life or limb.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "CDP", - "name": "Collateral Damage Potential", - "description": "This metric measures the potential for a loss in physical equipment, property damage or loss of life or limb.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/collateral_damage_potential_2_0_0.json b/data/json/decision_points/cvss/collateral_damage_potential_2_0_0.json index c08f0fe8..00206e66 100644 --- a/data/json/decision_points/cvss/collateral_damage_potential_2_0_0.json +++ b/data/json/decision_points/cvss/collateral_damage_potential_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Collateral Damage Potential", + "description": "This metric measures the potential for loss of life or physical assets.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "CDP", - "name": "Collateral Damage Potential", - "description": "This metric measures the potential for loss of life or physical assets.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/confidentiality_impact_1_0_0.json b/data/json/decision_points/cvss/confidentiality_impact_1_0_0.json index f8e633e6..8f9ad138 100644 --- a/data/json/decision_points/cvss/confidentiality_impact_1_0_0.json +++ b/data/json/decision_points/cvss/confidentiality_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Impact", + "description": "This metric measures the impact on confidentiality of a successful exploit of the vulnerability on the target system.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "C", - "name": "Confidentiality Impact", - "description": "This metric measures the impact on confidentiality of a successful exploit of the vulnerability on the target system.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/confidentiality_impact_2_0_0.json b/data/json/decision_points/cvss/confidentiality_impact_2_0_0.json index 5d8f0826..6f8c6c64 100644 --- a/data/json/decision_points/cvss/confidentiality_impact_2_0_0.json +++ b/data/json/decision_points/cvss/confidentiality_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Impact", + "description": "This metric measures the impact to the confidentiality of the information resources managed by a software component due to a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "C", - "name": "Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information resources managed by a software component due to a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/confidentiality_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/confidentiality_impact_to_the_subsequent_system_1_0_0.json index 741722cd..1b2041aa 100644 --- a/data/json/decision_points/cvss/confidentiality_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/confidentiality_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Impact to the Subsequent System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SC", - "name": "Confidentiality Impact to the Subsequent System", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/confidentiality_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/confidentiality_impact_to_the_vulnerable_system_3_0_0.json index ceea5568..6fc61ef9 100644 --- a/data/json/decision_points/cvss/confidentiality_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/confidentiality_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Impact to the Vulnerable System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "VC", - "name": "Confidentiality Impact to the Vulnerable System", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/confidentiality_requirement_1_0_0.json b/data/json/decision_points/cvss/confidentiality_requirement_1_0_0.json index 988ee409..04b9e92d 100644 --- a/data/json/decision_points/cvss/confidentiality_requirement_1_0_0.json +++ b/data/json/decision_points/cvss/confidentiality_requirement_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Requirement", + "description": "This metric measures the impact to the confidentiality of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "CR", - "name": "Confidentiality Requirement", - "description": "This metric measures the impact to the confidentiality of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/confidentiality_requirement_1_1_0.json b/data/json/decision_points/cvss/confidentiality_requirement_1_1_0.json index 2c508587..87453bab 100644 --- a/data/json/decision_points/cvss/confidentiality_requirement_1_1_0.json +++ b/data/json/decision_points/cvss/confidentiality_requirement_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Requirement", + "description": "This metric measures the impact to the confidentiality of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "CR", - "name": "Confidentiality Requirement", - "description": "This metric measures the impact to the confidentiality of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/confidentiality_requirement_1_1_1.json b/data/json/decision_points/cvss/confidentiality_requirement_1_1_1.json index 2e1ef437..1c71ed0d 100644 --- a/data/json/decision_points/cvss/confidentiality_requirement_1_1_1.json +++ b/data/json/decision_points/cvss/confidentiality_requirement_1_1_1.json @@ -1,10 +1,10 @@ { + "name": "Confidentiality Requirement", + "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", "namespace": "cvss", "version": "1.1.1", "schemaVersion": "1-0-1", "key": "CR", - "name": "Confidentiality Requirement", - "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_1_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_1_1_0_0.json index 9046163e..e4563635 100644 --- a/data/json/decision_points/cvss/equivalence_set_1_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_1_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 1", + "description": "AV/PR/UI with 3 levels specified in Table 24", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ1", - "name": "Equivalence Set 1", - "description": "AV/PR/UI with 3 levels specified in Table 24", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_2_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_2_1_0_0.json index f9fa06e5..db8745ce 100644 --- a/data/json/decision_points/cvss/equivalence_set_2_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_2_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 2", + "description": "AC/AT with 2 levels specified in Table 25", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ2", - "name": "Equivalence Set 2", - "description": "AC/AT with 2 levels specified in Table 25", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_3_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_3_1_0_0.json index a617a8f4..4b1aaf2b 100644 --- a/data/json/decision_points/cvss/equivalence_set_3_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_3_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 3", + "description": "VC/VI/VA with 3 levels specified in Table 26", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ3", - "name": "Equivalence Set 3", - "description": "VC/VI/VA with 3 levels specified in Table 26", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_4_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_4_1_0_0.json index 761d6ec8..d732ec5b 100644 --- a/data/json/decision_points/cvss/equivalence_set_4_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_4_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 4", + "description": "SC/SI/SA with 3 levels specified in Table 27", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ4", - "name": "Equivalence Set 4", - "description": "SC/SI/SA with 3 levels specified in Table 27", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_5_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_5_1_0_0.json index 1f1b7eec..f79d20a7 100644 --- a/data/json/decision_points/cvss/equivalence_set_5_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_5_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 5", + "description": "E with 3 levels specified in Table 28", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ5", - "name": "Equivalence Set 5", - "description": "E with 3 levels specified in Table 28", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/equivalence_set_6_1_0_0.json b/data/json/decision_points/cvss/equivalence_set_6_1_0_0.json index 599ec3b1..631acd7b 100644 --- a/data/json/decision_points/cvss/equivalence_set_6_1_0_0.json +++ b/data/json/decision_points/cvss/equivalence_set_6_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Equivalence Set 6", + "description": "VC/VI/VA+CR/CI/CA with 2 levels specified in Table 29", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EQ6", - "name": "Equivalence Set 6", - "description": "VC/VI/VA+CR/CI/CA with 2 levels specified in Table 29", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/exploit_code_maturity_1_2_0.json b/data/json/decision_points/cvss/exploit_code_maturity_1_2_0.json index a900808a..a4e59e23 100644 --- a/data/json/decision_points/cvss/exploit_code_maturity_1_2_0.json +++ b/data/json/decision_points/cvss/exploit_code_maturity_1_2_0.json @@ -1,10 +1,10 @@ { + "name": "Exploit Code Maturity", + "description": "measures the likelihood of the vulnerability being attacked, and is typically based on the current state of exploit techniques, exploit code availability, or active, 'in-the-wild' exploitation", "namespace": "cvss", "version": "1.2.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploit Code Maturity", - "description": "measures the likelihood of the vulnerability being attacked, and is typically based on the current state of exploit techniques, exploit code availability, or active, 'in-the-wild' exploitation", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/exploit_maturity_2_0_0.json b/data/json/decision_points/cvss/exploit_maturity_2_0_0.json index 879891f6..28eeebd3 100644 --- a/data/json/decision_points/cvss/exploit_maturity_2_0_0.json +++ b/data/json/decision_points/cvss/exploit_maturity_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Exploit Maturity", + "description": "This metric measures the likelihood of the vulnerability being attacked, and is based on the current state of exploit techniques, exploit code availability, or active, “in-the-wild” exploitation.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploit Maturity", - "description": "This metric measures the likelihood of the vulnerability being attacked, and is based on the current state of exploit techniques, exploit code availability, or active, “in-the-wild” exploitation.", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/exploitability_1_0_0.json b/data/json/decision_points/cvss/exploitability_1_0_0.json index be804085..707f297d 100644 --- a/data/json/decision_points/cvss/exploitability_1_0_0.json +++ b/data/json/decision_points/cvss/exploitability_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Exploitability", + "description": "This metric measures the current state of exploit technique or code availability and suggests a likelihood of exploitation.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploitability", - "description": "This metric measures the current state of exploit technique or code availability and suggests a likelihood of exploitation.", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/exploitability_1_1_0.json b/data/json/decision_points/cvss/exploitability_1_1_0.json index f2d07e9d..add3fd28 100644 --- a/data/json/decision_points/cvss/exploitability_1_1_0.json +++ b/data/json/decision_points/cvss/exploitability_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Exploitability", + "description": "This metric measures the current state of exploit technique or code availability and suggests a likelihood of exploitation.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploitability", - "description": "This metric measures the current state of exploit technique or code availability and suggests a likelihood of exploitation.", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/impact_bias_1_0_0.json b/data/json/decision_points/cvss/impact_bias_1_0_0.json index 97039be4..fc7316eb 100644 --- a/data/json/decision_points/cvss/impact_bias_1_0_0.json +++ b/data/json/decision_points/cvss/impact_bias_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Impact Bias", + "description": "This metric measures the impact bias of the vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "IB", - "name": "Impact Bias", - "description": "This metric measures the impact bias of the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/integrity_impact_1_0_0.json b/data/json/decision_points/cvss/integrity_impact_1_0_0.json index cf1dcc9b..5880fcf4 100644 --- a/data/json/decision_points/cvss/integrity_impact_1_0_0.json +++ b/data/json/decision_points/cvss/integrity_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Impact", + "description": "This metric measures the impact on integrity a successful exploit of the vulnerability will have on the target system.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "I", - "name": "Integrity Impact", - "description": "This metric measures the impact on integrity a successful exploit of the vulnerability will have on the target system.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/integrity_impact_2_0_0.json b/data/json/decision_points/cvss/integrity_impact_2_0_0.json index 48102023..ecb0fd66 100644 --- a/data/json/decision_points/cvss/integrity_impact_2_0_0.json +++ b/data/json/decision_points/cvss/integrity_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Impact", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "I", - "name": "Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/integrity_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/integrity_impact_to_the_subsequent_system_1_0_0.json index ab4089b3..80c99790 100644 --- a/data/json/decision_points/cvss/integrity_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/integrity_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Impact to the Subsequent System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SI", - "name": "Integrity Impact to the Subsequent System", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/integrity_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/integrity_impact_to_the_vulnerable_system_3_0_0.json index ad055d84..745ee9e1 100644 --- a/data/json/decision_points/cvss/integrity_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/integrity_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Impact to the Vulnerable System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "VI", - "name": "Integrity Impact to the Vulnerable System", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/integrity_requirement_1_0_0.json b/data/json/decision_points/cvss/integrity_requirement_1_0_0.json index 73d07de1..f49d6438 100644 --- a/data/json/decision_points/cvss/integrity_requirement_1_0_0.json +++ b/data/json/decision_points/cvss/integrity_requirement_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Requirement", + "description": "This metric measures the impact to the integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "IR", - "name": "Integrity Requirement", - "description": "This metric measures the impact to the integrity of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/integrity_requirement_1_1_0.json b/data/json/decision_points/cvss/integrity_requirement_1_1_0.json index 5515b3b4..7378845f 100644 --- a/data/json/decision_points/cvss/integrity_requirement_1_1_0.json +++ b/data/json/decision_points/cvss/integrity_requirement_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Integrity Requirement", + "description": "This metric measures the impact to the integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "IR", - "name": "Integrity Requirement", - "description": "This metric measures the impact to the integrity of a successfully exploited vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/integrity_requirement_1_1_1.json b/data/json/decision_points/cvss/integrity_requirement_1_1_1.json index 4a99083a..05fd2858 100644 --- a/data/json/decision_points/cvss/integrity_requirement_1_1_1.json +++ b/data/json/decision_points/cvss/integrity_requirement_1_1_1.json @@ -1,10 +1,10 @@ { + "name": "Integrity Requirement", + "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", "namespace": "cvss", "version": "1.1.1", "schemaVersion": "1-0-1", "key": "IR", - "name": "Integrity Requirement", - "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/modified_attack_complexity_3_0_0.json b/data/json/decision_points/cvss/modified_attack_complexity_3_0_0.json index 09fa2cab..6e8df236 100644 --- a/data/json/decision_points/cvss/modified_attack_complexity_3_0_0.json +++ b/data/json/decision_points/cvss/modified_attack_complexity_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Attack Complexity", + "description": "This metric describes the conditions beyond the attacker's control that must exist in order to exploit the vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "MAC", - "name": "Modified Attack Complexity", - "description": "This metric describes the conditions beyond the attacker's control that must exist in order to exploit the vulnerability.", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/modified_attack_complexity_3_0_1.json b/data/json/decision_points/cvss/modified_attack_complexity_3_0_1.json index 9ddd5581..a8bee010 100644 --- a/data/json/decision_points/cvss/modified_attack_complexity_3_0_1.json +++ b/data/json/decision_points/cvss/modified_attack_complexity_3_0_1.json @@ -1,10 +1,10 @@ { + "name": "Modified Attack Complexity", + "description": "This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. ", "namespace": "cvss", "version": "3.0.1", "schemaVersion": "1-0-1", "key": "MAC", - "name": "Modified Attack Complexity", - "description": "This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. ", "values": [ { "key": "L", diff --git a/data/json/decision_points/cvss/modified_attack_requirements_1_0_0.json b/data/json/decision_points/cvss/modified_attack_requirements_1_0_0.json index be523348..4f446155 100644 --- a/data/json/decision_points/cvss/modified_attack_requirements_1_0_0.json +++ b/data/json/decision_points/cvss/modified_attack_requirements_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Attack Requirements", + "description": "This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MAT", - "name": "Modified Attack Requirements", - "description": "This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_attack_vector_3_0_0.json b/data/json/decision_points/cvss/modified_attack_vector_3_0_0.json index afb49892..cd8261e7 100644 --- a/data/json/decision_points/cvss/modified_attack_vector_3_0_0.json +++ b/data/json/decision_points/cvss/modified_attack_vector_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Attack Vector", + "description": "This metric reflects the context by which vulnerability exploitation is possible. ", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "MAV", - "name": "Modified Attack Vector", - "description": "This metric reflects the context by which vulnerability exploitation is possible. ", "values": [ { "key": "P", diff --git a/data/json/decision_points/cvss/modified_attack_vector_3_0_1.json b/data/json/decision_points/cvss/modified_attack_vector_3_0_1.json index 32f378f7..35995809 100644 --- a/data/json/decision_points/cvss/modified_attack_vector_3_0_1.json +++ b/data/json/decision_points/cvss/modified_attack_vector_3_0_1.json @@ -1,10 +1,10 @@ { + "name": "Modified Attack Vector", + "description": "This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.", "namespace": "cvss", "version": "3.0.1", "schemaVersion": "1-0-1", "key": "MAV", - "name": "Modified Attack Vector", - "description": "This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.", "values": [ { "key": "P", diff --git a/data/json/decision_points/cvss/modified_availability_impact_2_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_2_0_0.json index 861be583..efea9be1 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_2_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Availability Impact", + "description": "This metric measures the impact to availability of a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "MA", - "name": "Modified Availability Impact", - "description": "This metric measures the impact to availability of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json index e1e91459..786f0390 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Availability Impact to the Subsequent System", + "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MSA", - "name": "Modified Availability Impact to the Subsequent System", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_availability_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_vulnerable_system_3_0_0.json index 7003a551..689120d5 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Availability Impact to the Vulnerable System", + "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "MVA", - "name": "Modified Availability Impact to the Vulnerable System", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_0.json b/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_0.json index 5920006a..ef523bac 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_0.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Confidentiality Impact", + "description": "This metric measures the impact to the confidentiality of the information resources managed by a software component due to a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "MC", - "name": "Modified Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information resources managed by a software component due to a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json index 1abda292..ea677a2a 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Confidentiality Impact to the Subsequent System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MSC", - "name": "Modified Confidentiality Impact to the Subsequent System", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_vulnerable_system_3_0_0.json index aba1fa8b..b3f09692 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Confidentiality Impact to the Vulnerable System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "MVC", - "name": "Modified Confidentiality Impact to the Vulnerable System", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_2_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_2_0_0.json index 359fb804..0e010de0 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_2_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Integrity Impact", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "MI", - "name": "Modified Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json index ec3d57b3..719e36b4 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Integrity Impact to the Subsequent System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MSI", - "name": "Modified Integrity Impact to the Subsequent System", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_vulnerable_system_3_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_vulnerable_system_3_0_0.json index 5a3c69e0..76f318a2 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_to_the_vulnerable_system_3_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_vulnerable_system_3_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Integrity Impact to the Vulnerable System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "namespace": "cvss", "version": "3.0.0", "schemaVersion": "1-0-1", "key": "MVI", - "name": "Modified Integrity Impact to the Vulnerable System", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/modified_privileges_required_1_0_0.json b/data/json/decision_points/cvss/modified_privileges_required_1_0_0.json index b31ad194..4aa2e7fe 100644 --- a/data/json/decision_points/cvss/modified_privileges_required_1_0_0.json +++ b/data/json/decision_points/cvss/modified_privileges_required_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Privileges Required", + "description": "This metric describes the level of privileges an attacker must possess before successfully exploiting the vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MPR", - "name": "Modified Privileges Required", - "description": "This metric describes the level of privileges an attacker must possess before successfully exploiting the vulnerability.", "values": [ { "key": "H", diff --git a/data/json/decision_points/cvss/modified_privileges_required_1_0_1.json b/data/json/decision_points/cvss/modified_privileges_required_1_0_1.json index 92297091..9edb12a4 100644 --- a/data/json/decision_points/cvss/modified_privileges_required_1_0_1.json +++ b/data/json/decision_points/cvss/modified_privileges_required_1_0_1.json @@ -1,10 +1,10 @@ { + "name": "Modified Privileges Required", + "description": "This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.", "namespace": "cvss", "version": "1.0.1", "schemaVersion": "1-0-1", "key": "MPR", - "name": "Modified Privileges Required", - "description": "This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.", "values": [ { "key": "H", diff --git a/data/json/decision_points/cvss/modified_scope_1_0_0.json b/data/json/decision_points/cvss/modified_scope_1_0_0.json index 21d82cba..7eb01d1c 100644 --- a/data/json/decision_points/cvss/modified_scope_1_0_0.json +++ b/data/json/decision_points/cvss/modified_scope_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified Scope", + "description": "the ability for a vulnerability in one software component to impact resources beyond its means, or privileges", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MS", - "name": "Modified Scope", - "description": "the ability for a vulnerability in one software component to impact resources beyond its means, or privileges", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/modified_user_interaction_1_0_0.json b/data/json/decision_points/cvss/modified_user_interaction_1_0_0.json index cea0d0c0..dab50cf5 100644 --- a/data/json/decision_points/cvss/modified_user_interaction_1_0_0.json +++ b/data/json/decision_points/cvss/modified_user_interaction_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified User Interaction", + "description": "This metric captures the requirement for a user, other than the attacker, to participate in the successful compromise of the vulnerable component.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MUI", - "name": "Modified User Interaction", - "description": "This metric captures the requirement for a user, other than the attacker, to participate in the successful compromise of the vulnerable component.", "values": [ { "key": "R", diff --git a/data/json/decision_points/cvss/modified_user_interaction_2_0_0.json b/data/json/decision_points/cvss/modified_user_interaction_2_0_0.json index a4242ca6..2fbfe36b 100644 --- a/data/json/decision_points/cvss/modified_user_interaction_2_0_0.json +++ b/data/json/decision_points/cvss/modified_user_interaction_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Modified User Interaction", + "description": "This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner. The resulting score is greatest when no user interaction is required.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "MUI", - "name": "Modified User Interaction", - "description": "This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner. The resulting score is greatest when no user interaction is required.", "values": [ { "key": "A", diff --git a/data/json/decision_points/cvss/privileges_required_1_0_0.json b/data/json/decision_points/cvss/privileges_required_1_0_0.json index e7a14402..0f918c46 100644 --- a/data/json/decision_points/cvss/privileges_required_1_0_0.json +++ b/data/json/decision_points/cvss/privileges_required_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Privileges Required", + "description": "This metric describes the level of privileges an attacker must possess before successfully exploiting the vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "PR", - "name": "Privileges Required", - "description": "This metric describes the level of privileges an attacker must possess before successfully exploiting the vulnerability.", "values": [ { "key": "H", diff --git a/data/json/decision_points/cvss/privileges_required_1_0_1.json b/data/json/decision_points/cvss/privileges_required_1_0_1.json index 79c6c94a..698e4dc3 100644 --- a/data/json/decision_points/cvss/privileges_required_1_0_1.json +++ b/data/json/decision_points/cvss/privileges_required_1_0_1.json @@ -1,10 +1,10 @@ { + "name": "Privileges Required", + "description": "This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.", "namespace": "cvss", "version": "1.0.1", "schemaVersion": "1-0-1", "key": "PR", - "name": "Privileges Required", - "description": "This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.", "values": [ { "key": "H", diff --git a/data/json/decision_points/cvss/provider_urgency_1_0_0.json b/data/json/decision_points/cvss/provider_urgency_1_0_0.json index 0e277cca..6a319c77 100644 --- a/data/json/decision_points/cvss/provider_urgency_1_0_0.json +++ b/data/json/decision_points/cvss/provider_urgency_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Provider Urgency", + "description": "Many vendors currently provide supplemental severity ratings to consumers via product security advisories. Other vendors publish Qualitative Severity Ratings from the CVSS Specification Document in their advisories. To facilitate a standardized method to incorporate additional provider-supplied assessment, an optional \"pass-through\" Supplemental Metric called Provider Urgency is available.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "U", - "name": "Provider Urgency", - "description": "Many vendors currently provide supplemental severity ratings to consumers via product security advisories. Other vendors publish Qualitative Severity Ratings from the CVSS Specification Document in their advisories. To facilitate a standardized method to incorporate additional provider-supplied assessment, an optional \"pass-through\" Supplemental Metric called Provider Urgency is available.", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/recovery_1_0_0.json b/data/json/decision_points/cvss/recovery_1_0_0.json index 8a4beda9..b8597662 100644 --- a/data/json/decision_points/cvss/recovery_1_0_0.json +++ b/data/json/decision_points/cvss/recovery_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Recovery", + "description": "The Recovery metric describes the resilience of a system to recover services, in terms of performance and availability, after an attack has been performed.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "R", - "name": "Recovery", - "description": "The Recovery metric describes the resilience of a system to recover services, in terms of performance and availability, after an attack has been performed.", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/remediation_level_1_0_0.json b/data/json/decision_points/cvss/remediation_level_1_0_0.json index 11f9384f..cc5a3866 100644 --- a/data/json/decision_points/cvss/remediation_level_1_0_0.json +++ b/data/json/decision_points/cvss/remediation_level_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Remediation Level", + "description": "This metric measures the remediation status of a vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "RL", - "name": "Remediation Level", - "description": "This metric measures the remediation status of a vulnerability.", "values": [ { "key": "OF", diff --git a/data/json/decision_points/cvss/remediation_level_1_1_0.json b/data/json/decision_points/cvss/remediation_level_1_1_0.json index ccaa439c..eda1100a 100644 --- a/data/json/decision_points/cvss/remediation_level_1_1_0.json +++ b/data/json/decision_points/cvss/remediation_level_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Remediation Level", + "description": "This metric measures the remediation status of a vulnerability.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "RL", - "name": "Remediation Level", - "description": "This metric measures the remediation status of a vulnerability.", "values": [ { "key": "OF", diff --git a/data/json/decision_points/cvss/report_confidence_1_0_0.json b/data/json/decision_points/cvss/report_confidence_1_0_0.json index 85940cf0..0dc24b8b 100644 --- a/data/json/decision_points/cvss/report_confidence_1_0_0.json +++ b/data/json/decision_points/cvss/report_confidence_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Report Confidence", + "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "RC", - "name": "Report Confidence", - "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "values": [ { "key": "UC", diff --git a/data/json/decision_points/cvss/report_confidence_1_1_0.json b/data/json/decision_points/cvss/report_confidence_1_1_0.json index 691f1e87..c3c2b7aa 100644 --- a/data/json/decision_points/cvss/report_confidence_1_1_0.json +++ b/data/json/decision_points/cvss/report_confidence_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Report Confidence", + "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "RC", - "name": "Report Confidence", - "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "values": [ { "key": "UC", diff --git a/data/json/decision_points/cvss/report_confidence_2_0_0.json b/data/json/decision_points/cvss/report_confidence_2_0_0.json index 502e1291..cf6cf0ca 100644 --- a/data/json/decision_points/cvss/report_confidence_2_0_0.json +++ b/data/json/decision_points/cvss/report_confidence_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Report Confidence", + "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "RC", - "name": "Report Confidence", - "description": "This metric measures the degree of confidence in the existence of the vulnerability and the credibility of the known technical details.", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/safety_1_0_0.json b/data/json/decision_points/cvss/safety_1_0_0.json index a72a7cd6..987de4d0 100644 --- a/data/json/decision_points/cvss/safety_1_0_0.json +++ b/data/json/decision_points/cvss/safety_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Safety", + "description": "The Safety decision point is a measure of the potential for harm to humans or the environment.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "S", - "name": "Safety", - "description": "The Safety decision point is a measure of the potential for harm to humans or the environment.", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/scope_1_0_0.json b/data/json/decision_points/cvss/scope_1_0_0.json index 2ed72c80..0025ac97 100644 --- a/data/json/decision_points/cvss/scope_1_0_0.json +++ b/data/json/decision_points/cvss/scope_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Scope", + "description": "the ability for a vulnerability in one software component to impact resources beyond its means, or privileges", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "S", - "name": "Scope", - "description": "the ability for a vulnerability in one software component to impact resources beyond its means, or privileges", "values": [ { "key": "U", diff --git a/data/json/decision_points/cvss/target_distribution_1_0_0.json b/data/json/decision_points/cvss/target_distribution_1_0_0.json index 1d86b7ca..97b94297 100644 --- a/data/json/decision_points/cvss/target_distribution_1_0_0.json +++ b/data/json/decision_points/cvss/target_distribution_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Target Distribution", + "description": "This metric measures the relative size of the field of target systems susceptible to the vulnerability. It is meant as an environment-specific indicator in order to approximate the percentage of systems within the environment that could be affected by the vulnerability.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "TD", - "name": "Target Distribution", - "description": "This metric measures the relative size of the field of target systems susceptible to the vulnerability. It is meant as an environment-specific indicator in order to approximate the percentage of systems within the environment that could be affected by the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/target_distribution_1_1_0.json b/data/json/decision_points/cvss/target_distribution_1_1_0.json index bc126152..5e0d93f0 100644 --- a/data/json/decision_points/cvss/target_distribution_1_1_0.json +++ b/data/json/decision_points/cvss/target_distribution_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Target Distribution", + "description": "This metric measures the relative size of the field of target systems susceptible to the vulnerability. It is meant as an environment-specific indicator in order to approximate the percentage of systems within the environment that could be affected by the vulnerability.", "namespace": "cvss", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "TD", - "name": "Target Distribution", - "description": "This metric measures the relative size of the field of target systems susceptible to the vulnerability. It is meant as an environment-specific indicator in order to approximate the percentage of systems within the environment that could be affected by the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/cvss/user_interaction_1_0_0.json b/data/json/decision_points/cvss/user_interaction_1_0_0.json index 84f623ba..eb4e9bfb 100644 --- a/data/json/decision_points/cvss/user_interaction_1_0_0.json +++ b/data/json/decision_points/cvss/user_interaction_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "User Interaction", + "description": "This metric captures the requirement for a user, other than the attacker, to participate in the successful compromise of the vulnerable component.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "UI", - "name": "User Interaction", - "description": "This metric captures the requirement for a user, other than the attacker, to participate in the successful compromise of the vulnerable component.", "values": [ { "key": "R", diff --git a/data/json/decision_points/cvss/user_interaction_2_0_0.json b/data/json/decision_points/cvss/user_interaction_2_0_0.json index 7794cc14..160107aa 100644 --- a/data/json/decision_points/cvss/user_interaction_2_0_0.json +++ b/data/json/decision_points/cvss/user_interaction_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "User Interaction", + "description": "This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner. The resulting score is greatest when no user interaction is required.", "namespace": "cvss", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "UI", - "name": "User Interaction", - "description": "This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner. The resulting score is greatest when no user interaction is required.", "values": [ { "key": "A", diff --git a/data/json/decision_points/cvss/value_density_1_0_0.json b/data/json/decision_points/cvss/value_density_1_0_0.json index a4f06724..1ca1a355 100644 --- a/data/json/decision_points/cvss/value_density_1_0_0.json +++ b/data/json/decision_points/cvss/value_density_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Value Density", + "description": "Value Density describes the resources that the attacker will gain control over with a single exploitation event. It has two possible values, diffuse and concentrated.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "V", - "name": "Value Density", - "description": "Value Density describes the resources that the attacker will gain control over with a single exploitation event. It has two possible values, diffuse and concentrated.", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/vulnerability_response_effort_1_0_0.json b/data/json/decision_points/cvss/vulnerability_response_effort_1_0_0.json index 71e2f3cc..bb334844 100644 --- a/data/json/decision_points/cvss/vulnerability_response_effort_1_0_0.json +++ b/data/json/decision_points/cvss/vulnerability_response_effort_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Vulnerability Response Effort", + "description": "The intention of the Vulnerability Response Effort metric is to provide supplemental information on how difficult it is for consumers to provide an initial response to the impact of vulnerabilities for deployed products and services in their infrastructure. The consumer can then take this additional information on effort required into consideration when applying mitigations and/or scheduling remediation.", "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "RE", - "name": "Vulnerability Response Effort", - "description": "The intention of the Vulnerability Response Effort metric is to provide supplemental information on how difficult it is for consumers to provide an initial response to the impact of vulnerabilities for deployed products and services in their infrastructure. The consumer can then take this additional information on effort required into consideration when applying mitigations and/or scheduling remediation.", "values": [ { "key": "X", diff --git a/data/json/decision_points/exploitation_1_0_0.json b/data/json/decision_points/exploitation_1_0_0.json index 42242c30..d1cf71b2 100644 --- a/data/json/decision_points/exploitation_1_0_0.json +++ b/data/json/decision_points/exploitation_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Exploitation", + "description": "The present state of exploitation of the vulnerability.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploitation", - "description": "The present state of exploitation of the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/exploitation_1_1_0.json b/data/json/decision_points/exploitation_1_1_0.json index f436738a..e54d2ace 100644 --- a/data/json/decision_points/exploitation_1_1_0.json +++ b/data/json/decision_points/exploitation_1_1_0.json @@ -1,10 +1,10 @@ { + "name": "Exploitation", + "description": "The present state of exploitation of the vulnerability.", "namespace": "ssvc", "version": "1.1.0", "schemaVersion": "1-0-1", "key": "E", - "name": "Exploitation", - "description": "The present state of exploitation of the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/human_impact_2_0_0.json b/data/json/decision_points/human_impact_2_0_0.json index b9fec592..80af1b78 100644 --- a/data/json/decision_points/human_impact_2_0_0.json +++ b/data/json/decision_points/human_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Human Impact", + "description": "Human Impact is a combination of Safety and Mission impacts.", "namespace": "ssvc", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "HI", - "name": "Human Impact", - "description": "Human Impact is a combination of Safety and Mission impacts.", "values": [ { "key": "L", diff --git a/data/json/decision_points/human_impact_2_0_1.json b/data/json/decision_points/human_impact_2_0_1.json index 9fd6ba91..3942e93a 100644 --- a/data/json/decision_points/human_impact_2_0_1.json +++ b/data/json/decision_points/human_impact_2_0_1.json @@ -1,10 +1,10 @@ { + "name": "Human Impact", + "description": "Human Impact is a combination of Safety and Mission impacts.", "namespace": "ssvc", "version": "2.0.1", "schemaVersion": "1-0-1", "key": "HI", - "name": "Human Impact", - "description": "Human Impact is a combination of Safety and Mission impacts.", "values": [ { "key": "L", diff --git a/data/json/decision_points/mission_and_well-being_impact_1_0_0.json b/data/json/decision_points/mission_and_well-being_impact_1_0_0.json index 20c2ad3a..95de41e6 100644 --- a/data/json/decision_points/mission_and_well-being_impact_1_0_0.json +++ b/data/json/decision_points/mission_and_well-being_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Mission and Well-Being Impact", + "description": "Mission and Well-Being Impact is a combination of Mission Prevalence and Public Well-Being Impact.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MWI", - "name": "Mission and Well-Being Impact", - "description": "Mission and Well-Being Impact is a combination of Mission Prevalence and Public Well-Being Impact.", "values": [ { "key": "L", diff --git a/data/json/decision_points/mission_impact_1_0_0.json b/data/json/decision_points/mission_impact_1_0_0.json index 3dd1a4ba..ac6b2915 100644 --- a/data/json/decision_points/mission_impact_1_0_0.json +++ b/data/json/decision_points/mission_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Mission Impact", + "description": "Impact on Mission Essential Functions of the Organization", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "MI", - "name": "Mission Impact", - "description": "Impact on Mission Essential Functions of the Organization", "values": [ { "key": "N", diff --git a/data/json/decision_points/mission_impact_2_0_0.json b/data/json/decision_points/mission_impact_2_0_0.json index 51f392e9..b0a3fc77 100644 --- a/data/json/decision_points/mission_impact_2_0_0.json +++ b/data/json/decision_points/mission_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Mission Impact", + "description": "Impact on Mission Essential Functions of the Organization", "namespace": "ssvc", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "MI", - "name": "Mission Impact", - "description": "Impact on Mission Essential Functions of the Organization", "values": [ { "key": "D", diff --git a/data/json/decision_points/public_safety_impact_2_0_0.json b/data/json/decision_points/public_safety_impact_2_0_0.json index 03eaa0d8..74b06423 100644 --- a/data/json/decision_points/public_safety_impact_2_0_0.json +++ b/data/json/decision_points/public_safety_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Public Safety Impact", + "description": "A coarse-grained representation of impact to public safety.", "namespace": "ssvc", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "PSI", - "name": "Public Safety Impact", - "description": "A coarse-grained representation of impact to public safety.", "values": [ { "key": "M", diff --git a/data/json/decision_points/public_safety_impact_2_0_1.json b/data/json/decision_points/public_safety_impact_2_0_1.json index e61afe04..7c60c4ef 100644 --- a/data/json/decision_points/public_safety_impact_2_0_1.json +++ b/data/json/decision_points/public_safety_impact_2_0_1.json @@ -1,10 +1,10 @@ { + "name": "Public Safety Impact", + "description": "A coarse-grained representation of impact to public safety.", "namespace": "ssvc", "version": "2.0.1", "schemaVersion": "1-0-1", "key": "PSI", - "name": "Public Safety Impact", - "description": "A coarse-grained representation of impact to public safety.", "values": [ { "key": "M", diff --git a/data/json/decision_points/public_value_added_1_0_0.json b/data/json/decision_points/public_value_added_1_0_0.json index a376f8bb..ae508569 100644 --- a/data/json/decision_points/public_value_added_1_0_0.json +++ b/data/json/decision_points/public_value_added_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Public Value Added", + "description": "How much value would a publication from the coordinator benefit the broader community?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "PVA", - "name": "Public Value Added", - "description": "How much value would a publication from the coordinator benefit the broader community?", "values": [ { "key": "L", diff --git a/data/json/decision_points/public_well-being_impact_1_0_0.json b/data/json/decision_points/public_well-being_impact_1_0_0.json index 2b1c02bd..7994e948 100644 --- a/data/json/decision_points/public_well-being_impact_1_0_0.json +++ b/data/json/decision_points/public_well-being_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Public Well-Being Impact", + "description": "A coarse-grained representation of impact to public well-being.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "PWI", - "name": "Public Well-Being Impact", - "description": "A coarse-grained representation of impact to public well-being.", "values": [ { "key": "M", diff --git a/data/json/decision_points/report_credibility_1_0_0.json b/data/json/decision_points/report_credibility_1_0_0.json index 06f2d323..8cf756bd 100644 --- a/data/json/decision_points/report_credibility_1_0_0.json +++ b/data/json/decision_points/report_credibility_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Report Credibility", + "description": "Is the report credible?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "RC", - "name": "Report Credibility", - "description": "Is the report credible?", "values": [ { "key": "NC", diff --git a/data/json/decision_points/report_public_1_0_0.json b/data/json/decision_points/report_public_1_0_0.json index ba36050a..5c4d19d8 100644 --- a/data/json/decision_points/report_public_1_0_0.json +++ b/data/json/decision_points/report_public_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Report Public", + "description": "Is a viable report of the details of the vulnerability already publicly available?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "RP", - "name": "Report Public", - "description": "Is a viable report of the details of the vulnerability already publicly available?", "values": [ { "key": "Y", diff --git a/data/json/decision_points/safety_impact_1_0_0.json b/data/json/decision_points/safety_impact_1_0_0.json index 7aadf352..fe240916 100644 --- a/data/json/decision_points/safety_impact_1_0_0.json +++ b/data/json/decision_points/safety_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Safety Impact", + "description": "The safety impact of the vulnerability.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SI", - "name": "Safety Impact", - "description": "The safety impact of the vulnerability.", "values": [ { "key": "N", diff --git a/data/json/decision_points/safety_impact_2_0_0.json b/data/json/decision_points/safety_impact_2_0_0.json index 19d74d6b..4f839fb8 100644 --- a/data/json/decision_points/safety_impact_2_0_0.json +++ b/data/json/decision_points/safety_impact_2_0_0.json @@ -1,10 +1,10 @@ { + "name": "Safety Impact", + "description": "The safety impact of the vulnerability. (based on IEC 61508)", "namespace": "ssvc", "version": "2.0.0", "schemaVersion": "1-0-1", "key": "SI", - "name": "Safety Impact", - "description": "The safety impact of the vulnerability. (based on IEC 61508)", "values": [ { "key": "N", diff --git a/data/json/decision_points/supplier_cardinality_1_0_0.json b/data/json/decision_points/supplier_cardinality_1_0_0.json index 0adc8300..ec1df5a8 100644 --- a/data/json/decision_points/supplier_cardinality_1_0_0.json +++ b/data/json/decision_points/supplier_cardinality_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Supplier Cardinality", + "description": "How many suppliers are responsible for the vulnerable component and its remediation or mitigation plan?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SC", - "name": "Supplier Cardinality", - "description": "How many suppliers are responsible for the vulnerable component and its remediation or mitigation plan?", "values": [ { "key": "O", diff --git a/data/json/decision_points/supplier_contacted_1_0_0.json b/data/json/decision_points/supplier_contacted_1_0_0.json index 2cceb5ed..c32d5755 100644 --- a/data/json/decision_points/supplier_contacted_1_0_0.json +++ b/data/json/decision_points/supplier_contacted_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Supplier Contacted", + "description": "Has the reporter made a good-faith effort to contact the supplier of the vulnerable component using a quality contact method?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SC", - "name": "Supplier Contacted", - "description": "Has the reporter made a good-faith effort to contact the supplier of the vulnerable component using a quality contact method?", "values": [ { "key": "N", diff --git a/data/json/decision_points/supplier_engagement_1_0_0.json b/data/json/decision_points/supplier_engagement_1_0_0.json index ffd69c94..d9f704b0 100644 --- a/data/json/decision_points/supplier_engagement_1_0_0.json +++ b/data/json/decision_points/supplier_engagement_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Supplier Engagement", + "description": "Is the supplier responding to the reporter’s contact effort and actively participating in the coordination effort?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SE", - "name": "Supplier Engagement", - "description": "Is the supplier responding to the reporter’s contact effort and actively participating in the coordination effort?", "values": [ { "key": "A", diff --git a/data/json/decision_points/supplier_involvement_1_0_0.json b/data/json/decision_points/supplier_involvement_1_0_0.json index d9c5b433..15d014e5 100644 --- a/data/json/decision_points/supplier_involvement_1_0_0.json +++ b/data/json/decision_points/supplier_involvement_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Supplier Involvement", + "description": "What is the state of the supplier’s work on addressing the vulnerability?", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "SI", - "name": "Supplier Involvement", - "description": "What is the state of the supplier’s work on addressing the vulnerability?", "values": [ { "key": "FR", diff --git a/data/json/decision_points/system_exposure_1_0_0.json b/data/json/decision_points/system_exposure_1_0_0.json index 45671101..c72411b5 100644 --- a/data/json/decision_points/system_exposure_1_0_0.json +++ b/data/json/decision_points/system_exposure_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "System Exposure", + "description": "The Accessible Attack Surface of the Affected System or Service", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "EXP", - "name": "System Exposure", - "description": "The Accessible Attack Surface of the Affected System or Service", "values": [ { "key": "S", diff --git a/data/json/decision_points/system_exposure_1_0_1.json b/data/json/decision_points/system_exposure_1_0_1.json index a6b713d4..4babf60e 100644 --- a/data/json/decision_points/system_exposure_1_0_1.json +++ b/data/json/decision_points/system_exposure_1_0_1.json @@ -1,10 +1,10 @@ { + "name": "System Exposure", + "description": "The Accessible Attack Surface of the Affected System or Service", "namespace": "ssvc", "version": "1.0.1", "schemaVersion": "1-0-1", "key": "EXP", - "name": "System Exposure", - "description": "The Accessible Attack Surface of the Affected System or Service", "values": [ { "key": "S", diff --git a/data/json/decision_points/technical_impact_1_0_0.json b/data/json/decision_points/technical_impact_1_0_0.json index 5f3c7375..92ecdb4e 100644 --- a/data/json/decision_points/technical_impact_1_0_0.json +++ b/data/json/decision_points/technical_impact_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Technical Impact", + "description": "The technical impact of the vulnerability.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "TI", - "name": "Technical Impact", - "description": "The technical impact of the vulnerability.", "values": [ { "key": "P", diff --git a/data/json/decision_points/utility_1_0_0.json b/data/json/decision_points/utility_1_0_0.json index 033b00a3..71d0ca5f 100644 --- a/data/json/decision_points/utility_1_0_0.json +++ b/data/json/decision_points/utility_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Utility", + "description": "The Usefulness of the Exploit to the Adversary", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "U", - "name": "Utility", - "description": "The Usefulness of the Exploit to the Adversary", "values": [ { "key": "L", diff --git a/data/json/decision_points/utility_1_0_1.json b/data/json/decision_points/utility_1_0_1.json index 79091345..5c22b7fe 100644 --- a/data/json/decision_points/utility_1_0_1.json +++ b/data/json/decision_points/utility_1_0_1.json @@ -1,10 +1,10 @@ { + "name": "Utility", + "description": "The Usefulness of the Exploit to the Adversary", "namespace": "ssvc", "version": "1.0.1", "schemaVersion": "1-0-1", "key": "U", - "name": "Utility", - "description": "The Usefulness of the Exploit to the Adversary", "values": [ { "key": "L", diff --git a/data/json/decision_points/value_density_1_0_0.json b/data/json/decision_points/value_density_1_0_0.json index 725b53fe..4658a012 100644 --- a/data/json/decision_points/value_density_1_0_0.json +++ b/data/json/decision_points/value_density_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Value Density", + "description": "The concentration of value in the target", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "VD", - "name": "Value Density", - "description": "The concentration of value in the target", "values": [ { "key": "D", diff --git a/data/json/decision_points/virulence_1_0_0.json b/data/json/decision_points/virulence_1_0_0.json index 5d2200d9..b08d9539 100644 --- a/data/json/decision_points/virulence_1_0_0.json +++ b/data/json/decision_points/virulence_1_0_0.json @@ -1,10 +1,10 @@ { + "name": "Virulence", + "description": "The speed at which the vulnerability can be exploited.", "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", "key": "V", - "name": "Virulence", - "description": "The speed at which the vulnerability can be exploited.", "values": [ { "key": "S", From 7fe39a3d6ef405f4b8e4242808174a48d25c9a13 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 11:24:01 -0400 Subject: [PATCH 19/99] empty the ssvc/decision_tables/__init__.py --- src/ssvc/decision_tables/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/ssvc/decision_tables/__init__.py b/src/ssvc/decision_tables/__init__.py index ae6a8836..b9c0e1cc 100644 --- a/src/ssvc/decision_tables/__init__.py +++ b/src/ssvc/decision_tables/__init__.py @@ -1,11 +1,3 @@ -#!/usr/bin/env python -""" -file: __init__.py -author: adh -created_at: 2/25/25 10:00 AM -""" - - # Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project @@ -18,11 +10,3 @@ # (“Third Party Software”). See LICENSE.md for more details. # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University - - -def main(): - pass - - -if __name__ == "__main__": - main() From b31e7d7955360fad02dbbce5747da2be52ceafcc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 11:35:48 -0400 Subject: [PATCH 20/99] rename OutcomeGroup.outcomes to OutcomeGroup.values Prepares for future ability to convert from OutcomeGroup to DecisionPoint --- src/ssvc/decision_tables/base.py | 2 +- src/ssvc/outcomes/base.py | 6 +++--- src/ssvc/outcomes/groups.py | 20 ++++++++++---------- src/ssvc/policy_generator.py | 6 +++--- src/test/test_outcomes.py | 4 ++-- src/test/test_policy_generator.py | 8 +++----- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index edfc5ad8..49ac51f4 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -125,7 +125,7 @@ def generate_mapping(self) -> dict[str, str]: dp.name.lower(): dp for dp in self.decision_point_group.decision_points } outcome_lookup = { - outcome.name.lower(): outcome for outcome in self.outcome_group.outcomes + outcome.name.lower(): outcome for outcome in self.outcome_group.values } dp_value_lookup = {} diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index 414b1364..39fdcd31 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -31,19 +31,19 @@ class OutcomeGroup(_Base, _Keyed, _Versioned, BaseModel): Models an outcome group. """ - outcomes: tuple[OutcomeValue, ...] + values: tuple[OutcomeValue, ...] def __iter__(self): """ Allow iteration over the outcomes in the group. """ - return iter(self.outcomes) + return iter(self.values) def __len__(self): """ Allow len() to be called on the group. """ - olist = list(self.outcomes) + olist = list(self.values) l = len(olist) return l diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 0d2aa387..2bf0223e 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -25,7 +25,7 @@ key="DSOI", description="The original SSVC outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Defer", key="D", description="Defer"), OutcomeValue(name="Scheduled", key="S", description="Scheduled"), OutcomeValue(name="Out-of-Cycle", key="O", description="Out-of-Cycle"), @@ -41,7 +41,7 @@ key="PUBLISH", description="The publish outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Do Not Publish", key="N", description="Do Not Publish"), OutcomeValue(name="Publish", key="P", description="Publish"), ), @@ -55,7 +55,7 @@ key="COORDINATE", description="The coordinate outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Decline", key="D", description="Decline"), OutcomeValue(name="Track", key="T", description="Track"), OutcomeValue(name="Coordinate", key="C", description="Coordinate"), @@ -70,7 +70,7 @@ key="MOSCOW", description="The Moscow outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Won't", key="W", description="Won't"), OutcomeValue(name="Could", key="C", description="Could"), OutcomeValue(name="Should", key="S", description="Should"), @@ -86,7 +86,7 @@ key="EISENHOWER", description="The Eisenhower outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Delete", key="D", description="Delete"), OutcomeValue(name="Delegate", key="G", description="Delegate"), OutcomeValue(name="Schedule", key="S", description="Schedule"), @@ -102,7 +102,7 @@ key="CVSS", description="The CVSS outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Low", key="L", description="Low"), OutcomeValue(name="Medium", key="M", description="Medium"), OutcomeValue(name="High", key="H", description="High"), @@ -119,7 +119,7 @@ description="The CISA outcome group. " "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", version="1.0.0", - outcomes=( + values=( OutcomeValue( name="Track", key="T", @@ -160,7 +160,7 @@ key="YES_NO", description="The Yes/No outcome group.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="No", key="N", description="No"), OutcomeValue(name="Yes", key="Y", description="Yes"), ), @@ -174,7 +174,7 @@ key="VALUE_COMPLEXITY", description="The Value/Complexity outcome group.", version="1.0.0", - outcomes=( + values=( # drop, reconsider later, easy win, do first OutcomeValue(name="Drop", key="D", description="Drop"), OutcomeValue(name="Reconsider Later", key="R", description="Reconsider Later"), @@ -191,7 +191,7 @@ key="PARANOIDS", description="PrioritizedRiskRemediation outcome group based on TheParanoids.", version="1.0.0", - outcomes=( + values=( OutcomeValue(name="Track 5", key="5", description="Track"), OutcomeValue(name="Track Closely 4", key="4", description="Track Closely"), OutcomeValue(name="Attend 3", key="3", description="Attend"), diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index c7f58b77..776abe76 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -170,7 +170,7 @@ def _create_policy(self): row[col2] = node[i] oc_idx = self.G.nodes[node]["outcome"] - row["outcome"] = self.outcomes.outcomes[oc_idx].name + row["outcome"] = self.outcomes.values[oc_idx].name row["idx_outcome"] = oc_idx rows.append(row) @@ -195,7 +195,7 @@ def emit_policy(self) -> None: def _assign_outcomes(self): node_count = len(self.G.nodes) - outcomes = [outcome.name for outcome in self.outcomes.outcomes] + outcomes = [outcome.name for outcome in self.outcomes.values] logger.debug(f"Outcomes: {outcomes}") layers = list(nx.topological_generations(self.G)) @@ -208,7 +208,7 @@ def _assign_outcomes(self): logger.debug(f"Toposort: {toposort[:4]}...{toposort[-4:]}") outcome_idx = 0 - assigned_counts = [0 for _ in self.outcomes.outcomes] + assigned_counts = [0 for _ in self.outcomes.values] for node in toposort: # step through the nodes in topological order # and assign outcomes to each node diff --git a/src/test/test_outcomes.py b/src/test/test_outcomes.py index 0095c676..85bdece2 100644 --- a/src/test/test_outcomes.py +++ b/src/test/test_outcomes.py @@ -35,7 +35,7 @@ def test_outcome_group(self): name="Outcome Group", key="OG", description="an outcome group", - outcomes=tuple(values), + values=tuple(values), ) self.assertEqual(og.name, "Outcome Group") @@ -44,7 +44,7 @@ def test_outcome_group(self): self.assertEqual(len(og), len(ALPHABET)) - og_outcomes = list(og.outcomes) + og_outcomes = list(og.values) for i, letter in enumerate(ALPHABET): self.assertEqual(og_outcomes[i].key, letter) self.assertEqual(og_outcomes[i].name, letter) diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 65132436..12233c91 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -34,9 +34,7 @@ def setUp(self) -> None: name="test", description="test", key="TEST", - outcomes=[ - OutcomeValue(key=c, name=c, description=c) for c in self.og_names - ], + values=[OutcomeValue(key=c, name=c, description=c) for c in self.og_names], ) self.dpg = SsvcDecisionPointGroup( name="test", @@ -57,7 +55,7 @@ def setUp(self) -> None: def test_pg_init(self): self.assertEqual(4, len(self.dpg.decision_points)) - self.assertEqual(4, len(self.og.outcomes)) + self.assertEqual(4, len(self.og.values)) pg = PolicyGenerator(dp_group=self.dpg, outcomes=self.og) for w in pg.outcome_weights: @@ -234,7 +232,7 @@ def test_emit_policy(self): for dpg in pg.dpg.decision_points: self.assertIn(dpg.name, stdout) - for og in pg.outcomes.outcomes: + for og in pg.outcomes.values: self.assertIn(og.name.lower(), stdout) def test_create_policy(self): From b720435851c470887dfd321382fa38d8e978a7f5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 11:41:10 -0400 Subject: [PATCH 21/99] add len() to _Valued mixin --- src/ssvc/_mixins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 10b19973..0fd2649e 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -78,6 +78,12 @@ def __iter__(self): """ return iter(self.values) + def __len__(self): + """ + Allow len() to be called on the object. + """ + return len(self.values) + def exclude_if_none(value): return value is None From 71c900384db9bebe0986621e5bb0351110175e76 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 11:41:26 -0400 Subject: [PATCH 22/99] use _Valued mixin --- src/ssvc/outcomes/base.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index 39fdcd31..f4316359 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -17,7 +17,7 @@ from pydantic import BaseModel -from ssvc._mixins import _Base, _Keyed, _Versioned +from ssvc._mixins import _Base, _Keyed, _Valued, _Versioned class OutcomeValue(_Base, _Keyed, BaseModel): @@ -26,25 +26,11 @@ class OutcomeValue(_Base, _Keyed, BaseModel): """ -class OutcomeGroup(_Base, _Keyed, _Versioned, BaseModel): +class OutcomeGroup(_Valued, _Base, _Keyed, _Versioned, BaseModel): """ Models an outcome group. """ values: tuple[OutcomeValue, ...] - def __iter__(self): - """ - Allow iteration over the outcomes in the group. - """ - return iter(self.values) - - def __len__(self): - """ - Allow len() to be called on the group. - """ - olist = list(self.values) - l = len(olist) - return l - # register all instances From 5b03c6a765ee7f4127678e706862d868fe0f4bb0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 11:41:10 -0400 Subject: [PATCH 23/99] add len() to _Valued mixin --- src/ssvc/_mixins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 10b19973..0fd2649e 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -78,6 +78,12 @@ def __iter__(self): """ return iter(self.values) + def __len__(self): + """ + Allow len() to be called on the object. + """ + return len(self.values) + def exclude_if_none(value): return value is None From 66e8410a8f07151adb8ee4e8062e7b957ded1b71 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 12:01:30 -0400 Subject: [PATCH 24/99] add tests --- src/test/test_dp_base.py | 8 ++++++++ src/test/test_mixins.py | 22 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index c6b580e6..58f3e3ed 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -42,6 +42,14 @@ def tearDown(self) -> None: # restore the original registry base._reset_registered() + def test_decision_point_basics(self): + from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned + + # inherits from mixins + mixins = [_Valued, _Base, _Keyed, _Versioned, _Namespaced] + for mixin in mixins: + self.assertIsInstance(self.dp, mixin) + def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) diff --git a/src/test/test_mixins.py b/src/test/test_mixins.py index f86ae5c1..cf049d7a 100644 --- a/src/test/test_mixins.py +++ b/src/test/test_mixins.py @@ -15,7 +15,7 @@ from pydantic import BaseModel, ValidationError -from ssvc._mixins import _Base, _Keyed, _Namespaced, _Versioned +from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned class TestMixins(unittest.TestCase): @@ -88,6 +88,22 @@ def test_keyed_create(self): self.assertRaises(ValidationError, _Keyed) + def test_valued_create(self): + values = ("foo", "bar", "baz", "quux") + obj = _Valued(values=values) + + # length + self.assertEqual(len(obj), len(values)) + + # iteration + for i, v in enumerate(obj): + self.assertEqual(v, values[i]) + + # values + self.assertEqual(obj.values, values) + + self.assertRaises(ValidationError, _Valued) + def test_mixin_combos(self): # We need to test all the combinations mixins = [ @@ -103,9 +119,7 @@ def test_mixin_combos(self): "has_default": True, }, ] - keys_with_defaults = [ - x["args"].keys() for x in mixins if x["has_default"] - ] + keys_with_defaults = [x["args"].keys() for x in mixins if x["has_default"]] # flatten the list keys_with_defaults = [ item for sublist in keys_with_defaults for item in sublist From c496db4a8d7c12bfe2e00a5dc57196e44cfa2ab8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 12:01:30 -0400 Subject: [PATCH 25/99] add tests --- src/test/test_dp_base.py | 8 ++++++++ src/test/test_mixins.py | 22 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index c6b580e6..58f3e3ed 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -42,6 +42,14 @@ def tearDown(self) -> None: # restore the original registry base._reset_registered() + def test_decision_point_basics(self): + from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned + + # inherits from mixins + mixins = [_Valued, _Base, _Keyed, _Versioned, _Namespaced] + for mixin in mixins: + self.assertIsInstance(self.dp, mixin) + def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) diff --git a/src/test/test_mixins.py b/src/test/test_mixins.py index f86ae5c1..cf049d7a 100644 --- a/src/test/test_mixins.py +++ b/src/test/test_mixins.py @@ -15,7 +15,7 @@ from pydantic import BaseModel, ValidationError -from ssvc._mixins import _Base, _Keyed, _Namespaced, _Versioned +from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned class TestMixins(unittest.TestCase): @@ -88,6 +88,22 @@ def test_keyed_create(self): self.assertRaises(ValidationError, _Keyed) + def test_valued_create(self): + values = ("foo", "bar", "baz", "quux") + obj = _Valued(values=values) + + # length + self.assertEqual(len(obj), len(values)) + + # iteration + for i, v in enumerate(obj): + self.assertEqual(v, values[i]) + + # values + self.assertEqual(obj.values, values) + + self.assertRaises(ValidationError, _Valued) + def test_mixin_combos(self): # We need to test all the combinations mixins = [ @@ -103,9 +119,7 @@ def test_mixin_combos(self): "has_default": True, }, ] - keys_with_defaults = [ - x["args"].keys() for x in mixins if x["has_default"] - ] + keys_with_defaults = [x["args"].keys() for x in mixins if x["has_default"]] # flatten the list keys_with_defaults = [ item for sublist in keys_with_defaults for item in sublist From fb2bc28aa821b9531622698a3af1b8b7146ea656 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 14:50:12 -0400 Subject: [PATCH 26/99] fix return to match type hint --- src/ssvc/doctools.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index 93b4591c..59c4b2b5 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -92,8 +92,7 @@ def remove_if_exists(file): logger.debug(f"File {file} does not exist, nothing to remove") -def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool -) -> None: +def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool) -> None: """ Generate the markdown table, json example, and markdown table file for a decision point. @@ -152,7 +151,7 @@ def dump_json( logger.warning( f"File {json_file} already exists, use --overwrite to replace" ) - return json_file + return str(json_file) def main(): From 91ab73bd44dc4f835d3acd977814614d85713c2d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 14:51:49 -0400 Subject: [PATCH 27/99] refactor registration and validation --- src/ssvc/decision_points/base.py | 21 +++++++------ src/ssvc/decision_points/cvss/helpers.py | 39 ++++++++++++------------ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 79716427..5dd0d656 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -18,9 +18,9 @@ import logging -from pydantic import BaseModel +from pydantic import BaseModel, model_validator -from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned +from ssvc._mixins import _Base, _Commented, _Keyed, _Namespaced, _Valued, _Versioned logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def _reset_registered(): REGISTERED_DECISION_POINTS = [] -class SsvcDecisionPointValue(_Base, _Keyed, BaseModel): +class SsvcDecisionPointValue(_Base, _Keyed, _Commented, BaseModel): """ Models a single value option for a decision point. """ @@ -64,19 +64,22 @@ def __str__(self): return self.name -class SsvcDecisionPoint(_Valued, _Keyed, _Versioned, _Namespaced, _Base, BaseModel): +class SsvcDecisionPoint( + _Valued, _Keyed, _Versioned, _Namespaced, _Base, _Commented, BaseModel +): """ Models a single decision point as a list of values. """ values: tuple[SsvcDecisionPointValue, ...] - def __init__(self, **data): - super().__init__(**data) - register(self) - - def __post_init__(self): + @model_validator(mode="after") + def _register(self): + """ + Register the decision point. + """ register(self) + return self def main(): diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index 25782192..c0527ec7 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -22,19 +22,20 @@ def _modify_3(dp: SsvcDecisionPoint): - _dp = deepcopy(dp) - _dp.name = "Modified " + _dp.name - _dp.key = "M" + _dp.key + _dp_dict = deepcopy(dp.model_dump()) + _dp_dict["name"] = "Modified " + _dp_dict["name"] + _dp_dict["key"] = "M" + _dp_dict["key"] # if there is no value named "Not Defined" value, add it nd = NOT_DEFINED_X - values = list(_dp.values) - - names = [v.name for v in values] + values = list(_dp_dict["values"]) + names = [v["name"] for v in values] if nd.name not in names: values.append(nd) - _dp.values = list(values) + _dp_dict["values"] = tuple(values) + + _dp = SsvcDecisionPoint(**_dp_dict) return _dp @@ -51,7 +52,6 @@ def modify_3(dp: SsvcDecisionPoint): """ _dp = _modify_3(dp) - _dp.__post_init__() # call post-init to update the key & register return _dp @@ -68,7 +68,6 @@ def modify_4(dp: SsvcDecisionPoint): _dp = _modify_3(dp) _dp = _modify_4(_dp) - _dp.__post_init__() # call post-init to update the key & register return _dp @@ -78,27 +77,29 @@ def _modify_4(dp: SsvcDecisionPoint): # this method was split out for testing purposes # assumes you've already done the 3.0 modifications - _dp = deepcopy(dp) - # Note: For MSC, MSI, and MSA, the lowest metric value is “Negligible” (N), not “None” (N). - if _dp.key in ["MSC", "MSI", "MSA"]: - for v in _dp.values: - if v.key == "N": - v.name = "Negligible" - v.description.replace(" no ", " negligible ") + _dp_dict = deepcopy(dp.model_dump()) + key = _dp_dict["key"] + if key in ["MSC", "MSI", "MSA"]: + for v in _dp_dict["values"]: + if v["key"] == "N": + v["name"] = "Negligible" + v["description"] = v["description"].replace(" no ", " negligible ") break # Note: For MSI, There is also a highest severity level, Safety (S), in addition to the same values as the # corresponding Base Metric (High, Medium, Low). - if _dp.key == "MSI": + if key == "MSI": _SAFETY = SsvcDecisionPointValue( name="Safety", key="S", description="The Safety metric value measures the impact regarding the Safety of a human actor or " "participant that can be predictably injured as a result of the vulnerability being exploited.", ) - values = list(_dp.values) + values = list(_dp_dict["values"]) values.append(_SAFETY) - _dp.values = list(values) + _dp_dict["values"] = tuple(values) + + _dp = SsvcDecisionPoint(**_dp_dict) return _dp From 5447c62553a1a1968ac4ee3a2b6c39560d3a1eca Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 14:52:22 -0400 Subject: [PATCH 28/99] updated cvss description text --- ...fied_availability_impact_to_the_subsequent_system_1_0_0.json | 2 +- ...d_confidentiality_impact_to_the_subsequent_system_1_0_0.json | 2 +- ...odified_integrity_impact_to_the_subsequent_system_1_0_0.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json index 786f0390..b36e78ae 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json @@ -9,7 +9,7 @@ { "key": "N", "name": "Negligible", - "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." + "description": "There is negligible impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json index ea677a2a..03e23cf7 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json @@ -9,7 +9,7 @@ { "key": "N", "name": "Negligible", - "description": "There is no loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." + "description": "There is negligible loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json index 719e36b4..ab2207f7 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json @@ -9,7 +9,7 @@ { "key": "N", "name": "Negligible", - "description": "There is no loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." + "description": "There is negligible loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." }, { "key": "L", From 3d7ebcf79f53927c67ce6e9463048b08ec13d121 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 13 Mar 2025 15:02:38 -0400 Subject: [PATCH 29/99] allow comments --- src/ssvc/decision_tables/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 49ac51f4..5ceef527 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -20,7 +20,7 @@ import pandas as pd from pydantic import BaseModel, field_validator -from ssvc._mixins import _Base, _Namespaced, _Versioned +from ssvc._mixins import _Base, _Commented, _Namespaced, _Versioned from ssvc.csv_analyzer import check_topological_order from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.base import OutcomeGroup @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) -class DecisionTable(_Versioned, _Namespaced, _Base, BaseModel): +class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): """ The DecisionTable class is a model for decisions in SSVC. From 208b1b34f0e60c8a2d342b49352c3014db3ff1cd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 14:53:57 -0400 Subject: [PATCH 30/99] wip commit --- src/ssvc/decision_points/base.py | 8 + src/ssvc/decision_points/cvss/helpers.py | 2 +- src/ssvc/decision_tables/base.py | 230 +++++++++++----------- src/ssvc/outcomes/groups.py | 2 +- src/ssvc/policy_generator.py | 3 + src/test/test_cvss_helpers.py | 10 +- src/test/test_decision_table.py | 146 ++++++++++++++ src/test/test_dp_base.py | 2 +- src/test/test_prioritization_framework.py | 2 +- 9 files changed, 287 insertions(+), 118 deletions(-) create mode 100644 src/test/test_decision_table.py diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 5dd0d656..432e85f9 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -73,6 +73,14 @@ class SsvcDecisionPoint( values: tuple[SsvcDecisionPointValue, ...] + @model_validator(mode="after") + def _prepend_value_keys(self): + delim = ":" + for value in self.values: + if delim not in value.key: + value.key = delim.join((self.namespace, self.key, value.key)) + return self + @model_validator(mode="after") def _register(self): """ diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index c0527ec7..a3aba975 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -81,7 +81,7 @@ def _modify_4(dp: SsvcDecisionPoint): key = _dp_dict["key"] if key in ["MSC", "MSI", "MSA"]: for v in _dp_dict["values"]: - if v["key"] == "N": + if v["key"].endswith(":N"): v["name"] = "Negligible" v["description"] = v["description"].replace(" no ", " negligible ") break diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 5ceef527..ae2b37eb 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -16,19 +16,28 @@ Provides a DecisionTable class that can be used to model decisions in SSVC """ import logging +from typing import Self import pandas as pd -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, model_validator from ssvc._mixins import _Base, _Commented, _Namespaced, _Versioned from ssvc.csv_analyzer import check_topological_order +from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup +from ssvc.outcomes.base import OutcomeGroup, OutcomeValue from ssvc.policy_generator import PolicyGenerator logger = logging.getLogger(__name__) +def name_to_key(name: str) -> str: + """ + Convert a name to a key by converting to lowercase and replacing spaces with underscores. + """ + return name.lower().replace(" ", "_") + + class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): """ The DecisionTable class is a model for decisions in SSVC. @@ -42,70 +51,62 @@ class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): decision_point_group: SsvcDecisionPointGroup outcome_group: OutcomeGroup - mapping: dict[str, str] + mapping: list = None + _df: pd.DataFrame = None - def __init__(self, **data): - super().__init__(**data) + @property + def outcome_lookup(self) -> dict[str, OutcomeValue]: + """ + Return a lookup table for outcomes. - if not self.mapping: - mapping = self.generate_mapping() - self.__class__.validate_mapping(mapping) - self.mapping = mapping + Returns: + dict: A dictionary of outcomes keyed by outcome value name + """ + return { + name_to_key(outcome.name): outcome for outcome in self.outcome_group.values + } - @classmethod - def mapping_to_table(cls, data: dict) -> pd.DataFrame: + @property + def dp_lookup(self) -> dict[str, SsvcDecisionPoint]: """ - Convert the mapping to a pandas DataFrame. + Return a lookup table for decision points. + + Returns: + dict: A dictionary of decision points keyed by decision point name """ - # extract column names from keys - values = {} - - cols = [] - for key in data.keys(): - parts = key.split(",") - for part in parts: - (_, dp, _) = part.split(":") - cols.append(dp) - - # add the outcome column - first_value = list(data.values())[0] - (okey, _) = first_value.split(":") - cols.append(okey) - - # set up the lists for the columns - for col in cols: - col = col.lower() - values[col] = [] - - for key, value in data.items(): - key = key.lower() - value = value.lower() - - parts = key.split(",") - for part in parts: - (ns, dp, val) = part.split(":") - values[dp].append(val) - - (og_key, og_valkey) = value.split(":") - values[og_key].append(og_valkey) - - # now values is a dict of columnar data - df = pd.DataFrame(values) - # the last column is the outcome - return df + return { + name_to_key(dp.name): dp for dp in self.decision_point_group.decision_points + } - # stub for validating mapping - @field_validator("mapping", mode="before") - @classmethod - def validate_mapping(cls, data): + @property + def dp_value_lookup(self) -> dict[str, dict[str, SsvcDecisionPointValue]]: """ - Placeholder for validating the mapping. + Return a lookup table for decision point values. + Returns: + dict: A dictionary of decision point values keyed by decision point name and value name """ - if len(data) == 0: - return data + dp_value_lookup = {} + for dp in self.decision_point_group.decision_points: + key1 = name_to_key(dp.name) + dp_value_lookup[key1] = {} + for dp_value in dp.values: + key2 = name_to_key(dp_value.name) + dp_value_lookup[key1][key2] = dp_value + return dp_value_lookup + + @model_validator(mode="after") + def _populate_df(self) -> Self: + if self._df is None: + self._df = self.generate_df() + return self - df = cls.mapping_to_table(data) - target = df.columns[-1] + @model_validator(mode="after") + def validate_mapping(self): + """ + Placeholder for validating the mapping. + """ + df = self._df + target = df.columns[-1].lower().replace(" ", "_") problems: list = check_topological_order(df, target) @@ -114,66 +115,76 @@ def validate_mapping(cls, data): else: logger.debug("Mapping passes topological order check") - return data + return self - def generate_mapping(self) -> dict[str, str]: + @model_validator(mode="after") + def _populate_mapping(self) -> Self: """ - Populate the mapping with all possible combinations of decision points. + Populate the mapping if it is not provided. + Args: + data: + + Returns: + """ - mapping = {} - dp_lookup = { - dp.name.lower(): dp for dp in self.decision_point_group.decision_points - } - outcome_lookup = { - outcome.name.lower(): outcome for outcome in self.outcome_group.values - } + if not self.mapping: + mapping = self.table_to_mapping(self._df) + self.mapping = mapping + return self - dp_value_lookup = {} - for dp in self.decision_point_group.decision_points: - key1 = dp.name.lower() - dp_value_lookup[key1] = {} - for dp_value in dp.values: - key2 = dp_value.name.lower() - dp_value_lookup[key1][key2] = dp_value + def as_csv(self) -> str: + """ + Convert the mapping to a CSV string. + """ + raise NotImplementedError + + def as_df(self) -> pd.DataFrame: + """ + Convert the mapping to a pandas DataFrame. + """ + return self.generate_df() + # stub for validating mapping + def generate_df(self) -> pd.DataFrame: + """ + Populate the mapping with all possible combinations of decision points. + """ with PolicyGenerator( dp_group=self.decision_point_group, outcomes=self.outcome_group, ) as policy: - table: pd.DataFrame = policy.clean_policy() - - # the table is a pandas DataFrame - # the columns are the decision points, with the last column being the outcome - # the rows are the possible combinations of decision points - # we need to convert this back to specific decision points and outcomes - for row in table.itertuples(): - outcome_name = row[-1].lower() - outcome = outcome_lookup[outcome_name] - - dp_value_names = row[1:-1] - dp_value_names = [dp_name.lower() for dp_name in dp_value_names] + df: pd.DataFrame = policy.clean_policy() - columns = [col.lower() for col in table.columns] - - # construct the key for the mapping - dp_values = [] - for col, val in zip(columns, dp_value_names): - value_lookup = dp_value_lookup[col] - dp = dp_lookup[col] - val = value_lookup[val] - - key_delim = ":" - k = key_delim.join([dp.namespace, dp.key, val.key]) - dp_values.append(k) - - key = ",".join([str(k) for k in dp_values]) - - outcome_group = self.outcome_group - outcome_str = ":".join([outcome_group.key, outcome.key]) - - mapping[key] = outcome_str + return df - return mapping + def table_to_mapping(self, df: pd.DataFrame) -> list[tuple[str, ...]]: + # copy dataframe + df = pd.DataFrame(df) + + columns = [name_to_key(col) for col in df.columns] + df.columns = columns + data = [] + for index, row in df.iterrows(): + row_data = [] + for column in columns: + value = None + ovalue = None + value_name = name_to_key(row[column]) + try: + value = self.dp_value_lookup[column][value_name] + except KeyError: + ovalue = self.outcome_lookup[value_name] + + if value is not None: + row_data.append(value.key) + + if ovalue is None: + raise ValueError("Outcome value not found") + row_data = tuple(row_data) + t = tuple([row_data, ovalue.key]) + + data.append(t) + return data # convenience alias @@ -194,15 +205,10 @@ def main(): version="1.0.0", decision_point_group=dpg, outcome_group=og, - mapping={}, ) print(dfw.model_dump_json(indent=2)) - print() - print() - print("### JSON SCHEMA ###") - import json - print(json.dumps(DecisionTable.model_json_schema(), indent=2)) + print(dfw._df) if __name__ == "__main__": diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 2bf0223e..73c6c91c 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -66,7 +66,7 @@ """ MOSCOW = OutcomeGroup( - name="Must, Should, Could, Won't", + name="MoSCoW", key="MOSCOW", description="The Moscow outcome group.", version="1.0.0", diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 776abe76..8d119b51 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -179,7 +179,10 @@ def _create_policy(self): def clean_policy(self) -> pd.DataFrame: df = self.policy.copy() + # rename "outcome" column to outcome group name + df = df.rename(columns={"outcome": self.outcomes.name}) print_cols = [c for c in df.columns if not c.startswith("idx_")] + for c in print_cols: df[c] = df[c].str.lower() diff --git a/src/test/test_cvss_helpers.py b/src/test/test_cvss_helpers.py index a5dd413e..1eaf10e8 100644 --- a/src/test/test_cvss_helpers.py +++ b/src/test/test_cvss_helpers.py @@ -48,7 +48,7 @@ def fake_ms_impacts() -> list[CvssDecisionPoint]: return dps -class MyTestCase(unittest.TestCase): +class TestCvssHelpers(unittest.TestCase): def setUp(self) -> None: self.dps = [] for i in range(3): @@ -81,7 +81,13 @@ def test_modify_3(self): self.assertTrue(modified.name.startswith("Modified")) self.assertIn("Not Defined", [v.name for v in modified.values]) - self.assertIn("X", [v.key for v in modified.values]) + + found = False + for v in modified.values: + if v.key.endswith(":X"): + found = True + break + self.assertTrue(found) def test_modify_4(self): # _modify 4 assumes you've already done the Modify 3 step diff --git a/src/test/test_decision_table.py b/src/test/test_decision_table.py new file mode 100644 index 00000000..44f743c5 --- /dev/null +++ b/src/test/test_decision_table.py @@ -0,0 +1,146 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +import tempfile +import unittest +from itertools import product + +import pandas as pd + +from ssvc.decision_tables import base +from ssvc.decision_tables.base import name_to_key +from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.outcomes.base import OutcomeGroup, OutcomeValue + + +class MyTestCase(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.tempdir_path = self.tempdir.name + + dps = [] + for i in range(3): + dpvs = [] + for j in range(3): + dpv = base.SsvcDecisionPointValue( + name=f"Value {i}{j}", + key=f"DP{i}V{j}", + description=f"Decision Point {i} Value {j} Description", + ) + dpvs.append(dpv) + + dp = base.SsvcDecisionPoint( + name=f"Decision Point {i}", + key=f"DP{i}", + description=f"Decision Point {i} Description", + version="1.0.0", + namespace="name1", + values=tuple(dpvs), + ) + dps.append(dp) + + self.dpg = SsvcDecisionPointGroup( + name="Decision Point Group", + description="Decision Point Group description", + decision_points=tuple(dps), + ) + + ogvs = [] + for i in range(3): + ogv = OutcomeValue( + name=f"Outcome Value {i}", + key=f"ov{i}", + description=f"Outcome Value {i} description", + ) + ogvs.append(ogv) + + self.og = OutcomeGroup( + name="Outcome Group", + key="OG", + description="Outcome Group description", + values=tuple(ogvs), + ) + + self.dt = base.DecisionTable( + name="foo", + description="foo description", + decision_point_group=self.dpg, + outcome_group=self.og, + ) + + def tearDown(self): + self.tempdir.cleanup() + + def test_outcome_lookup(self): + d = self.dt.outcome_lookup + self.assertEqual(len(d), len(self.og.values)) + + for i, v in enumerate(self.og.values): + vname = name_to_key(v.name) + self.assertEqual(d[vname], v) + + def test_dp_lookup(self): + d = self.dt.dp_lookup + self.assertEqual(len(d), len(self.dpg.decision_points)) + + for i, dp in enumerate(self.dpg.decision_points): + dpname = name_to_key(dp.name) + self.assertEqual(d[dpname], dp) + + def test_dp_value_lookup(self): + d = self.dt.dp_value_lookup + for dp in self.dpg.decision_points: + dpname = name_to_key(dp.name) + self.assertEqual(len(d[dpname]), len(dp.values)) + + for i, v in enumerate(dp.values): + vname = name_to_key(v.name) + self.assertEqual(d[dpname][vname], v) + + def test_populate_df(self): + with self.subTest("df is set, no change"): + data = { + "a": [1, 2, 3], + "b": [4, 5, 6], + "c": [7, 8, 9], + } + df = pd.DataFrame(data) + self.dt._df = df + self.dt._populate_df() + self.assertTrue(df.equals(self.dt._df)) + + with self.subTest("df is None, populate"): + self.dt._df = None + self.dt._populate_df() + self.assertFalse(df.equals(self.dt._df)) + self.assertIsNotNone(self.dt._df) + self.assertIsInstance(self.dt._df, pd.DataFrame) + + with self.subTest("check df contents"): + nrows = len(list(product(*[dp.values for dp in self.dpg.decision_points]))) + self.assertEqual(len(self.dt._df), nrows) + ncols = len(self.dpg.decision_points) + 1 + self.assertEqual(len(self.dt._df.columns), ncols) + + def test_validate_mapping(self): + with self.subTest("no problems"): + self.dt.validate_mapping() + + with self.subTest("problems"): + # set one of the outcomes out of order + self.dt._df.iloc[0, -1] = "ov2" + with self.assertRaises(ValueError): + self.dt.validate_mapping() + + +if __name__ == "__main__": + unittest.main() diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index 58f3e3ed..15a807d6 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -84,7 +84,7 @@ def test_ssvc_value(self): for i, obj in enumerate(self.values): # should have name, key, description self.assertEqual(obj.name, f"foo{i}") - self.assertEqual(obj.key, f"bar{i}") + self.assertTrue(obj.key.endswith(f"_bar{i}")) self.assertEqual(obj.description, f"baz{i}") # should not have namespace, version diff --git a/src/test/test_prioritization_framework.py b/src/test/test_prioritization_framework.py index 83f7f046..7cd7b1c9 100644 --- a/src/test/test_prioritization_framework.py +++ b/src/test/test_prioritization_framework.py @@ -50,7 +50,7 @@ def test_create(self): self.assertGreater(len(self.framework.mapping), 0) def test_generate_mapping(self): - result = self.framework.generate_mapping() + result = self.framework.generate_df() # there should be one row in result for each combination of decision points combo_count = len(list(self.framework.decision_point_group.combinations())) From 3c983c9df64c8d5a4e7178c11d49b39835b38300 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 15:52:54 -0400 Subject: [PATCH 31/99] add a namespace Enum along with a pydantic dataclass validator to enforce it Valid = str in enum OR str.startswith("x_") --- src/ssvc/namespaces.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/ssvc/namespaces.py diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py new file mode 100644 index 00000000..0dc30fbc --- /dev/null +++ b/src/ssvc/namespaces.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +Provides a namespace enum +""" +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from enum import StrEnum, auto + +# extensions / experimental namespaces should start with the following prefix +# this is to avoid conflicts with official namespaces +X_PFX = "x_" + + +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() + + +class NamespaceValidator: + """Custom type for validating namespaces.""" + + @classmethod + def validate(cls, value: str) -> str: + if value in NameSpace.__members__.values(): + return value + if value.startswith(X_PFX): + return value + raise ValueError( + f"Invalid namespace: {value}. Must be one of {[ns.value for ns in NameSpace]} or start with '{X_PFX}'." + ) + + def __get_validators__(cls): + yield cls.validate + + +def main(): + for ns in NameSpace: + print(ns) + + +if __name__ == "__main__": + main() From 3a44a447e2bf911d182ef0e86e33e9741843a42d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 15:53:25 -0400 Subject: [PATCH 32/99] add validator to _Namespaced mixin class --- src/ssvc/_mixins.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 414c99e1..446ee5c7 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -20,6 +20,7 @@ from pydantic import BaseModel, ConfigDict, field_validator from semver import Version +from ssvc.namespaces import NamespaceValidator from . import _schemaVersion @@ -54,7 +55,12 @@ class _Namespaced(BaseModel): Mixin class for namespaced SSVC objects. """ - namespace: str = "ssvc" + namespace: str + + @field_validator("namespace", mode="before") + @classmethod + def validate_namespace(cls, value): + return NamespaceValidator.validate(value) class _Keyed(BaseModel): From 34ead88a254bf2cb5e5d4eefeeb6faaf1e43ddf9 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 15:54:05 -0400 Subject: [PATCH 33/99] refactor base classes to use NameSpace enum values --- src/ssvc/decision_points/base.py | 2 ++ src/ssvc/decision_points/cvss/base.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 869e3263..dd79f041 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -21,6 +21,7 @@ from pydantic import BaseModel from ssvc._mixins import _Base, _Keyed, _Namespaced, _Versioned +from ssvc.namespaces import NameSpace logger = logging.getLogger(__name__) @@ -66,6 +67,7 @@ class SsvcDecisionPoint(_Base, _Keyed, _Versioned, _Namespaced, BaseModel): Models a single decision point as a list of values. """ + namespace: str = NameSpace.SSVC values: list[SsvcDecisionPointValue] = [] def __iter__(self): diff --git a/src/ssvc/decision_points/cvss/base.py b/src/ssvc/decision_points/cvss/base.py index 9a935991..1fc721ac 100644 --- a/src/ssvc/decision_points/cvss/base.py +++ b/src/ssvc/decision_points/cvss/base.py @@ -18,6 +18,7 @@ from pydantic import BaseModel from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.namespaces import NameSpace class CvssDecisionPoint(SsvcDecisionPoint, BaseModel): @@ -25,4 +26,4 @@ class CvssDecisionPoint(SsvcDecisionPoint, BaseModel): Models a single CVSS decision point as a list of values. """ - namespace: str = "cvss" + namespace: NameSpace = NameSpace.CVSS From 8acba47e070adb8b2bdba6f229a154a0996d5c44 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 15:54:46 -0400 Subject: [PATCH 34/99] add optional "x_" prefix as valid namespace pattern --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 019accee..30849621 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^[a-z0-9-]{3,4}[a-z0-9/\\.-]*$", + "pattern": "^(x_)?[a-z0-9-]{3,4}[a-z0-9/\\.-]*$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { From 5208b696773675a9e2ddc490a2907adb86fec39f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 15:55:16 -0400 Subject: [PATCH 35/99] update unit tests --- src/test/test_doc_helpers.py | 10 +++------- src/test/test_dp_base.py | 6 +++--- src/test/test_mixins.py | 36 ++++++++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/test/test_doc_helpers.py b/src/test/test_doc_helpers.py index 76b217f4..fbbb7f45 100644 --- a/src/test/test_doc_helpers.py +++ b/src/test/test_doc_helpers.py @@ -20,18 +20,14 @@ class MyTestCase(unittest.TestCase): def setUp(self): self.dp = SsvcDecisionPoint( - namespace="test", + namespace="x_test", name="test name", description="test description", key="TK", version="1.0.0", values=( - SsvcDecisionPointValue( - name="A", key="A", description="A Definition" - ), - SsvcDecisionPointValue( - name="B", key="B", description="B Definition" - ), + SsvcDecisionPointValue(name="A", key="A", description="A Definition"), + SsvcDecisionPointValue(name="B", key="B", description="B Definition"), ), ) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index c6b580e6..a386b94c 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -34,7 +34,7 @@ def setUp(self) -> None: key="bar", description="baz", version="1.0.0", - namespace="name1", + namespace="x_test", values=tuple(self.values), ) @@ -64,7 +64,7 @@ def test_registry(self): key="asdfasdf", description="asdfasdf", version="1.33.1", - namespace="asdfasdf", + namespace="x_test", values=self.values, ) @@ -90,7 +90,7 @@ def test_ssvc_decision_point(self): self.assertEqual(obj.key, "bar") self.assertEqual(obj.description, "baz") self.assertEqual(obj.version, "1.0.0") - self.assertEqual(obj.namespace, "name1") + self.assertEqual(obj.namespace, "x_test") self.assertEqual(len(self.values), len(obj.values)) def test_ssvc_value_json_roundtrip(self): diff --git a/src/test/test_mixins.py b/src/test/test_mixins.py index f86ae5c1..19261599 100644 --- a/src/test/test_mixins.py +++ b/src/test/test_mixins.py @@ -12,10 +12,12 @@ # U.S. Patent and Trademark Office by Carnegie Mellon University import unittest +from random import randint from pydantic import BaseModel, ValidationError from ssvc._mixins import _Base, _Keyed, _Namespaced, _Versioned +from ssvc.namespaces import NameSpace class TestMixins(unittest.TestCase): @@ -69,11 +71,27 @@ def test_asdict_roundtrip(self): self.assertEqual(obj2.description, "baz") def test_namespaced_create(self): - obj = _Namespaced() - self.assertEqual(obj.namespace, "ssvc") - - obj = _Namespaced(namespace="quux") - self.assertEqual(obj.namespace, "quux") + # error if no namespace given + with self.assertRaises(ValidationError): + _Namespaced() + + # use the official namespace values + for ns in NameSpace: + obj = _Namespaced(namespace=ns) + self.assertEqual(obj.namespace, ns) + + # error if namespace is not in the enum + # and it doesn't start with x_ + self.assertNotIn("quux", NameSpace) + with self.assertRaises(ValidationError): + _Namespaced(namespace="quux") + + # custom namespaces are allowed as long as they start with x_ + for _ in range(100): + # we're just fuzzing some random strings here + ns = f"x_{randint(1000,1000000)}" + obj = _Namespaced(namespace=ns) + self.assertEqual(obj.namespace, ns) def test_versioned_create(self): obj = _Versioned() @@ -94,8 +112,8 @@ def test_mixin_combos(self): {"class": _Keyed, "args": {"key": "fizz"}, "has_default": False}, { "class": _Namespaced, - "args": {"namespace": "buzz"}, - "has_default": True, + "args": {"namespace": "x_test"}, + "has_default": False, }, { "class": _Versioned, @@ -103,9 +121,7 @@ def test_mixin_combos(self): "has_default": True, }, ] - keys_with_defaults = [ - x["args"].keys() for x in mixins if x["has_default"] - ] + keys_with_defaults = [x["args"].keys() for x in mixins if x["has_default"]] # flatten the list keys_with_defaults = [ item for sublist in keys_with_defaults for item in sublist From 9c36947648fab524825751c276d5d46ffa65962a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 16:00:34 -0400 Subject: [PATCH 36/99] add docstrings --- src/ssvc/namespaces.py | 17 +++++++++++++++++ src/test/test_mixins.py | 13 +++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 0dc30fbc..058d711e 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -23,6 +23,10 @@ class NameSpace(StrEnum): + """ + Defines the official namespaces for SSVC. + """ + # 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() @@ -34,6 +38,19 @@ class NamespaceValidator: @classmethod def validate(cls, value: str) -> str: + """ + Validate the namespace value. The value must be one of the official namespaces or start with 'x_'. + + Args: + value: a string representing a namespace + + Returns: + the validated namespace value + + Raises: + ValueError: if the value is not a valid namespace + + """ if value in NameSpace.__members__.values(): return value if value.startswith(X_PFX): diff --git a/src/test/test_mixins.py b/src/test/test_mixins.py index 19261599..864e78f5 100644 --- a/src/test/test_mixins.py +++ b/src/test/test_mixins.py @@ -70,22 +70,23 @@ def test_asdict_roundtrip(self): self.assertEqual(obj2.name, "quux") self.assertEqual(obj2.description, "baz") - def test_namespaced_create(self): + def test_namespaced_create_errors(self): # error if no namespace given with self.assertRaises(ValidationError): _Namespaced() - # use the official namespace values - for ns in NameSpace: - obj = _Namespaced(namespace=ns) - self.assertEqual(obj.namespace, ns) - # error if namespace is not in the enum # and it doesn't start with x_ self.assertNotIn("quux", NameSpace) with self.assertRaises(ValidationError): _Namespaced(namespace="quux") + def test_namespaced_create(self): + # use the official namespace values + for ns in NameSpace: + obj = _Namespaced(namespace=ns) + self.assertEqual(obj.namespace, ns) + # custom namespaces are allowed as long as they start with x_ for _ in range(100): # we're just fuzzing some random strings here From d49afbf5316f401fad430aad3feb9cabb8548897 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 18 Mar 2025 16:18:52 -0400 Subject: [PATCH 37/99] bump python test version to 3.12 --- .github/workflows/python-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ecbe9b4c..eda4f001 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -21,10 +21,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-tags: true - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip From da21986dd6cd56063b83dc297b449dac1810366a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 11:18:20 -0400 Subject: [PATCH 38/99] update the regex pattern for namespaces, add validation to pydantic field --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- src/ssvc/_mixins.py | 6 ++++-- src/ssvc/namespaces.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 30849621..0d7a8809 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^(x_)?[a-z0-9-]{3,4}[a-z0-9/\\.-]*$", + "pattern": "^(x_)?[a-z0-9]{3,4}([a-z0-9]*[/.-]?[a-z0-9]*[a-z0-9])*$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 446ee5c7..71bf1f66 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -17,7 +17,7 @@ from typing import Optional -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from semver import Version from ssvc.namespaces import NamespaceValidator @@ -55,7 +55,9 @@ class _Namespaced(BaseModel): Mixin class for namespaced SSVC objects. """ - namespace: str + # the field definition enforces the pattern for namespaces + # additional validation is performed in the field_validator immediately after the pattern check + namespace: str = Field(pattern=NS_PATTERN) @field_validator("namespace", mode="before") @classmethod diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 058d711e..208e69e6 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -15,12 +15,24 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +import re from enum import StrEnum, auto # extensions / experimental namespaces should start with the following prefix # this is to avoid conflicts with official namespaces X_PFX = "x_" +# pattern to match +# `^(x_)`: `x_` prefix is optional +# `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters +# `([a-z0-9]+[/.-]?[a-z0-9]*)*[a-z0-9]`: remainder can contain alphanumeric characters, +# periods, hyphens, and forward slashes +# `[/.-]?`: only one punctuation character is allowed between alphanumeric characters +# `[a-z0-9]*`: but an arbitrary number of alphanumeric characters can be between punctuation characters +# `([a-z0-9]+[/.-]?[a-z0-9]*)*` and the total number of punctuation characters is not limited +# `[a-z0-9]$`: the string must end with an alphanumeric character +NS_PATTERN = re.compile(r"^(x_)?[a-z0-9]{3,4}([a-z0-9]*[/.-]?[a-z0-9]*[a-z0-9])*$") + class NameSpace(StrEnum): """ From b57c735f06ca9dd262f80f113cde7260f57e35fc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 11:18:44 -0400 Subject: [PATCH 39/99] refactor namespace validation methods --- src/ssvc/_mixins.py | 19 +++++++++++++++++-- src/ssvc/namespaces.py | 19 ++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 71bf1f66..de117379 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -20,7 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator from semver import Version -from ssvc.namespaces import NamespaceValidator +from ssvc.namespaces import NS_PATTERN, NameSpace from . import _schemaVersion @@ -62,7 +62,22 @@ class _Namespaced(BaseModel): @field_validator("namespace", mode="before") @classmethod def validate_namespace(cls, value): - return NamespaceValidator.validate(value) + """ + Validate the namespace field. + The value will have already been checked against the pattern in the field definition. + The value must be one of the official namespaces or start with 'x_'. + + Args: + value: a string representing a namespace + + Returns: + the validated namespace value + + Raises: + ValueError: if the value is not a valid namespace + """ + + return NameSpace.validate(value) class _Keyed(BaseModel): diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 208e69e6..135c3b24 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -44,17 +44,13 @@ class NameSpace(StrEnum): SSVC = auto() CVSS = auto() - -class NamespaceValidator: - """Custom type for validating namespaces.""" - @classmethod - def validate(cls, value: str) -> str: + def validate(cls, value): """ - Validate the namespace value. The value must be one of the official namespaces or start with 'x_'. + Validate the namespace value. Args: - value: a string representing a namespace + value: the namespace value to validate Returns: the validated namespace value @@ -63,17 +59,14 @@ def validate(cls, value: str) -> str: ValueError: if the value is not a valid namespace """ - if value in NameSpace.__members__.values(): + if value in cls.__members__.values(): return value - if value.startswith(X_PFX): + if value.startswith(X_PFX) and NS_PATTERN.match(value): return value raise ValueError( - f"Invalid namespace: {value}. Must be one of {[ns.value for ns in NameSpace]} or start with '{X_PFX}'." + f"Invalid namespace: {value}. Must be one of {[ns.value for ns in cls]} or start with '{X_PFX}'." ) - def __get_validators__(cls): - yield cls.validate - def main(): for ns in NameSpace: From 4c5e9cde4539a1a064e7d393fad987aabb369da0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 11:18:55 -0400 Subject: [PATCH 40/99] add unit tests --- src/test/test_namespaces.py | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/test_namespaces.py diff --git a/src/test/test_namespaces.py b/src/test/test_namespaces.py new file mode 100644 index 00000000..3866c8cc --- /dev/null +++ b/src/test/test_namespaces.py @@ -0,0 +1,79 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +import unittest + +from ssvc.namespaces import NS_PATTERN, NameSpace + + +class MyTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_ns_pattern(self): + should_match = [ + "foo", + "foo.bar", + "foo.bar.baz", + "foo/bar/baz/quux", + "foo.bar/baz.quux", + ] + should_match.extend([f"x_{ns}" for ns in should_match]) + + for ns in should_match: + with self.subTest(ns=ns): + self.assertTrue(NS_PATTERN.match(ns), ns) + + should_not_match = [ + "", + "ab", + ".foo", + "foo..bar", + "foo/bar//baz", + "foo/bar/baz/", + "(&(&" "foo\\bar", + "foo|bar|baz", + ] + + should_not_match.extend([f"_{ns}" for ns in should_not_match]) + + for ns in should_not_match: + with self.subTest(ns=ns): + self.assertFalse(NS_PATTERN.match(ns)) + + def test_namspace_enum(self): + for ns in NameSpace: + self.assertEqual(ns.name.lower(), ns.value) + + # make sure we have an SSVC namespace with the correct value + self.assertIn("SSVC", NameSpace.__members__) + values = [ns.value for ns in NameSpace] + self.assertIn("ssvc", values) + + def test_namespace_validator(self): + for ns in NameSpace: + self.assertTrue(NameSpace.validate(ns.value)) + + for ns in ["foo", "bar", "baz", "quux"]: + with self.assertRaises(ValueError): + NameSpace.validate(ns) + + for ns in ["x_foo", "x_bar", "x_baz", "x_quux"]: + self.assertEqual(ns, NameSpace.validate(ns)) + + +if __name__ == "__main__": + unittest.main() From d8f5a88df96d4c857e4b68e93005a3ddaa922332 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 12:41:57 -0400 Subject: [PATCH 41/99] simplify regex to avoid inefficiencies --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- src/ssvc/namespaces.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 0d7a8809..43fd8bfc 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^(x_)?[a-z0-9]{3,4}([a-z0-9]*[/.-]?[a-z0-9]*[a-z0-9])*$", + "pattern": "^(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 135c3b24..725ec00e 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -25,13 +25,12 @@ # pattern to match # `^(x_)`: `x_` prefix is optional # `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters -# `([a-z0-9]+[/.-]?[a-z0-9]*)*[a-z0-9]`: remainder can contain alphanumeric characters, -# periods, hyphens, and forward slashes # `[/.-]?`: only one punctuation character is allowed between alphanumeric characters -# `[a-z0-9]*`: but an arbitrary number of alphanumeric characters can be between punctuation characters -# `([a-z0-9]+[/.-]?[a-z0-9]*)*` and the total number of punctuation characters is not limited -# `[a-z0-9]$`: the string must end with an alphanumeric character -NS_PATTERN = re.compile(r"^(x_)?[a-z0-9]{3,4}([a-z0-9]*[/.-]?[a-z0-9]*[a-z0-9])*$") +# `[a-z0-9]+`: at least one alphanumeric character is required after the punctuation character +# `([/.-]?[a-z0-9]+)*`: zero or more occurrences of the punctuation character followed by at least one alphanumeric character +# `$`: end of the string +# last character must be alphanumeric +NS_PATTERN = re.compile(r"^(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$") class NameSpace(StrEnum): From e5fe103a5306585eeba082bc5060179adc271c93 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 14:08:19 -0400 Subject: [PATCH 42/99] add length requirements to namespace patterns and fields --- .../schema/v1/Decision_Point-1-0-1.schema.json | 2 +- src/ssvc/_mixins.py | 2 +- src/ssvc/namespaces.py | 3 ++- src/test/test_mixins.py | 18 ++++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 43fd8bfc..eddfe163 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$", + "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index de117379..1c8a4a24 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -57,7 +57,7 @@ class _Namespaced(BaseModel): # the field definition enforces the pattern for namespaces # additional validation is performed in the field_validator immediately after the pattern check - namespace: str = Field(pattern=NS_PATTERN) + namespace: str = Field(pattern=NS_PATTERN, min_length=3, max_length=25) @field_validator("namespace", mode="before") @classmethod diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 725ec00e..fcb48c80 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -23,6 +23,7 @@ X_PFX = "x_" # pattern to match +# `(?=.{3,25}$)`: 3-25 characters long # `^(x_)`: `x_` prefix is optional # `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters # `[/.-]?`: only one punctuation character is allowed between alphanumeric characters @@ -30,7 +31,7 @@ # `([/.-]?[a-z0-9]+)*`: zero or more occurrences of the punctuation character followed by at least one alphanumeric character # `$`: end of the string # last character must be alphanumeric -NS_PATTERN = re.compile(r"^(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$") +NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$") class NameSpace(StrEnum): diff --git a/src/test/test_mixins.py b/src/test/test_mixins.py index 864e78f5..4db76959 100644 --- a/src/test/test_mixins.py +++ b/src/test/test_mixins.py @@ -81,6 +81,24 @@ def test_namespaced_create_errors(self): with self.assertRaises(ValidationError): _Namespaced(namespace="quux") + # error if namespace starts with x_ but is too short + with self.assertRaises(ValidationError): + _Namespaced(namespace="x_") + + # error if namespace starts with x_ but is too long + for i in range(100): + shortest = "x_aaa" + ns = shortest + "a" * i + with self.subTest(ns=ns): + # length limit set in the NS_PATTERN regex + if len(ns) <= 25: + # expect success on shorter than limit + _Namespaced(namespace=ns) + else: + # expect failure on longer than limit + with self.assertRaises(ValidationError): + _Namespaced(namespace=ns) + def test_namespaced_create(self): # use the official namespace values for ns in NameSpace: From dd7efec9a8423a99c0283816bb21dbac35eee5cf Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 14:34:26 -0400 Subject: [PATCH 43/99] refactor regex again --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- src/ssvc/namespaces.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index eddfe163..98ab1f4c 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$", + "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+){0,22}$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index fcb48c80..64dcf2f7 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -22,16 +22,17 @@ # this is to avoid conflicts with official namespaces X_PFX = "x_" + # pattern to match # `(?=.{3,25}$)`: 3-25 characters long # `^(x_)`: `x_` prefix is optional # `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters # `[/.-]?`: only one punctuation character is allowed between alphanumeric characters # `[a-z0-9]+`: at least one alphanumeric character is required after the punctuation character -# `([/.-]?[a-z0-9]+)*`: zero or more occurrences of the punctuation character followed by at least one alphanumeric character +# `([/.-]?[a-z0-9]+){0,22}`: zero to 22 occurrences of the punctuation character followed by at least one alphanumeric character +# (note that the total limit will kick in at or before this point) # `$`: end of the string -# last character must be alphanumeric -NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+)*$") +NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+){0,22}$") class NameSpace(StrEnum): From 3b7f34a8ef133242eadeca34a88955d42df1f7f6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 14:45:03 -0400 Subject: [PATCH 44/99] add docstrings --- src/ssvc/namespaces.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 64dcf2f7..f931a7ac 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -38,6 +38,21 @@ class NameSpace(StrEnum): """ Defines the official namespaces for SSVC. + + The namespace value must be one of the members of this enum or start with the prefix specified in X_PFX. + Namespaces must be 3-25 lowercase characters long and must start with 3-4 alphanumeric characters after the optional prefix. + Limited punctuation characters (/.-) are allowed between alphanumeric characters, but only one at a time. + + Examples: + + - `ssvc` is *valid* because it is present in the enum + - `custom` is *invalid* because it does not start with the experimental prefix and is not in the enum + - `x_custom` is *valid* because it starts with the experimental prefix and meets the pattern requirements + - `x_custom/extension` is *valid* because it starts with the experimental prefix and meets the pattern requirements + - `x_custom/extension/with/multiple/segments` is *invalid* because it exceeds the maximum length + - `x_custom//extension` is *invalid* because it has multiple punctuation characters in a row + - `x_custom.extension.` is *invalid* because it does not end with an alphanumeric character + - `x_custom.extension.9` is *valid* because it meets the pattern requirements """ # auto() is used to automatically assign values to the members. From 643f193008fe3deff2d24451997bb84a0910467b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 15:10:30 -0400 Subject: [PATCH 45/99] add docs, update docstrings --- .../v1/Decision_Point-1-0-1.schema.json | 2 +- docs/reference/code/index.md | 1 + docs/reference/code/namespaces.md | 3 ++ mkdocs.yml | 1 + src/ssvc/_mixins.py | 4 +-- src/ssvc/namespaces.py | 31 ++++++++++++++----- 6 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 docs/reference/code/namespaces.md diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 98ab1f4c..54aeaa29 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -48,7 +48,7 @@ "namespace": { "type": "string", "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", - "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+){0,22}$", + "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$", "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] }, "version": { diff --git a/docs/reference/code/index.md b/docs/reference/code/index.md index 8f2f47ad..0d36bea8 100644 --- a/docs/reference/code/index.md +++ b/docs/reference/code/index.md @@ -6,4 +6,5 @@ These include: - [CSV Analyzer](analyze_csv.md) - [Policy Generator](policy_generator.md) - [Outcomes](outcomes.md) +- [Namespaces](namespaces.md) - [Doctools](doctools.md) diff --git a/docs/reference/code/namespaces.md b/docs/reference/code/namespaces.md new file mode 100644 index 00000000..bc7ed7b4 --- /dev/null +++ b/docs/reference/code/namespaces.md @@ -0,0 +1,3 @@ +# SSVC Namespaces + +::: ssvc.namespaces diff --git a/mkdocs.yml b/mkdocs.yml index 2e47540c..b7f686c3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,6 +112,7 @@ nav: - CSV Analyzer: 'reference/code/analyze_csv.md' - Policy Generator: 'reference/code/policy_generator.md' - Outcomes: 'reference/code/outcomes.md' + - Namespaces: 'reference/code/namespaces.md' - Doctools: 'reference/code/doctools.md' - Calculator: 'ssvc-calc/index.md' - About: diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 1c8a4a24..2e7edfb2 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -34,7 +34,7 @@ class _Versioned(BaseModel): @field_validator("version") @classmethod - def validate_version(cls, value): + def validate_version(cls, value: str) -> str: """ Validate the version field. Args: @@ -61,7 +61,7 @@ class _Namespaced(BaseModel): @field_validator("namespace", mode="before") @classmethod - def validate_namespace(cls, value): + def validate_namespace(cls, value: str) -> str: """ Validate the namespace field. The value will have already been checked against the pattern in the field definition. diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index f931a7ac..74fc921b 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """ -Provides a namespace enum +SSVC objects use namespaces to distinguish between objects that arise from different +stakeholders or analytical category sources. This module defines the official namespaces +for SSVC and provides a method to validate namespace values. """ # Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors @@ -18,10 +20,8 @@ import re from enum import StrEnum, auto -# extensions / experimental namespaces should start with the following prefix -# this is to avoid conflicts with official namespaces X_PFX = "x_" - +"""The prefix for extension namespaces. Extension namespaces must start with this prefix.""" # pattern to match # `(?=.{3,25}$)`: 3-25 characters long @@ -32,7 +32,20 @@ # `([/.-]?[a-z0-9]+){0,22}`: zero to 22 occurrences of the punctuation character followed by at least one alphanumeric character # (note that the total limit will kick in at or before this point) # `$`: end of the string -NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3,4}([/.-]?[a-z0-9]+){0,22}$") +NS_PATTERN = re.compile(r"^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$") +"""The regular expression pattern for validating namespaces. + +Note: + Namespace values must + + - be 3-25 characters long + - contain only lowercase alphanumeric characters and limited punctuation characters (`/`,`.` and `-`) + - have only one punctuation character in a row + - start with 3-4 alphanumeric characters after the optional extension prefix + - end with an alphanumeric character + + See examples in the `NameSpace` enum. +""" class NameSpace(StrEnum): @@ -43,7 +56,8 @@ class NameSpace(StrEnum): Namespaces must be 3-25 lowercase characters long and must start with 3-4 alphanumeric characters after the optional prefix. Limited punctuation characters (/.-) are allowed between alphanumeric characters, but only one at a time. - Examples: + Example: + Following are examples of valid and invalid namespace values: - `ssvc` is *valid* because it is present in the enum - `custom` is *invalid* because it does not start with the experimental prefix and is not in the enum @@ -61,9 +75,10 @@ class NameSpace(StrEnum): CVSS = auto() @classmethod - def validate(cls, value): + def validate(cls, value: str) -> str: """ - Validate the namespace value. + Validate the namespace value. Valid values are members of the enum or start with the experimental prefix and + meet the specified pattern requirements. Args: value: the namespace value to validate From b02d228e4566753cea6d1683eb9b13ff2324ad2c Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Wed, 19 Mar 2025 15:39:16 -0400 Subject: [PATCH 46/99] Update Decision_Point-1-0-1.schema.json Modify Namespace information and examples as wel.. --- data/schema/v1/Decision_Point-1-0-1.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 54aeaa29..9a864ed0 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -47,9 +47,9 @@ }, "namespace": { "type": "string", - "description": "Namespace (a short, unique string): For example, \"ssvc\" or \"cvss\" to indicate the source of the decision point. See SSVC Documentation for details.", + "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\", \"nciss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$", - "examples": ["ssvc", "cvss", "ssvc-jp", "ssvc/acme", "ssvc/example.com"] + "examples": ["ssvc", "cvss", "nciss", "x_text"] }, "version": { "type": "string", From 02bf023ec31ee3932489e4b06ce628b9e40a3926 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Wed, 19 Mar 2025 15:42:53 -0400 Subject: [PATCH 47/99] Update Decision_Point-1-0-1.schema.json Matching x_custom/extension as examples for schema docs. --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 9a864ed0..8e4d1732 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -49,7 +49,7 @@ "type": "string", "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\", \"nciss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$", - "examples": ["ssvc", "cvss", "nciss", "x_text"] + "examples": ["ssvc", "cvss", "nciss", "x_custom","x_custom/extension"] }, "version": { "type": "string", From 8b482752cfe7fce6731ad9b32bcda4657ea31cef Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 15:47:58 -0400 Subject: [PATCH 48/99] we shouldn't mention nciss yet as it's still a draft PR --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 8e4d1732..549c9458 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -49,7 +49,7 @@ "type": "string", "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\", \"nciss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$", - "examples": ["ssvc", "cvss", "nciss", "x_custom","x_custom/extension"] + "examples": ["ssvc", "cvss", "x_custom","x_custom/extension"] }, "version": { "type": "string", From 2e229b26641f5422e9c423ad04fbe81a599bdfc6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 19 Mar 2025 15:48:48 -0400 Subject: [PATCH 49/99] missed an nciss --- data/schema/v1/Decision_Point-1-0-1.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/schema/v1/Decision_Point-1-0-1.schema.json b/data/schema/v1/Decision_Point-1-0-1.schema.json index 549c9458..0d1faf9c 100644 --- a/data/schema/v1/Decision_Point-1-0-1.schema.json +++ b/data/schema/v1/Decision_Point-1-0-1.schema.json @@ -47,7 +47,7 @@ }, "namespace": { "type": "string", - "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\", \"nciss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", + "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.", "pattern": "^(?=.{3,25}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,22}$", "examples": ["ssvc", "cvss", "x_custom","x_custom/extension"] }, From 5b42e12a5d8de142c37d1110401431c2c5607857 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 20 Mar 2025 11:18:35 -0400 Subject: [PATCH 50/99] revert wip changes --- src/ssvc/decision_points/base.py | 9 +++++---- src/ssvc/decision_points/cvss/helpers.py | 2 +- src/test/test_cvss_helpers.py | 8 +------- src/test/test_dp_base.py | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index d7a1d59e..8a5077ee 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -18,7 +18,7 @@ import logging -from pydantic import BaseModel, model_validator +from pydantic import BaseModel, Field, model_validator from ssvc._mixins import _Base, _Commented, _Keyed, _Namespaced, _Valued, _Versioned from ssvc.namespaces import NameSpace @@ -74,13 +74,14 @@ class SsvcDecisionPoint( namespace: str = NameSpace.SSVC values: tuple[SsvcDecisionPointValue, ...] + value_dict: dict = Field(default_factory=dict, exclude=True) @model_validator(mode="after") - def _prepend_value_keys(self): + def _assign_value_dict(self): delim = ":" for value in self.values: - if delim not in value.key: - value.key = delim.join((self.namespace, self.key, value.key)) + dict_key = delim.join((self.namespace, self.key, value.key)) + self.value_dict[dict_key] = value return self @model_validator(mode="after") diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index a3aba975..c0527ec7 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -81,7 +81,7 @@ def _modify_4(dp: SsvcDecisionPoint): key = _dp_dict["key"] if key in ["MSC", "MSI", "MSA"]: for v in _dp_dict["values"]: - if v["key"].endswith(":N"): + if v["key"] == "N": v["name"] = "Negligible" v["description"] = v["description"].replace(" no ", " negligible ") break diff --git a/src/test/test_cvss_helpers.py b/src/test/test_cvss_helpers.py index 1eaf10e8..3101efff 100644 --- a/src/test/test_cvss_helpers.py +++ b/src/test/test_cvss_helpers.py @@ -81,13 +81,7 @@ def test_modify_3(self): self.assertTrue(modified.name.startswith("Modified")) self.assertIn("Not Defined", [v.name for v in modified.values]) - - found = False - for v in modified.values: - if v.key.endswith(":X"): - found = True - break - self.assertTrue(found) + self.assertIn("X", [v.key for v in modified.values]) def test_modify_4(self): # _modify 4 assumes you've already done the Modify 3 step diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index 788faf8b..58b626a6 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -84,7 +84,7 @@ def test_ssvc_value(self): for i, obj in enumerate(self.values): # should have name, key, description self.assertEqual(obj.name, f"foo{i}") - self.assertTrue(obj.key.endswith(f"_bar{i}")) + self.assertEqual(obj.key, f"bar{i}") self.assertEqual(obj.description, f"baz{i}") # should not have namespace, version From 62306bf7f7d50e093d3d582099e30de7c88d818e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 20 Mar 2025 11:18:52 -0400 Subject: [PATCH 51/99] add new test for value_dict --- src/test/test_dp_base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index 58b626a6..09412802 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -101,6 +101,23 @@ def test_ssvc_decision_point(self): self.assertEqual(obj.namespace, "x_test") self.assertEqual(len(self.values), len(obj.values)) + def test_ssvc_decision_point_value_dict(self): + obj = self.dp + # should have values_dict + self.assertTrue(hasattr(obj, "value_dict")) + self.assertEqual(len(obj.value_dict), len(self.values)) + # keys of value dict should be namespace:key:value.key + for value in self.values: + key = f"{obj.namespace}:{obj.key}:{value.key}" + self.assertIn(key, obj.value_dict) + self.assertEqual(obj.value_dict[key], value) + + # values_dict should NOT appear in serialization + # not in the data structure + self.assertNotIn("value_dict", obj.model_dump()) + # not in the json + self.assertNotIn("value_dict", obj.model_dump_json()) + def test_ssvc_value_json_roundtrip(self): for i, obj in enumerate(self.values): json = obj.model_dump_json() From 4eba0ad250aefe5fd532e18e9f5bf42647334d9e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 20 Mar 2025 13:46:49 -0400 Subject: [PATCH 52/99] fix unit tests --- src/test/test_decision_table.py | 3 ++- src/test/test_prioritization_framework.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/test_decision_table.py b/src/test/test_decision_table.py index 44f743c5..f22eb4a1 100644 --- a/src/test/test_decision_table.py +++ b/src/test/test_decision_table.py @@ -43,7 +43,7 @@ def setUp(self): key=f"DP{i}", description=f"Decision Point {i} Description", version="1.0.0", - namespace="name1", + namespace="x_test", values=tuple(dpvs), ) dps.append(dp) @@ -73,6 +73,7 @@ def setUp(self): self.dt = base.DecisionTable( name="foo", description="foo description", + namespace="x_test", decision_point_group=self.dpg, outcome_group=self.og, ) diff --git a/src/test/test_prioritization_framework.py b/src/test/test_prioritization_framework.py index 7cd7b1c9..eccc75b8 100644 --- a/src/test/test_prioritization_framework.py +++ b/src/test/test_prioritization_framework.py @@ -29,13 +29,14 @@ def setUp(self): name="Test Prioritization Framework", description="Test Prioritization Framework Description", version="1.0.0", + namespace="x_test", decision_point_group=SsvcDecisionPointGroup( name="Test Decision Point Group", description="Test Decision Point Group Description", decision_points=[exploitation_dp, exposure_dp, safety_dp], ), outcome_group=dsoi_og, - mapping={}, + mapping=[], ) pass From 2f0263e2f27c4b09a922a1525d397c263b55761e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 20 Mar 2025 13:47:11 -0400 Subject: [PATCH 53/99] improve name-to-key transformation --- src/ssvc/decision_tables/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index ae2b37eb..5073ebb4 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -16,6 +16,7 @@ Provides a DecisionTable class that can be used to model decisions in SSVC """ import logging +import re from typing import Self import pandas as pd @@ -35,7 +36,9 @@ def name_to_key(name: str) -> str: """ Convert a name to a key by converting to lowercase and replacing spaces with underscores. """ - return name.lower().replace(" ", "_") + # replace non-alphanumeric characters with underscores + new_name = re.sub(r"[^a-z0-9]+", "_", name.lower()) + return new_name class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): @@ -106,7 +109,7 @@ def validate_mapping(self): Placeholder for validating the mapping. """ df = self._df - target = df.columns[-1].lower().replace(" ", "_") + target = name_to_key(df.columns[-1]) problems: list = check_topological_order(df, target) From a995c7fcf1843240b0fe405c1016dd0f2cca7548 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Mar 2025 16:23:05 -0400 Subject: [PATCH 54/99] renamed SsvcDecisionPointValue to DecisionPointValue because it's not namepace-specific Also starting to move things around for namespace organization --- src/ssvc/_mixins.py | 11 ++- src/ssvc/decision_points/__init__.py | 25 ++---- src/ssvc/decision_points/automatable.py | 11 +-- src/ssvc/decision_points/base.py | 87 ++++++++++++++----- src/ssvc/decision_points/critical_software.py | 7 +- src/ssvc/decision_points/cvss/_not_defined.py | 6 +- .../decision_points/cvss/attack_complexity.py | 20 ++--- .../cvss/attack_requirements.py | 6 +- .../decision_points/cvss/attack_vector.py | 28 +++--- .../decision_points/cvss/authentication.py | 12 +-- .../cvss/availability_impact.py | 20 ++--- .../cvss/availability_requirement.py | 14 +-- src/ssvc/decision_points/cvss/base.py | 4 +- .../cvss/collateral_damage_potential.py | 16 ++-- .../cvss/confidentiality_impact.py | 20 ++--- .../cvss/confidentiality_requirement.py | 14 +-- .../decision_points/cvss/equivalence_set_1.py | 8 +- .../decision_points/cvss/equivalence_set_2.py | 6 +- .../decision_points/cvss/equivalence_set_3.py | 8 +- .../decision_points/cvss/equivalence_set_4.py | 8 +- .../decision_points/cvss/equivalence_set_5.py | 25 ++++-- .../decision_points/cvss/equivalence_set_6.py | 20 +++-- .../decision_points/cvss/exploit_maturity.py | 24 ++--- src/ssvc/decision_points/cvss/helpers.py | 17 ++-- src/ssvc/decision_points/cvss/impact_bias.py | 10 +-- .../decision_points/cvss/integrity_impact.py | 20 ++--- .../cvss/integrity_requirement.py | 14 +-- .../cvss/privileges_required.py | 14 +-- .../cvss/qualitative_severity.py | 12 +-- .../decision_points/cvss/remediation_level.py | 10 +-- .../decision_points/cvss/report_confidence.py | 14 +-- src/ssvc/decision_points/cvss/scope.py | 6 +- .../cvss/subsequent_availability_impact.py | 8 +- .../cvss/subsequent_confidentiality_impact.py | 8 +- .../cvss/subsequent_integrity_impact.py | 8 +- .../cvss/supplemental/automatable.py | 6 +- .../cvss/supplemental/provider_urgency.py | 10 +-- .../cvss/supplemental/recovery.py | 8 +- .../cvss/supplemental/safety.py | 6 +- .../cvss/supplemental/value_density.py | 6 +- .../vulnerability_response_effort.py | 8 +- .../cvss/target_distribution.py | 10 +-- .../decision_points/cvss/user_interaction.py | 12 +-- src/ssvc/decision_points/exploitation.py | 11 +-- src/ssvc/decision_points/helpers.py | 8 +- src/ssvc/decision_points/high_value_asset.py | 7 +- src/ssvc/decision_points/human_impact.py | 23 ++--- src/ssvc/decision_points/in_kev.py | 7 +- src/ssvc/decision_points/mission_impact.py | 17 ++-- .../decision_points/mission_prevalence.py | 8 +- .../decision_points/public_safety_impact.py | 17 ++-- .../decision_points/public_value_added.py | 9 +- .../decision_points/report_credibility.py | 7 +- src/ssvc/decision_points/report_public.py | 7 +- src/ssvc/decision_points/safety_impact.py | 21 ++--- src/ssvc/decision_points/ssvc_/__init__.py | 15 ++++ src/ssvc/decision_points/ssvc_/base.py | 29 +++++++ .../decision_points/supplier_cardinality.py | 7 +- .../decision_points/supplier_contacted.py | 7 +- .../decision_points/supplier_engagement.py | 7 +- .../decision_points/supplier_involvement.py | 9 +- src/ssvc/decision_points/system_exposure.py | 11 +-- src/ssvc/decision_points/technical_impact.py | 7 +- src/ssvc/decision_points/utility.py | 16 ++-- src/ssvc/decision_points/value_density.py | 7 +- src/ssvc/decision_tables/base.py | 48 +++++----- src/ssvc/doc_helpers.py | 2 +- src/ssvc/doctools.py | 2 +- src/ssvc/dp_groups/base.py | 41 ++++----- src/ssvc/outcomes/base.py | 4 +- src/test/test_cvss_helpers.py | 12 +-- src/test/test_decision_table.py | 7 +- src/test/test_doc_helpers.py | 8 +- src/test/test_doctools.py | 1 - src/test/test_dp_base.py | 32 ++----- src/test/test_dp_groups.py | 58 +++---------- src/test/test_dp_helpers.py | 12 +-- src/test/test_policy_generator.py | 6 +- 78 files changed, 583 insertions(+), 514 deletions(-) create mode 100644 src/ssvc/decision_points/ssvc_/__init__.py create mode 100644 src/ssvc/decision_points/ssvc_/base.py diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index fabbdc8b..420bac5b 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator from semver import Version +from ssvc import _schemaVersion from ssvc.namespaces import NS_PATTERN, NameSpace -from . import _schemaVersion class _Versioned(BaseModel): @@ -30,7 +30,6 @@ class _Versioned(BaseModel): """ version: str = "0.0.0" - schemaVersion: str = _schemaVersion @field_validator("version") @classmethod @@ -50,6 +49,14 @@ def validate_version(cls, value: str) -> str: return version.__str__() +class _SchemaVersioned(_Versioned, BaseModel): + """ + Mixin class for version + """ + + schemaVersion: str = _schemaVersion + + class _Namespaced(BaseModel): """ Mixin class for namespaced SSVC objects. diff --git a/src/ssvc/decision_points/__init__.py b/src/ssvc/decision_points/__init__.py index 0ffc1a00..04246c4a 100644 --- a/src/ssvc/decision_points/__init__.py +++ b/src/ssvc/decision_points/__init__.py @@ -1,22 +1,3 @@ -#!/usr/bin/env python -""" -The ssvc.decision_points package provides a set of decision points for use in SSVC decision functions. - -Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a -single aspect of the input to a decision function. Decision points should have the following characteristics: - -- A name or label -- A description -- A version (a semantic version string) -- A namespace (a short, unique string): For example, "ssvc" or "cvss" to indicate the source of the decision point -- A key (a short, unique string) that can be used to identify the decision point in a shorthand way -- A short enumeration of possible values - -In turn, each value should have the following characteristics: -- A name or label -- A description -- A key (a short, unique string) that can be used to identify the value in a shorthand way -""" # Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project @@ -29,5 +10,9 @@ # (“Third Party Software”). See LICENSE.md for more details. # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +""" +The ssvc.decision_points package provides a set of decision points for use in SSVC decision functions. -from .base import SsvcDecisionPoint, SsvcDecisionPointValue +Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a +single aspect of the input to a decision function. +""" diff --git a/src/ssvc/decision_points/automatable.py b/src/ssvc/decision_points/automatable.py index 843626b5..0afe0075 100644 --- a/src/ssvc/decision_points/automatable.py +++ b/src/ssvc/decision_points/automatable.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -RAPID = SsvcDecisionPointValue( +RAPID = DecisionPointValue( name="Rapid", key="R", description="Steps 1-4 of the of the kill chain can be reliably automated. If the vulnerability allows remote " "code execution or command injection, the default response should be rapid.", ) -SLOW = SsvcDecisionPointValue( +SLOW = DecisionPointValue( name="Slow", key="S", description="Steps 1-4 of the kill chain cannot be reliably automated for this vulnerability for some reason. " @@ -44,13 +45,13 @@ ) -AUT_NO = SsvcDecisionPointValue( +AUT_NO = DecisionPointValue( name="No", key="N", description="Attackers cannot reliably automate steps 1-4 of the kill chain for this vulnerability. " "These steps are (1) reconnaissance, (2) weaponization, (3) delivery, and (4) exploitation.", ) -AUT_YES = SsvcDecisionPointValue( +AUT_YES = DecisionPointValue( name="Yes", key="Y", description="Attackers can reliably automate steps 1-4 of the kill chain.", diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 8a5077ee..3d09b215 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -18,10 +18,17 @@ import logging -from pydantic import BaseModel, Field, model_validator - -from ssvc._mixins import _Base, _Commented, _Keyed, _Namespaced, _Valued, _Versioned -from ssvc.namespaces import NameSpace +from pydantic import BaseModel, model_validator + +from ssvc._mixins import ( + _Base, + _Commented, + _Keyed, + _Namespaced, + _SchemaVersioned, + _Valued, + _Versioned, +) logger = logging.getLogger(__name__) @@ -56,33 +63,53 @@ def _reset_registered(): REGISTERED_DECISION_POINTS = [] -class SsvcDecisionPointValue(_Base, _Keyed, _Commented, BaseModel): +class DecisionPointValue(_Base, _Keyed, _Commented, BaseModel): """ Models a single value option for a decision point. + + Each value should have the following attributes: + + - name (str): A name + - description (str): A description + - key (str): A key (a short, unique string) that can be used to identify the value in a shorthand way + - _comment (str): An optional comment that will be included in the object. """ def __str__(self): return self.name -class SsvcDecisionPoint( - _Valued, _Keyed, _Versioned, _Namespaced, _Base, _Commented, BaseModel +class ValueSummary(_Versioned, _Keyed, _Namespaced, BaseModel): + """ + A ValueSummary is a simple object that represents a single value for a decision point. + It includes the parent decision point's key, version, namespace, and the value key. + These can be used to reference a specific value in a decision point. + """ + + value: str + + def __str__(self): + s = ":".join([self.namespace, self.key, self.version, self.value]) + return s + + +class DecisionPoint( + _Valued, _Keyed, _SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel ): """ Models a single decision point as a list of values. - """ - namespace: str = NameSpace.SSVC - values: tuple[SsvcDecisionPointValue, ...] - value_dict: dict = Field(default_factory=dict, exclude=True) + Decision points should have the following attributes: - @model_validator(mode="after") - def _assign_value_dict(self): - delim = ":" - for value in self.values: - dict_key = delim.join((self.namespace, self.key, value.key)) - self.value_dict[dict_key] = value - return self + - name (str): The name of the decision point + - description (str): A description of the decision point + - version (str): A semantic version string for the decision point + - namespace (str): The namespace (a short, unique string): For example, "ssvc" or "cvss" to indicate the source of the decision point + - key (str): A key (a short, unique string within the namespace) that can be used to identify the decision point in a shorthand way + - values (tuple): A tuple of DecisionPointValue objects + """ + + values: tuple[DecisionPointValue, ...] @model_validator(mode="after") def _register(self): @@ -92,20 +119,36 @@ def _register(self): register(self) return self + @property + def value_summaries(self) -> list[ValueSummary]: + """ + Return a list of value summaries. + """ + summaries = [] + for value in self.values: + summary = ValueSummary( + key=self.key, + version=self.version, + namespace=self.namespace, + value=value.key, + ) + summaries.append(summary) + return summaries + def main(): - opt_none = SsvcDecisionPointValue( + opt_none = DecisionPointValue( name="None", key="N", description="No exploit available" ) - opt_poc = SsvcDecisionPointValue( + opt_poc = DecisionPointValue( name="PoC", key="P", description="Proof of concept exploit available" ) - opt_active = SsvcDecisionPointValue( + opt_active = DecisionPointValue( name="Active", key="A", description="Active exploitation observed" ) opts = [opt_none, opt_poc, opt_active] - dp = SsvcDecisionPoint( + dp = DecisionPoint( _comment="This is an optional comment that will be included in the object.", values=opts, name="Exploitation", diff --git a/src/ssvc/decision_points/critical_software.py b/src/ssvc/decision_points/critical_software.py index d9dad7d9..8516f137 100644 --- a/src/ssvc/decision_points/critical_software.py +++ b/src/ssvc/decision_points/critical_software.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="System meets a critical software definition.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="System does not meet a critical software definition.", diff --git a/src/ssvc/decision_points/cvss/_not_defined.py b/src/ssvc/decision_points/cvss/_not_defined.py index 8581f2b1..4a3341c1 100644 --- a/src/ssvc/decision_points/cvss/_not_defined.py +++ b/src/ssvc/decision_points/cvss/_not_defined.py @@ -15,16 +15,16 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue -NOT_DEFINED_ND = SsvcDecisionPointValue( +NOT_DEFINED_ND = DecisionPointValue( name="Not Defined", key="ND", description="This metric value is not defined. See CVSS documentation for details.", ) -NOT_DEFINED_X = SsvcDecisionPointValue( +NOT_DEFINED_X = DecisionPointValue( name="Not Defined", key="X", description="This metric value is not defined. See CVSS documentation for details.", diff --git a/src/ssvc/decision_points/cvss/attack_complexity.py b/src/ssvc/decision_points/cvss/attack_complexity.py index 5fa68d59..f5d92d18 100644 --- a/src/ssvc/decision_points/cvss/attack_complexity.py +++ b/src/ssvc/decision_points/cvss/attack_complexity.py @@ -15,17 +15,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH_3 = SsvcDecisionPointValue( +_HIGH_3 = DecisionPointValue( name="High", key="H", description="A successful attack depends on conditions beyond the attacker's control.", ) -_LOW_3 = SsvcDecisionPointValue( +_LOW_3 = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist. An attacker can expect " @@ -33,20 +33,20 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Specialized access conditions exist." ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="The access conditions are somewhat specialized.", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Specialized access conditions exist; for example: the system is exploitable during specific windows " @@ -54,7 +54,7 @@ "configurations), or the system is exploitable with victim interaction (vulnerability exploitable " "only if user opens e-mail)", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist; the system is always " @@ -100,7 +100,7 @@ Defines LOW and HIGH values for CVSS Attack Complexity. """ -LOW_4 = SsvcDecisionPointValue( +LOW_4 = DecisionPointValue( name="Low", key="L", description="The attacker must take no measurable action to exploit the vulnerability. The attack requires no " @@ -108,7 +108,7 @@ "success against the vulnerable system. ", ) -HIGH_4 = SsvcDecisionPointValue( +HIGH_4 = DecisionPointValue( name="High", key="H", description="The successful attack depends on the evasion or circumvention of security-enhancing " diff --git a/src/ssvc/decision_points/cvss/attack_requirements.py b/src/ssvc/decision_points/cvss/attack_requirements.py index 8ae4ba7d..48fbc683 100644 --- a/src/ssvc/decision_points/cvss/attack_requirements.py +++ b/src/ssvc/decision_points/cvss/attack_requirements.py @@ -15,11 +15,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AT_NONE = SsvcDecisionPointValue( +_AT_NONE = DecisionPointValue( name="None", key="N", description="The successful attack does not depend on the deployment and execution conditions of the vulnerable " @@ -28,7 +28,7 @@ ) -_PRESENT = SsvcDecisionPointValue( +_PRESENT = DecisionPointValue( name="Present", key="P", description="The successful attack depends on the presence of specific deployment and execution conditions of " diff --git a/src/ssvc/decision_points/cvss/attack_vector.py b/src/ssvc/decision_points/cvss/attack_vector.py index f20a102d..10d1c003 100644 --- a/src/ssvc/decision_points/cvss/attack_vector.py +++ b/src/ssvc/decision_points/cvss/attack_vector.py @@ -15,17 +15,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_REMOTE = SsvcDecisionPointValue( +_REMOTE = DecisionPointValue( name="Remote", key="R", description="The vulnerability is exploitable remotely.", ) -_LOCAL = SsvcDecisionPointValue( +_LOCAL = DecisionPointValue( name="Local", key="L", description="The vulnerability is only exploitable locally (i.e., it requires physical access or authenticated " @@ -46,7 +46,7 @@ Defines LOCAL and REMOTE values for CVSS Access Vector. """ -_NETWORK = SsvcDecisionPointValue( +_NETWORK = DecisionPointValue( name="Network", key="N", description="A vulnerability exploitable with network access means the vulnerable software is bound to the " @@ -54,14 +54,14 @@ "vulnerability is often termed 'remotely exploitable'.", ) -_ADJACENT = SsvcDecisionPointValue( +_ADJACENT = DecisionPointValue( name="Adjacent Network", key="A", description="A vulnerability exploitable with adjacent network access requires the attacker to have access to " "either the broadcast or collision domain of the vulnerable software.", ) -_LOCAL_2 = SsvcDecisionPointValue( +_LOCAL_2 = DecisionPointValue( name="Local", key="L", description="A vulnerability exploitable with only local access requires the attacker to have either physical " @@ -85,7 +85,7 @@ """ -_NETWORK_2 = SsvcDecisionPointValue( +_NETWORK_2 = DecisionPointValue( name="Network", key="N", description="A vulnerability exploitable with network access means the vulnerable component is bound to the " @@ -94,7 +94,7 @@ "exploitable one or more network hops away (e.g. across layer 3 boundaries from routers).", ) -_ADJACENT_2 = SsvcDecisionPointValue( +_ADJACENT_2 = DecisionPointValue( name="Adjacent", key="A", description="A vulnerability exploitable with adjacent network access means the vulnerable component is bound to " @@ -103,7 +103,7 @@ "3 boundary (e.g. a router).", ) -_LOCAL_3 = SsvcDecisionPointValue( +_LOCAL_3 = DecisionPointValue( name="Local", key="L", description="A vulnerability exploitable with Local access means that the vulnerable component is not bound to " @@ -112,7 +112,7 @@ "on User Interaction to execute a malicious file.", ) -_PHYSICAL_2 = SsvcDecisionPointValue( +_PHYSICAL_2 = DecisionPointValue( name="Physical", key="P", description="A vulnerability exploitable with Physical access requires the attacker to physically touch or " @@ -138,7 +138,7 @@ # CVSS v4 Attack Vector -_NETWORK_3 = SsvcDecisionPointValue( +_NETWORK_3 = DecisionPointValue( name="Network", key="N", description="The vulnerable system is bound to the network stack and the set of possible attackers extends beyond " @@ -147,7 +147,7 @@ "protocol level one or more network hops away (e.g., across one or more routers).", ) -_ADJACENT_3 = SsvcDecisionPointValue( +_ADJACENT_3 = DecisionPointValue( name="Adjacent", key="A", description="The vulnerable system is bound to a protocol stack, but the attack is limited at the protocol level " @@ -157,7 +157,7 @@ "administrative network zone).", ) -_LOCAL_4 = SsvcDecisionPointValue( +_LOCAL_4 = DecisionPointValue( name="Local", key="L", description="The vulnerable system is not bound to the network stack and the attacker’s path is via " @@ -168,7 +168,7 @@ "malicious document).", ) -_PHYSICAL_3 = SsvcDecisionPointValue( +_PHYSICAL_3 = DecisionPointValue( name="Physical", key="P", description="The attack requires the attacker to physically touch or manipulate the vulnerable system. Physical " diff --git a/src/ssvc/decision_points/cvss/authentication.py b/src/ssvc/decision_points/cvss/authentication.py index 516966f1..ea986106 100644 --- a/src/ssvc/decision_points/cvss/authentication.py +++ b/src/ssvc/decision_points/cvss/authentication.py @@ -16,35 +16,35 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AUTH_NONE = SsvcDecisionPointValue( +_AUTH_NONE = DecisionPointValue( name="None", key="N", description="Authentication is not required to exploit the vulnerability.", ) -_SINGLE = SsvcDecisionPointValue( +_SINGLE = DecisionPointValue( name="Single", key="S", description="The vulnerability requires an attacker to be logged into the system (such as at a command line or via a desktop session or web interface).", ) -_MULTIPLE = SsvcDecisionPointValue( +_MULTIPLE = DecisionPointValue( name="Multiple", key="M", description="Exploiting the vulnerability requires that the attacker authenticate two or more times, even if the same credentials are used each time.", ) -_REQUIRED = SsvcDecisionPointValue( +_REQUIRED = DecisionPointValue( name="Required", key="R", description="Authentication is required to access and exploit the vulnerability.", ) -_NOT_REQUIRED = SsvcDecisionPointValue( +_NOT_REQUIRED = DecisionPointValue( name="Not Required", key="N", description="Authentication is not required to access or exploit the vulnerability.", diff --git a/src/ssvc/decision_points/cvss/availability_impact.py b/src/ssvc/decision_points/cvss/availability_impact.py index bc689915..6077b6ab 100644 --- a/src/ssvc/decision_points/cvss/availability_impact.py +++ b/src/ssvc/decision_points/cvss/availability_impact.py @@ -16,11 +16,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="There is total loss of availability, resulting in the attacker being able to fully deny access to " @@ -28,25 +28,25 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="There is reduced performance or interruptions in resource availability.", ) -_NONE_2 = SsvcDecisionPointValue( +_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no impact to the availability of the system.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="Total shutdown of the affected resource. The attacker can render the resource completely unavailable.", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="Considerable lag in or interruptions in resource availability. For example, a network-based flood " @@ -54,7 +54,7 @@ "number of connections successfully complete.", ) -_NONE_1 = SsvcDecisionPointValue( +_NONE_1 = DecisionPointValue( name="None", key="N", description="No impact on availability." ) @@ -89,7 +89,7 @@ Updates None. Removes Partial and Complete. Adds Low and High values for CVSS Availability Impact. """ -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="There is total loss of availability, resulting in the attacker being able to fully deny access to " @@ -97,7 +97,7 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="There is reduced performance or interruptions in resource availability. Even if repeated " @@ -107,7 +107,7 @@ "serious consequence to the Vulnerable System.", ) -_NONE_3 = SsvcDecisionPointValue( +_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no impact to availability within the Vulnerable System.", diff --git a/src/ssvc/decision_points/cvss/availability_requirement.py b/src/ssvc/decision_points/cvss/availability_requirement.py index 09cd2660..42fbb7d6 100644 --- a/src/ssvc/decision_points/cvss/availability_requirement.py +++ b/src/ssvc/decision_points/cvss/availability_requirement.py @@ -16,7 +16,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -25,21 +25,21 @@ from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of availability is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of availability is likely to have a serious adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of availability is likely to have only a limited adverse effect on the organization or " @@ -77,21 +77,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of availability is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of availability is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of availability is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/base.py b/src/ssvc/decision_points/cvss/base.py index 1fc721ac..aa1ff23b 100644 --- a/src/ssvc/decision_points/cvss/base.py +++ b/src/ssvc/decision_points/cvss/base.py @@ -17,11 +17,11 @@ from pydantic import BaseModel -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPoint from ssvc.namespaces import NameSpace -class CvssDecisionPoint(SsvcDecisionPoint, BaseModel): +class CvssDecisionPoint(DecisionPoint, BaseModel): """ Models a single CVSS decision point as a list of values. """ diff --git a/src/ssvc/decision_points/cvss/collateral_damage_potential.py b/src/ssvc/decision_points/cvss/collateral_damage_potential.py index 3c541009..5f348c04 100644 --- a/src/ssvc/decision_points/cvss/collateral_damage_potential.py +++ b/src/ssvc/decision_points/cvss/collateral_damage_potential.py @@ -16,49 +16,49 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_ND from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_MEDIUM_HIGH = SsvcDecisionPointValue( +_MEDIUM_HIGH = DecisionPointValue( name="Medium-High", key="MH", description="A successful exploit of this vulnerability may result in significant physical or property damage or loss.", ) -_LOW_MEDIUM = SsvcDecisionPointValue( +_LOW_MEDIUM = DecisionPointValue( name="Low-Medium", key="LM", description="A successful exploit of this vulnerability may result in moderate physical or property damage or loss.", ) -_CDP_NONE_2 = SsvcDecisionPointValue( +_CDP_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no potential for loss of life, physical assets, productivity or revenue.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="A successful exploit of this vulnerability may result in catastrophic physical or property damage and loss. The range of effect may be over a wide area.", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="A successful exploit of this vulnerability may result in significant physical or property damage or loss.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="A successful exploit of this vulnerability may result in light physical or property damage or loss. The system itself may be damaged or destroyed.", ) -_CDP_NONE = SsvcDecisionPointValue( +_CDP_NONE = DecisionPointValue( name="None", key="N", description="There is no potential for physical or property damage.", diff --git a/src/ssvc/decision_points/cvss/confidentiality_impact.py b/src/ssvc/decision_points/cvss/confidentiality_impact.py index c56abb6a..6550eda5 100644 --- a/src/ssvc/decision_points/cvss/confidentiality_impact.py +++ b/src/ssvc/decision_points/cvss/confidentiality_impact.py @@ -15,11 +15,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="There is total loss of confidentiality, resulting in all resources within the impacted component " @@ -28,7 +28,7 @@ "steals the administrator's password, or private encryption keys of a web server.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, " @@ -37,13 +37,13 @@ "impacted component.", ) -_CI_NONE_2 = SsvcDecisionPointValue( +_CI_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no loss of confidentiality within the impacted component.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="A total compromise of critical system information. A complete loss of system protection resulting in " @@ -51,7 +51,7 @@ "system's data (memory, files, etc).", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="There is considerable informational disclosure. Access to critical system files is possible. There " @@ -59,7 +59,7 @@ "the scope of the loss is constrained.", ) -_CI_NONE = SsvcDecisionPointValue( +_CI_NONE = DecisionPointValue( name="None", key="N", description="No impact on confidentiality.", @@ -98,7 +98,7 @@ """ -_HIGH_1 = SsvcDecisionPointValue( +_HIGH_1 = DecisionPointValue( name="High", key="H", description="There is total loss of confidentiality, resulting in all resources within the impacted component " @@ -107,7 +107,7 @@ "steals the administrator's password, or private encryption keys of a web server.", ) -_LOW_1 = SsvcDecisionPointValue( +_LOW_1 = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, " @@ -116,7 +116,7 @@ "impacted component.", ) -_CI_NONE_3 = SsvcDecisionPointValue( +_CI_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no loss of confidentiality within the impacted component.", diff --git a/src/ssvc/decision_points/cvss/confidentiality_requirement.py b/src/ssvc/decision_points/cvss/confidentiality_requirement.py index 288c05c8..05b1a781 100644 --- a/src/ssvc/decision_points/cvss/confidentiality_requirement.py +++ b/src/ssvc/decision_points/cvss/confidentiality_requirement.py @@ -16,7 +16,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -24,21 +24,21 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of confidentiality is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of confidentiality is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of confidentiality is likely to have only a limited adverse effect on the organization or " @@ -75,21 +75,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of confidentiality is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of confidentiality is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of confidentiality is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/equivalence_set_1.py b/src/ssvc/decision_points/cvss/equivalence_set_1.py index 6e832210..c37fd66c 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_1.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_1.py @@ -15,23 +15,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: AV:P or not(AV:N or PR:N or UI:N)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: (AV:N or PR:N or UI:N) and not (AV:N and PR:N and UI:N) and not AV:P", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: AV:N and PR:N and UI:N", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_2.py b/src/ssvc/decision_points/cvss/equivalence_set_2.py index b20e1cd1..60a3a2ea 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_2.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_2.py @@ -15,7 +15,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -23,12 +23,12 @@ # Levels Constraints Highest Severity Vector(s) # 0 AC:L and AT:N AC:L/AT:N # 1 not (AC:L and AT:N) AC:L/AT:P or AC:H/AT:N -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Low", key="L", description="1: not (AC:L and AT:N)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: AC:L and AT:N", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_3.py b/src/ssvc/decision_points/cvss/equivalence_set_3.py index 5d551b39..56b06191 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_3.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_3.py @@ -15,7 +15,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -24,17 +24,17 @@ # 0 VC:H and VI:H VC:H/VI:H/VA:H # 1 not (VC:H and VI:H) and (VC:H or VI:H or VA:H) VC:L/VI:H/VA:H or VC:H/VI:L/VA:H # 2 not (VC:H or VI:H or VA:H) VC:L/VI:L/VA:L -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: not (VC:H or VI:H or VA:H)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: not (VC:H and VI:H) and (VC:H or VI:H or VA:H)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: VC:H and VI:H", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_4.py b/src/ssvc/decision_points/cvss/equivalence_set_4.py index c7caf550..ffb099a2 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_4.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_4.py @@ -15,7 +15,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -23,17 +23,17 @@ # 0 MSI:S or MSA:S SC:H/SI:S/SA:S # 1 not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H) SC:H/SI:H/SA:H # 2 not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H) SC:L/SI:L/SA:L -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: MSI:S or MSA:S", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_5.py b/src/ssvc/decision_points/cvss/equivalence_set_5.py index 30e3fdba..6895c4e6 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_5.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_5.py @@ -15,7 +15,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -23,9 +23,21 @@ # 0 E:A E:A # 1 E:P E:P # 2 E:U E:U -TWO = SsvcDecisionPointValue(name="Low", key="L", description="2: E:U", ) -ONE = SsvcDecisionPointValue(name="Medium", key="M", description="1: E:P", ) -ZERO = SsvcDecisionPointValue(name="High", key="H", description="0: E:A", ) +TWO = DecisionPointValue( + name="Low", + key="L", + description="2: E:U", +) +ONE = DecisionPointValue( + name="Medium", + key="M", + description="1: E:P", +) +ZERO = DecisionPointValue( + name="High", + key="H", + description="0: E:A", +) EQ5 = CvssDecisionPoint( name="Equivalence Set 5", key="EQ5", @@ -35,16 +47,17 @@ TWO, ONE, ZERO, -), + ), ) VERSIONS = (EQ5,) LATEST = VERSIONS[-1] + def main(): print_versions_and_diffs(VERSIONS) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/ssvc/decision_points/cvss/equivalence_set_6.py b/src/ssvc/decision_points/cvss/equivalence_set_6.py index 4b4887c8..8d99c49d 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_6.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_6.py @@ -15,17 +15,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs # EQ6 → VC/VI/VA+CR/CI/CA with 2 levels specified in Table 29 # 0 (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H) VC:H/VI:H/VA:H/CR:H/IR:H/AR:H # 1 not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H) VC:H/VI:H/VA:H/CR:M/IR:M/AR:M or VC:H/VI:H/VA:L/CR:M/IR:M/AR:H or VC:H/VI:L/VA:H/CR:M/IR:H/AR:M or VC:H/VI:L/VA:L/CR:M/IR:H/AR:H or VC:L/VI:H/VA:H/CR:H/IR:M/AR:M or VC:L/VI:H/VA:L/CR:H/IR:M/AR:H or VC:L/VI:L/VA:H/CR:H/IR:H/AR:M or VC:L/VI:L/VA:L/CR:H/IR:H/AR:H -ONE = SsvcDecisionPointValue(name="Low", key="L", - description="1: not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H)", ) -ZERO = SsvcDecisionPointValue(name="High", key="H", - description="0: (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)", ) +ONE = DecisionPointValue( + name="Low", + key="L", + description="1: not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H)", +) +ZERO = DecisionPointValue( + name="High", + key="H", + description="0: (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)", +) EQ6 = CvssDecisionPoint( name="Equivalence Set 6", key="EQ6", @@ -40,10 +46,10 @@ VERSIONS = (EQ6,) LATEST = VERSIONS[-1] + def main(): print_versions_and_diffs(VERSIONS) -if __name__ == '__main__': +if __name__ == "__main__": main() - diff --git a/src/ssvc/decision_points/cvss/exploit_maturity.py b/src/ssvc/decision_points/cvss/exploit_maturity.py index 2a3eee2a..96f9ea15 100644 --- a/src/ssvc/decision_points/cvss/exploit_maturity.py +++ b/src/ssvc/decision_points/cvss/exploit_maturity.py @@ -16,7 +16,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -24,7 +24,7 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Functional autonomous code exists, or no exploit is required (manual trigger) and details are widely " @@ -34,14 +34,14 @@ "easy-to-use automated tools.", ) -_FUNCTIONAL_2 = SsvcDecisionPointValue( +_FUNCTIONAL_2 = DecisionPointValue( name="Functional", key="F", description="Functional exploit code is available. The code works in most situations where the vulnerability " "exists.", ) -_PROOF_OF_CONCEPT_2 = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT_2 = DecisionPointValue( name="Proof-of-Concept", key="POC", description="Proof-of-concept exploit code is available, or an attack demonstration is not practical for most " @@ -49,13 +49,13 @@ "modification by a skilled attacker.", ) -_UNPROVEN_2 = SsvcDecisionPointValue( +_UNPROVEN_2 = DecisionPointValue( name="Unproven", key="U", description="No exploit code is available, or an exploit is theoretical.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Either the vulnerability is exploitable by functional mobile autonomous code or no exploit is " @@ -64,14 +64,14 @@ "via a mobile autonomous agent (a worm or virus).", ) -_FUNCTIONAL = SsvcDecisionPointValue( +_FUNCTIONAL = DecisionPointValue( name="Functional", key="F", description="Functional exploit code is available. The code works in most situations where the vulnerability is " "exploitable.", ) -_PROOF_OF_CONCEPT = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT = DecisionPointValue( name="Proof of Concept", key="P", description="Proof of concept exploit code or an attack demonstration that is not practically applicable to " @@ -79,7 +79,7 @@ "require substantial hand tuning by a skilled attacker for use against deployed systems.", ) -_UNPROVEN = SsvcDecisionPointValue( +_UNPROVEN = DecisionPointValue( name="Unproven", key="U", description="No exploit code is yet available or an exploit method is entirely theoretical.", @@ -140,7 +140,7 @@ """ -_ATTACKED = SsvcDecisionPointValue( +_ATTACKED = DecisionPointValue( name="Attacked", key="A", description="Based on available threat intelligence either of the following must apply: Attacks targeting " @@ -148,7 +148,7 @@ "to exploit the vulnerability are publicly or privately available (such as exploit toolkits)", ) -_PROOF_OF_CONCEPT_3 = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT_3 = DecisionPointValue( name="Proof-of-Concept", key="P", description="Based on available threat intelligence each of the following must apply: Proof-of-concept exploit " @@ -157,7 +157,7 @@ "(i.e., the “Attacked” value does not apply)", ) -_UNREPORTED = SsvcDecisionPointValue( +_UNREPORTED = DecisionPointValue( name="Unreported", key="U", description="Based on available threat intelligence each of the following must apply: No knowledge of publicly " diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index c0527ec7..457e985b 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -17,11 +17,12 @@ from copy import deepcopy -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X +from ssvc.decision_points.cvss.base import CvssDecisionPoint as DecisionPoint -def _modify_3(dp: SsvcDecisionPoint): +def _modify_3(dp: DecisionPoint): _dp_dict = deepcopy(dp.model_dump()) _dp_dict["name"] = "Modified " + _dp_dict["name"] _dp_dict["key"] = "M" + _dp_dict["key"] @@ -35,12 +36,12 @@ def _modify_3(dp: SsvcDecisionPoint): values.append(nd) _dp_dict["values"] = tuple(values) - _dp = SsvcDecisionPoint(**_dp_dict) + _dp = DecisionPoint(**_dp_dict) return _dp -def modify_3(dp: SsvcDecisionPoint): +def modify_3(dp: DecisionPoint): """ Prepends "Modified " to the name and "M" to the key of the given object. Also adds a value of "Not Defined" to the values list. @@ -55,7 +56,7 @@ def modify_3(dp: SsvcDecisionPoint): return _dp -def modify_4(dp: SsvcDecisionPoint): +def modify_4(dp: DecisionPoint): """ Modifies a CVSS v4 Base Metric decision point object. @@ -72,7 +73,7 @@ def modify_4(dp: SsvcDecisionPoint): return _dp -def _modify_4(dp: SsvcDecisionPoint): +def _modify_4(dp: DecisionPoint): # note: # this method was split out for testing purposes # assumes you've already done the 3.0 modifications @@ -89,7 +90,7 @@ def _modify_4(dp: SsvcDecisionPoint): # Note: For MSI, There is also a highest severity level, Safety (S), in addition to the same values as the # corresponding Base Metric (High, Medium, Low). if key == "MSI": - _SAFETY = SsvcDecisionPointValue( + _SAFETY = DecisionPointValue( name="Safety", key="S", description="The Safety metric value measures the impact regarding the Safety of a human actor or " @@ -99,7 +100,7 @@ def _modify_4(dp: SsvcDecisionPoint): values.append(_SAFETY) _dp_dict["values"] = tuple(values) - _dp = SsvcDecisionPoint(**_dp_dict) + _dp = DecisionPoint(**_dp_dict) return _dp diff --git a/src/ssvc/decision_points/cvss/impact_bias.py b/src/ssvc/decision_points/cvss/impact_bias.py index ad113083..4d79f656 100644 --- a/src/ssvc/decision_points/cvss/impact_bias.py +++ b/src/ssvc/decision_points/cvss/impact_bias.py @@ -15,29 +15,29 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AVAILABILITY = SsvcDecisionPointValue( +_AVAILABILITY = DecisionPointValue( name="Availability", key="A", description="Availability Impact is assigned greater weight than Confidentiality Impact or Integrity Impact.", ) -_INTEGRITY = SsvcDecisionPointValue( +_INTEGRITY = DecisionPointValue( name="Integrity", key="I", description="Integrity Impact is assigned greater weight than Confidentiality Impact or Availability Impact.", ) -_CONFIDENTIALITY = SsvcDecisionPointValue( +_CONFIDENTIALITY = DecisionPointValue( name="Confidentiality", key="C", description="Confidentiality impact is assigned greater weight than Integrity Impact or Availability Impact.", ) -_NORMAL = SsvcDecisionPointValue( +_NORMAL = DecisionPointValue( name="Normal", key="N", description="Confidentiality Impact, Integrity Impact, and Availability Impact are all assigned the same weight.", diff --git a/src/ssvc/decision_points/cvss/integrity_impact.py b/src/ssvc/decision_points/cvss/integrity_impact.py index 6cc2584c..bddbb576 100644 --- a/src/ssvc/decision_points/cvss/integrity_impact.py +++ b/src/ssvc/decision_points/cvss/integrity_impact.py @@ -16,18 +16,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_II_HIGH = SsvcDecisionPointValue( +_II_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection.", ) -_II_LOW = SsvcDecisionPointValue( +_II_LOW = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -35,19 +35,19 @@ "direct, serious impact on the impacted component.", ) -_II_NONE_2 = SsvcDecisionPointValue( +_II_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no impact to the integrity of the system.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="A total compromise of system integrity. There is a complete loss of system protection resulting in " "the entire system being compromised. The attacker has sovereign control to modify any system files.", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="Considerable breach in integrity. Modification of critical system files or information is possible, " @@ -56,7 +56,7 @@ "but at random or in a limited context or scope.", ) -_II_NONE = SsvcDecisionPointValue( +_II_NONE = DecisionPointValue( name="None", key="N", description="No impact on integrity." ) @@ -91,13 +91,13 @@ Updates None. Removes Partial and Complete. Adds Low and High values for CVSS Integrity Impact. """ -_II_HIGH_2 = SsvcDecisionPointValue( +_II_HIGH_2 = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection.", ) -_II_LOW_2 = SsvcDecisionPointValue( +_II_LOW_2 = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -106,7 +106,7 @@ ) -_II_NONE_3 = SsvcDecisionPointValue( +_II_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no loss of integrity within the Vulnerable System.", diff --git a/src/ssvc/decision_points/cvss/integrity_requirement.py b/src/ssvc/decision_points/cvss/integrity_requirement.py index fed364a6..9d1037af 100644 --- a/src/ssvc/decision_points/cvss/integrity_requirement.py +++ b/src/ssvc/decision_points/cvss/integrity_requirement.py @@ -16,7 +16,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -24,21 +24,21 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of integrity is likely to have a catastrophic adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of integrity is likely to have a serious adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of integrity is likely to have only a limited adverse effect on the organization or individuals " @@ -75,21 +75,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of integrity is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of integrity is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of integrity is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/privileges_required.py b/src/ssvc/decision_points/cvss/privileges_required.py index e9cb0ea5..d6bc27d7 100644 --- a/src/ssvc/decision_points/cvss/privileges_required.py +++ b/src/ssvc/decision_points/cvss/privileges_required.py @@ -15,11 +15,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="The attacker is authorized with (i.e. requires) privileges that provide significant (e.g. " @@ -27,7 +27,7 @@ "files.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="The attacker is authorized with (i.e. requires) privileges that provide basic user capabilities that " @@ -35,7 +35,7 @@ "privileges may have the ability to cause an impact only to non-sensitive resources.", ) -_PR_NONE = SsvcDecisionPointValue( +_PR_NONE = DecisionPointValue( name="None", key="N", description="The attacker is unauthorized prior to attack, and therefore does not require any access to settings " @@ -62,14 +62,14 @@ """ -_PR_NONE_2 = SsvcDecisionPointValue( +_PR_NONE_2 = DecisionPointValue( name="None", key="N", description="The attacker is unauthorized prior to attack, and therefore does not require any access to settings " "or files to carry out an attack.", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="The attacker is authorized with (i.e., requires) privileges that provide basic capabilities that " @@ -77,7 +77,7 @@ "an attacker with Low privileges has the ability to access only non-sensitive resources.", ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="The attacker is authorized with (i.e., requires) privileges that provide significant (e.g., " diff --git a/src/ssvc/decision_points/cvss/qualitative_severity.py b/src/ssvc/decision_points/cvss/qualitative_severity.py index 23358fe7..64c1ab03 100644 --- a/src/ssvc/decision_points/cvss/qualitative_severity.py +++ b/src/ssvc/decision_points/cvss/qualitative_severity.py @@ -15,32 +15,32 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -QS_NONE = SsvcDecisionPointValue( +QS_NONE = DecisionPointValue( name="None", key="N", description="No severity rating (0.0)", ) -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="Low (0.1 - 3.9)", ) -MEDIUM = SsvcDecisionPointValue( +MEDIUM = DecisionPointValue( name="Medium", key="M", description="Medium (4.0 - 6.9)", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="High (7.0 - 8.9)", ) -CRITICAL = SsvcDecisionPointValue( +CRITICAL = DecisionPointValue( name="Critical", key="C", description="Critical (9.0 - 10.0)", diff --git a/src/ssvc/decision_points/cvss/remediation_level.py b/src/ssvc/decision_points/cvss/remediation_level.py index 73a031ef..ee1a8020 100644 --- a/src/ssvc/decision_points/cvss/remediation_level.py +++ b/src/ssvc/decision_points/cvss/remediation_level.py @@ -16,19 +16,19 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_UNAVAILABLE = SsvcDecisionPointValue( +_UNAVAILABLE = DecisionPointValue( name="Unavailable", key="U", description="There is either no solution available or it is impossible to apply.", ) -_WORKAROUND = SsvcDecisionPointValue( +_WORKAROUND = DecisionPointValue( name="Workaround", key="W", description="There is an unofficial, non-vendor solution available. In some cases, users of the affected " @@ -37,14 +37,14 @@ "plugging the hole for the mean time and no official remediation is available, this value can be set.", ) -_TEMPORARY_FIX = SsvcDecisionPointValue( +_TEMPORARY_FIX = DecisionPointValue( name="Temporary Fix", key="TF", description="There is an official but temporary fix available. This includes instances where the vendor issues a " "temporary hotfix, tool or official workaround.", ) -_OFFICIAL_FIX = SsvcDecisionPointValue( +_OFFICIAL_FIX = DecisionPointValue( name="Official Fix", key="OF", description="A complete vendor solution is available. Either the vendor has issued the final, official patch " diff --git a/src/ssvc/decision_points/cvss/report_confidence.py b/src/ssvc/decision_points/cvss/report_confidence.py index 93ea24fc..6145153b 100644 --- a/src/ssvc/decision_points/cvss/report_confidence.py +++ b/src/ssvc/decision_points/cvss/report_confidence.py @@ -16,7 +16,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -24,7 +24,7 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_CONFIRMED_2 = SsvcDecisionPointValue( +_CONFIRMED_2 = DecisionPointValue( name="Confirmed", key="C", description="Detailed reports exist, or functional reproduction is possible (functional exploits may provide " @@ -32,7 +32,7 @@ "or the author or vendor of the affected code has confirmed the presence of the vulnerability.", ) -_REASONABLE = SsvcDecisionPointValue( +_REASONABLE = DecisionPointValue( name="Reasonable", key="R", description="Significant details are published, but researchers either do not have full confidence in the root " @@ -41,7 +41,7 @@ "impact is able to be verified (proof-of-concept exploits may provide this).", ) -_UNKNOWN = SsvcDecisionPointValue( +_UNKNOWN = DecisionPointValue( name="Unknown", key="U", description="There are reports of impacts that indicate a vulnerability is present. The reports indicate that the " @@ -51,7 +51,7 @@ "differences described.", ) -_CONFIRMED = SsvcDecisionPointValue( +_CONFIRMED = DecisionPointValue( name="Confirmed", key="C", description="Vendor or author of the affected technology has acknowledged that the vulnerability exists. This " @@ -60,7 +60,7 @@ "widespread exploitation.", ) -_UNCORROBORATED = SsvcDecisionPointValue( +_UNCORROBORATED = DecisionPointValue( name="Uncorroborated", key="UR", description="Multiple non-official sources; possibily including independent security companies or research " @@ -68,7 +68,7 @@ "ambiguity.", ) -_UNCONFIRMED = SsvcDecisionPointValue( +_UNCONFIRMED = DecisionPointValue( name="Unconfirmed", key="UC", description="A single unconfirmed source or possibly several conflicting reports. There is little confidence in " diff --git a/src/ssvc/decision_points/cvss/scope.py b/src/ssvc/decision_points/cvss/scope.py index 9eaf0b35..9783aafe 100644 --- a/src/ssvc/decision_points/cvss/scope.py +++ b/src/ssvc/decision_points/cvss/scope.py @@ -16,18 +16,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_CHANGED = SsvcDecisionPointValue( +_CHANGED = DecisionPointValue( name="Changed", key="C", description="An exploited vulnerability can affect resources beyond the authorization privileges intended by the " "vulnerable component. In this case the vulnerable component and the impacted component are different.", ) -_UNCHANGED = SsvcDecisionPointValue( +_UNCHANGED = DecisionPointValue( name="Unchanged", key="U", description="An exploited vulnerability can only affect resources managed by the same authority. In this case the " diff --git a/src/ssvc/decision_points/cvss/subsequent_availability_impact.py b/src/ssvc/decision_points/cvss/subsequent_availability_impact.py index 1a4cf449..3d90b15e 100644 --- a/src/ssvc/decision_points/cvss/subsequent_availability_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_availability_impact.py @@ -15,11 +15,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_SA_HIGH = SsvcDecisionPointValue( +_SA_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of availability, resulting in the attacker being able to fully deny access to " @@ -27,7 +27,7 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_SA_LOW = SsvcDecisionPointValue( +_SA_LOW = DecisionPointValue( name="Low", key="L", description="Performance is reduced or there are interruptions in resource availability. Even if repeated " @@ -35,7 +35,7 @@ "deny service to legitimate users.", ) -_SA_NONE = SsvcDecisionPointValue( +_SA_NONE = DecisionPointValue( name="None", key="N", description="There is no impact to availability within the Subsequent System or all availability impact is " diff --git a/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py b/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py index 413dc803..77f0a540 100644 --- a/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py @@ -15,18 +15,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="There is no loss of confidentiality within the Subsequent System or all confidentiality impact is " "constrained to the Vulnerable System.", ) -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, but the " @@ -34,7 +34,7 @@ "limited. The information disclosure does not cause a direct, serious loss to the Subsequent System.", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of confidentiality, resulting in all resources within the Subsequent System " diff --git a/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py b/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py index 4a2efbf5..ccb02300 100644 --- a/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py @@ -15,11 +15,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -SI_HIGH = SsvcDecisionPointValue( +SI_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection. For example, the attacker is able " @@ -28,7 +28,7 @@ "System.", ) -SI_LOW = SsvcDecisionPointValue( +SI_LOW = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -36,7 +36,7 @@ "serious impact to the Subsequent System.", ) -SI_NONE = SsvcDecisionPointValue( +SI_NONE = DecisionPointValue( name="None", key="N", description="There is no loss of integrity within the Subsequent System or all integrity impact is constrained to " diff --git a/src/ssvc/decision_points/cvss/supplemental/automatable.py b/src/ssvc/decision_points/cvss/supplemental/automatable.py index 38c1ffe2..a54bc0a4 100644 --- a/src/ssvc/decision_points/cvss/supplemental/automatable.py +++ b/src/ssvc/decision_points/cvss/supplemental/automatable.py @@ -15,18 +15,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="Attackers cannot reliably automate all 4 steps of the kill chain for this vulnerability for " "some reason. These steps are reconnaissance, weaponization, delivery, and exploitation.", ) -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="Attackers can reliably automate all 4 steps of the kill chain. These steps are " diff --git a/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py b/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py index 4aaeb452..af34df68 100644 --- a/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py +++ b/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py @@ -15,27 +15,27 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -RED = SsvcDecisionPointValue( +RED = DecisionPointValue( name="Red", key="R", description="Provider has assessed the impact of this vulnerability as having the highest urgency.", ) -AMBER = SsvcDecisionPointValue( +AMBER = DecisionPointValue( name="Amber", key="A", description="Provider has assessed the impact of this vulnerability as having a moderate urgency.", ) -GREEN = SsvcDecisionPointValue( +GREEN = DecisionPointValue( name="Green", key="G", description="Provider has assessed the impact of this vulnerability as having a reduced urgency.", ) -CLEAR = SsvcDecisionPointValue( +CLEAR = DecisionPointValue( name="Clear", key="C", description="Provider has assessed the impact of this vulnerability as having no urgency (Informational).", diff --git a/src/ssvc/decision_points/cvss/supplemental/recovery.py b/src/ssvc/decision_points/cvss/supplemental/recovery.py index 14ea6ef8..717c38da 100644 --- a/src/ssvc/decision_points/cvss/supplemental/recovery.py +++ b/src/ssvc/decision_points/cvss/supplemental/recovery.py @@ -15,23 +15,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -AUTOMATIC = SsvcDecisionPointValue( +AUTOMATIC = DecisionPointValue( name="Automatic", key="A", description="The system recovers services automatically after an attack has been performed.", ) -USER = SsvcDecisionPointValue( +USER = DecisionPointValue( name="User", key="U", description="The system requires manual intervention by the user to recover services, after an attack has " "been performed.", ) -IRRECOVERABLE = SsvcDecisionPointValue( +IRRECOVERABLE = DecisionPointValue( name="Irrecoverable", key="I", description="The system services are irrecoverable by the user, after an attack has been performed.", diff --git a/src/ssvc/decision_points/cvss/supplemental/safety.py b/src/ssvc/decision_points/cvss/supplemental/safety.py index 14152687..c9d0d106 100644 --- a/src/ssvc/decision_points/cvss/supplemental/safety.py +++ b/src/ssvc/decision_points/cvss/supplemental/safety.py @@ -16,18 +16,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -PRESENT = SsvcDecisionPointValue( +PRESENT = DecisionPointValue( name="Present", key="P", description="Consequences of the vulnerability meet definition of IEC 61508 consequence categories of " '"marginal," "critical," or "catastrophic."', ) -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="Consequences of the vulnerability meet definition of IEC 61508 consequence category " diff --git a/src/ssvc/decision_points/cvss/supplemental/value_density.py b/src/ssvc/decision_points/cvss/supplemental/value_density.py index 4e56c1dc..0372d0a9 100644 --- a/src/ssvc/decision_points/cvss/supplemental/value_density.py +++ b/src/ssvc/decision_points/cvss/supplemental/value_density.py @@ -15,18 +15,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -DIFFUSE = SsvcDecisionPointValue( +DIFFUSE = DecisionPointValue( name="Diffuse", key="D", description="The vulnerable system has limited resources. That is, the resources that the attacker will " "gain control over with a single exploitation event are relatively small.", ) -CONCENTRATED = SsvcDecisionPointValue( +CONCENTRATED = DecisionPointValue( name="Concentrated", key="C", description="The vulnerable system is rich in resources. Heuristically, such systems are often the direct " diff --git a/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py b/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py index d69077ee..8688f141 100644 --- a/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py +++ b/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py @@ -15,23 +15,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="The effort required to respond to a vulnerability is low/trivial.", ) -MODERATE = SsvcDecisionPointValue( +MODERATE = DecisionPointValue( name="Moderate", key="M", description="The actions required to respond to a vulnerability require some effort on behalf of the " "consumer and could cause minimal service impact to implement.", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="The actions required to respond to a vulnerability are significant and/or difficult, and may " diff --git a/src/ssvc/decision_points/cvss/target_distribution.py b/src/ssvc/decision_points/cvss/target_distribution.py index 2f408e67..981cfadf 100644 --- a/src/ssvc/decision_points/cvss/target_distribution.py +++ b/src/ssvc/decision_points/cvss/target_distribution.py @@ -16,34 +16,34 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Targets exist inside the environment on a considerable scale. Between 50% - 100% of the total " "environment is considered at risk.", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Targets exist inside the environment, but on a medium scale. Between 16% - 49% of the total " "environment is at risk.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Targets exist inside the environment, but on a small scale. Between 1% - 15% of the total " "environment is at risk.", ) -_TD_NONE = SsvcDecisionPointValue( +_TD_NONE = DecisionPointValue( name="None", key="N", description="No target systems exist, or targets are so highly specialized that they only exist in a laboratory " diff --git a/src/ssvc/decision_points/cvss/user_interaction.py b/src/ssvc/decision_points/cvss/user_interaction.py index 02e75941..23a22fb5 100644 --- a/src/ssvc/decision_points/cvss/user_interaction.py +++ b/src/ssvc/decision_points/cvss/user_interaction.py @@ -16,18 +16,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_REQUIRED = SsvcDecisionPointValue( +_REQUIRED = DecisionPointValue( name="Required", key="R", description="Successful exploitation of this vulnerability requires a user to take some action before the " "vulnerability can be exploited.", ) -_UI_NONE = SsvcDecisionPointValue( +_UI_NONE = DecisionPointValue( name="None", key="N", description="The vulnerable system can be exploited without interaction from any user.", @@ -49,14 +49,14 @@ Defines None and Required values for CVSS User Interaction. """ -_UI_NONE_2 = SsvcDecisionPointValue( +_UI_NONE_2 = DecisionPointValue( name="None", key="N", description="The vulnerable system can be exploited without interaction from any human user, other than the " "attacker.", ) -_PASSIVE = SsvcDecisionPointValue( +_PASSIVE = DecisionPointValue( name="Passive", key="P", description="Successful exploitation of this vulnerability requires limited interaction by the targeted user with " @@ -64,7 +64,7 @@ "and do not require that the user actively subvert protections built into the vulnerable system.", ) -_ACTIVE = SsvcDecisionPointValue( +_ACTIVE = DecisionPointValue( name="Active", key="A", description="Successful exploitation of this vulnerability requires a targeted user to perform specific, " diff --git a/src/ssvc/decision_points/exploitation.py b/src/ssvc/decision_points/exploitation.py index bb1a2a52..a8d0fc73 100644 --- a/src/ssvc/decision_points/exploitation.py +++ b/src/ssvc/decision_points/exploitation.py @@ -15,17 +15,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -ACTIVE = SsvcDecisionPointValue( +ACTIVE = DecisionPointValue( name="Active", key="A", description="Shared, observable, reliable evidence that the exploit is being" " used in the wild by real attackers; there is credible public reporting.", ) -POC_1 = SsvcDecisionPointValue( +POC_1 = DecisionPointValue( name="PoC", key="P", description="One of the following cases is true: (1) private evidence of exploitation is attested but not shared; " @@ -33,13 +34,13 @@ " or ExploitDB; or (4) the vulnerability has a well-known method of exploitation.", ) -POC_2 = SsvcDecisionPointValue( +POC_2 = DecisionPointValue( name="Public PoC", key="P", description="One of the following is true: (1) Typical public PoC exists in sources such as Metasploit or websites like ExploitDB; or (2) the vulnerability has a well-known method of exploitation.", ) -EXP_NONE = SsvcDecisionPointValue( +EXP_NONE = DecisionPointValue( name="None", key="N", description="There is no evidence of active exploitation and no public proof of concept (PoC) of how to exploit the vulnerability.", diff --git a/src/ssvc/decision_points/helpers.py b/src/ssvc/decision_points/helpers.py index 111ec8e1..15949dee 100644 --- a/src/ssvc/decision_points/helpers.py +++ b/src/ssvc/decision_points/helpers.py @@ -20,10 +20,10 @@ import semver -from ssvc.decision_points import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPoint -def dp_diff(dp1: SsvcDecisionPoint, dp2: SsvcDecisionPoint) -> list[str]: +def dp_diff(dp1: DecisionPoint, dp2: DecisionPoint) -> list[str]: """ Compares two decision points and returns a list of differences. @@ -212,7 +212,7 @@ def dp_diff(dp1: SsvcDecisionPoint, dp2: SsvcDecisionPoint) -> list[str]: return diffs -def show_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: +def show_diffs(versions: Sequence[DecisionPoint]) -> None: if len(versions) < 2: print("Not enough versions to compare") return @@ -223,7 +223,7 @@ def show_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: print() -def print_versions_and_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: +def print_versions_and_diffs(versions: Sequence[DecisionPoint]) -> None: """ Prints the json representation of each version and then shows the diffs between each version. diff --git a/src/ssvc/decision_points/high_value_asset.py b/src/ssvc/decision_points/high_value_asset.py index 35d2c9d4..33b19790 100644 --- a/src/ssvc/decision_points/high_value_asset.py +++ b/src/ssvc/decision_points/high_value_asset.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="System meets a high value asset definition.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="System does not meet a high value asset definition.", diff --git a/src/ssvc/decision_points/human_impact.py b/src/ssvc/decision_points/human_impact.py index ac2deac0..dbdab943 100644 --- a/src/ssvc/decision_points/human_impact.py +++ b/src/ssvc/decision_points/human_impact.py @@ -16,65 +16,66 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -LOW_1 = SsvcDecisionPointValue( +LOW_1 = DecisionPointValue( name="Low", key="L", description="Mission Prevalence:Minimal AND Public Well-Being Impact:Minimal", ) -LOW_2 = SsvcDecisionPointValue( +LOW_2 = DecisionPointValue( name="Low", key="L", description="Safety Impact:(None OR Minor) AND Mission Impact:(None OR Degraded OR Crippled)", ) -LOW_3 = SsvcDecisionPointValue( +LOW_3 = DecisionPointValue( name="Low", key="L", description="Safety Impact:(Negligible) AND Mission Impact:(None OR Degraded OR Crippled)", ) -MEDIUM_1 = SsvcDecisionPointValue( +MEDIUM_1 = DecisionPointValue( name="Medium", key="M", description="Mission Prevalence:Support AND Public Well-Being Impact:(Minimal OR Material)", ) -MEDIUM_2 = SsvcDecisionPointValue( +MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="(Safety Impact:(None OR Minor) AND Mission Impact:MEF Failure) OR (Safety Impact:Major AND Mission Impact:(None OR Degraded OR Crippled))", ) -MEDIUM_3 = SsvcDecisionPointValue( +MEDIUM_3 = DecisionPointValue( name="Medium", key="M", description="(Safety Impact:Negligible AND Mission Impact:MEF Failure) OR (Safety Impact:Marginal AND Mission Impact:(None OR Degraded OR Crippled))", ) -HIGH_1 = SsvcDecisionPointValue( +HIGH_1 = DecisionPointValue( name="High", key="H", description="Mission Prevalence:Essential OR Public Well-Being Impact:(Irreversible)", ) -HIGH_2 = SsvcDecisionPointValue( +HIGH_2 = DecisionPointValue( name="High", key="H", description="(Safety Impact:Hazardous AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Major AND Mission Impact:MEF Failure)", ) -HIGH_3 = SsvcDecisionPointValue( +HIGH_3 = DecisionPointValue( name="High", key="H", description="(Safety Impact:Critical AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Marginal AND Mission Impact:MEF Failure)", ) -VERY_HIGH_1 = SsvcDecisionPointValue( +VERY_HIGH_1 = DecisionPointValue( name="Very High", key="VH", description="Safety Impact:Catastrophic OR Mission Impact:Mission Failure", diff --git a/src/ssvc/decision_points/in_kev.py b/src/ssvc/decision_points/in_kev.py index 31466aaa..66464e1a 100644 --- a/src/ssvc/decision_points/in_kev.py +++ b/src/ssvc/decision_points/in_kev.py @@ -15,16 +15,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="Vulnerability is listed in KEV.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="Vulnerability is not listed in KEV.", diff --git a/src/ssvc/decision_points/mission_impact.py b/src/ssvc/decision_points/mission_impact.py index d0a3a132..62ffe13e 100644 --- a/src/ssvc/decision_points/mission_impact.py +++ b/src/ssvc/decision_points/mission_impact.py @@ -17,40 +17,39 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -MISSION_FAILURE = SsvcDecisionPointValue( +MISSION_FAILURE = DecisionPointValue( name="Mission Failure", key="MF", description="Multiple or all mission essential functions fail; ability to recover those functions degraded; organization’s ability to deliver its overall mission fails", ) -MEF_FAILURE = SsvcDecisionPointValue( +MEF_FAILURE = DecisionPointValue( name="MEF Failure", key="MEF", description="Any one mission essential function fails for period of time longer than acceptable; overall mission of the organization degraded but can still be accomplished for a time", ) -MEF_CRIPPLED = SsvcDecisionPointValue( +MEF_CRIPPLED = DecisionPointValue( name="MEF Support Crippled", key="MSC", description="Activities that directly support essential functions are crippled; essential functions continue for a time", ) -MI_NED = SsvcDecisionPointValue( +MI_NED = DecisionPointValue( name="Non-Essential Degraded", key="NED", description="Degradation of non-essential functions; chronic degradation would eventually harm essential functions", ) -MI_NONE = SsvcDecisionPointValue( - name="None", key="N", description="Little to no impact" -) +MI_NONE = DecisionPointValue(name="None", key="N", description="Little to no impact") # combine MI_NONE and MI_NED into a single value -DEGRADED = SsvcDecisionPointValue( +DEGRADED = DecisionPointValue( name="Degraded", key="D", description="Little to no impact up to degradation of non-essential functions; chronic degradation would eventually harm essential functions", diff --git a/src/ssvc/decision_points/mission_prevalence.py b/src/ssvc/decision_points/mission_prevalence.py index 8bf55920..9a1cfab2 100644 --- a/src/ssvc/decision_points/mission_prevalence.py +++ b/src/ssvc/decision_points/mission_prevalence.py @@ -17,23 +17,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue, SsvcDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -MINIMAL = SsvcDecisionPointValue( +MINIMAL = DecisionPointValue( name="Minimal", key="M", description="Neither Support nor Essential apply. " "The vulnerable component may be used within the entities, but it is not used as a mission-essential component, nor does it provide impactful support to mission-essential functions.", ) -SUPPORT = SsvcDecisionPointValue( +SUPPORT = DecisionPointValue( name="Support", key="S", description="The vulnerable component only supports MEFs for two or more entities.", ) -ESSENTIAL = SsvcDecisionPointValue( +ESSENTIAL = DecisionPointValue( name="Essential", key="E", description="The vulnerable component directly provides capabilities that constitute at least one MEF for at least one entity; component failure may (but does not necessarily) lead to overall mission failure.", diff --git a/src/ssvc/decision_points/public_safety_impact.py b/src/ssvc/decision_points/public_safety_impact.py index 54df0a8e..d2575532 100644 --- a/src/ssvc/decision_points/public_safety_impact.py +++ b/src/ssvc/decision_points/public_safety_impact.py @@ -17,16 +17,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -MINIMAL_1 = SsvcDecisionPointValue( +MINIMAL_1 = DecisionPointValue( name="Minimal", description="The effect is below the threshold for all aspects described in material. ", key="M", ) -MATERIAL = SsvcDecisionPointValue( +MATERIAL = DecisionPointValue( name="Material", description="Any one or more of these conditions hold. " "Physical harm: Does one or more of the following: " @@ -41,7 +42,7 @@ key="M", ) -IRREVERSIBLE = SsvcDecisionPointValue( +IRREVERSIBLE = DecisionPointValue( name="Irreversible", description="Any one or more of these conditions hold. " "Physical harm: One or both of the following are true: (a) Multiple fatalities are likely." @@ -54,23 +55,23 @@ key="I", ) -SIGNIFICANT = SsvcDecisionPointValue( +SIGNIFICANT = DecisionPointValue( name="Significant", description="Safety Impact:(Major OR Hazardous OR Catastrophic)", key="S", ) -MINIMAL_2 = SsvcDecisionPointValue( +MINIMAL_2 = DecisionPointValue( name="Minimal", description="Safety Impact:(None OR Minor)", key="M" ) -SIGNIFICANT_1 = SsvcDecisionPointValue( +SIGNIFICANT_1 = DecisionPointValue( name="Significant", description="Safety Impact:(Marginal OR Critical OR Catastrophic)", key="S", ) -MINIMAL_3 = SsvcDecisionPointValue( +MINIMAL_3 = DecisionPointValue( name="Minimal", description="Safety Impact:Negligible", key="M" ) diff --git a/src/ssvc/decision_points/public_value_added.py b/src/ssvc/decision_points/public_value_added.py index 87b4700a..412c2277 100644 --- a/src/ssvc/decision_points/public_value_added.py +++ b/src/ssvc/decision_points/public_value_added.py @@ -17,22 +17,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -LIMITED = SsvcDecisionPointValue( +LIMITED = DecisionPointValue( name="Limited", key="L", description="Minimal value added to the existing public information because existing information is already high quality and in multiple outlets.", ) -AMPLIATIVE = SsvcDecisionPointValue( +AMPLIATIVE = DecisionPointValue( name="Ampliative", key="A", description="Amplifies and/or augments the existing public information about the vulnerability, for example, adds additional detail, addresses or corrects errors in other public information, draws further attention to the vulnerability, etc.", ) -PRECEDENCE = SsvcDecisionPointValue( +PRECEDENCE = DecisionPointValue( name="Precedence", key="P", description="The publication would be the first publicly available, or be coincident with the first publicly available.", diff --git a/src/ssvc/decision_points/report_credibility.py b/src/ssvc/decision_points/report_credibility.py index 1e4cf105..6aaddc03 100644 --- a/src/ssvc/decision_points/report_credibility.py +++ b/src/ssvc/decision_points/report_credibility.py @@ -17,16 +17,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -NOT_CREDIBLE = SsvcDecisionPointValue( +NOT_CREDIBLE = DecisionPointValue( name="Not Credible", key="NC", description="The report is not credible.", ) -CREDIBLE = SsvcDecisionPointValue( +CREDIBLE = DecisionPointValue( name="Credible", key="C", description="The report is credible.", diff --git a/src/ssvc/decision_points/report_public.py b/src/ssvc/decision_points/report_public.py index a072e185..3c308cb4 100644 --- a/src/ssvc/decision_points/report_public.py +++ b/src/ssvc/decision_points/report_public.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="A public report of the vulnerability exists.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="No public report of the vulnerability exists.", diff --git a/src/ssvc/decision_points/safety_impact.py b/src/ssvc/decision_points/safety_impact.py index 5a5c16ae..72c90e3f 100644 --- a/src/ssvc/decision_points/safety_impact.py +++ b/src/ssvc/decision_points/safety_impact.py @@ -17,10 +17,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -CATASTROPHIC = SsvcDecisionPointValue( +CATASTROPHIC = DecisionPointValue( name="Catastrophic", key="C", description="Any one or more of these conditions hold. " @@ -32,7 +33,7 @@ "Psychological: N/A.", ) -HAZARDOUS = SsvcDecisionPointValue( +HAZARDOUS = DecisionPointValue( name="Hazardous", key="H", description="Any one or more of these conditions hold. " @@ -44,7 +45,7 @@ "Psychological: N/A.", ) -MAJOR = SsvcDecisionPointValue( +MAJOR = DecisionPointValue( name="Major", key="J", description="Any one or more of these conditions hold. " @@ -57,7 +58,7 @@ "Psychological: Widespread emotional or psychological harm, sufficient to be cause for counselling or therapy, to populations of people.", ) -MINOR = SsvcDecisionPointValue( +MINOR = DecisionPointValue( name="Minor", key="M", description="Any one or more of these conditions hold. " @@ -70,7 +71,7 @@ "Psychological: Emotional or psychological harm, sufficient to be cause for counselling or therapy, to multiple persons.", ) -SAF_NONE = SsvcDecisionPointValue( +SAF_NONE = DecisionPointValue( name="None", key="N", description="The effect is below the threshold for all aspects described in Minor.", @@ -79,7 +80,7 @@ ## Based on the IEC 61508 standard ## Catastrophic, Critical, Marginal, Negligible -CATASTROPHIC_2 = SsvcDecisionPointValue( +CATASTROPHIC_2 = DecisionPointValue( name="Catastrophic", key="C", description="Any one or more of these conditions hold.

" @@ -91,7 +92,7 @@ "- *Psychological*: N/A.", ) -CRITICAL = SsvcDecisionPointValue( +CRITICAL = DecisionPointValue( name="Critical", key="R", description="Any one or more of these conditions hold.

" @@ -103,7 +104,7 @@ "- *Psychological*: N/A.", ) -MARGINAL = SsvcDecisionPointValue( +MARGINAL = DecisionPointValue( name="Marginal", key="M", description="Any one or more of these conditions hold.

" @@ -116,7 +117,7 @@ "- *Psychological*: Widespread emotional or psychological harm, sufficient to be cause for counselling or therapy, to populations of people.", ) -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="Any one or more of these conditions hold.

" diff --git a/src/ssvc/decision_points/ssvc_/__init__.py b/src/ssvc/decision_points/ssvc_/__init__.py new file mode 100644 index 00000000..3fa844ad --- /dev/null +++ b/src/ssvc/decision_points/ssvc_/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +This package contains SSVC decision points belonging to the `ssvc` namespace. +""" diff --git a/src/ssvc/decision_points/ssvc_/base.py b/src/ssvc/decision_points/ssvc_/base.py new file mode 100644 index 00000000..e4fcf0f3 --- /dev/null +++ b/src/ssvc/decision_points/ssvc_/base.py @@ -0,0 +1,29 @@ +""" +Provides the base class for all decision points in the `ssvc` namespace. +""" + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from pydantic import BaseModel + +from ssvc.decision_points.base import DecisionPoint +from ssvc.namespaces import NameSpace + + +class SsvcDecisionPoint(DecisionPoint, BaseModel): + """ + Models a single decision point as a list of values. + """ + + namespace: str = NameSpace.SSVC diff --git a/src/ssvc/decision_points/supplier_cardinality.py b/src/ssvc/decision_points/supplier_cardinality.py index 934ebfdf..ba315933 100644 --- a/src/ssvc/decision_points/supplier_cardinality.py +++ b/src/ssvc/decision_points/supplier_cardinality.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -MULTIPLE = SsvcDecisionPointValue( +MULTIPLE = DecisionPointValue( name="Multiple", key="M", description="There are multiple suppliers of the vulnerable component.", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="One", key="O", description="There is only one supplier of the vulnerable component.", diff --git a/src/ssvc/decision_points/supplier_contacted.py b/src/ssvc/decision_points/supplier_contacted.py index f3586008..efe57631 100644 --- a/src/ssvc/decision_points/supplier_contacted.py +++ b/src/ssvc/decision_points/supplier_contacted.py @@ -15,16 +15,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="The supplier has been contacted.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="The supplier has not been contacted.", diff --git a/src/ssvc/decision_points/supplier_engagement.py b/src/ssvc/decision_points/supplier_engagement.py index cb0aef24..163eaec3 100644 --- a/src/ssvc/decision_points/supplier_engagement.py +++ b/src/ssvc/decision_points/supplier_engagement.py @@ -17,16 +17,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -UNRESPONSIVE = SsvcDecisionPointValue( +UNRESPONSIVE = DecisionPointValue( name="Unresponsive", key="U", description="The supplier is not responding to the reporter’s contact effort and not actively participating in the coordination effort.", ) -ACTIVE = SsvcDecisionPointValue( +ACTIVE = DecisionPointValue( name="Active", key="A", description="The supplier is responding to the reporter’s contact effort and actively participating in the coordination effort.", diff --git a/src/ssvc/decision_points/supplier_involvement.py b/src/ssvc/decision_points/supplier_involvement.py index 823afd4d..5fb1f744 100644 --- a/src/ssvc/decision_points/supplier_involvement.py +++ b/src/ssvc/decision_points/supplier_involvement.py @@ -16,22 +16,23 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -UNCOOPERATIVE = SsvcDecisionPointValue( +UNCOOPERATIVE = DecisionPointValue( name="Uncooperative/Unresponsive", key="UU", description="The supplier has not responded, declined to generate a remediation, or no longer exists.", ) -COOPERATIVE = SsvcDecisionPointValue( +COOPERATIVE = DecisionPointValue( name="Cooperative", key="C", description="The supplier is actively generating a patch or fix; they may or may not have provided a mitigation or work-around in the mean time.", ) -FIX_READY = SsvcDecisionPointValue( +FIX_READY = DecisionPointValue( name="Fix Ready", key="FR", description="The supplier has provided a patch or fix.", diff --git a/src/ssvc/decision_points/system_exposure.py b/src/ssvc/decision_points/system_exposure.py index 9f0c813a..e9e913ce 100644 --- a/src/ssvc/decision_points/system_exposure.py +++ b/src/ssvc/decision_points/system_exposure.py @@ -16,17 +16,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -EXP_UNAVOIDABLE = SsvcDecisionPointValue( +EXP_UNAVOIDABLE = DecisionPointValue( name="Unavoidable", key="U", description="Internet or another widely accessible network where access cannot plausibly be restricted or " "controlled (e.g., DNS servers, web servers, VOIP servers, email servers)", ) -EXP_CONTROLLED = SsvcDecisionPointValue( +EXP_CONTROLLED = DecisionPointValue( name="Controlled", key="C", description="Networked service with some access restrictions or mitigations already in place (whether locally or on the network). " @@ -37,7 +38,7 @@ "execute it, then exposure should be small.", ) -EXP_SMALL = SsvcDecisionPointValue( +EXP_SMALL = DecisionPointValue( name="Small", key="S", description="Local service or program; highly controlled network", @@ -57,7 +58,7 @@ ) # EXP_OPEN is just a rename of EXP_UNAVOIDABLE -EXP_OPEN = SsvcDecisionPointValue( +EXP_OPEN = DecisionPointValue( name="Open", key="O", description="Internet or another widely accessible network where access cannot plausibly be restricted or " diff --git a/src/ssvc/decision_points/technical_impact.py b/src/ssvc/decision_points/technical_impact.py index 3fa10eff..87d9a1dc 100644 --- a/src/ssvc/decision_points/technical_impact.py +++ b/src/ssvc/decision_points/technical_impact.py @@ -17,16 +17,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -TOTAL = SsvcDecisionPointValue( +TOTAL = DecisionPointValue( name="Total", key="T", description="The exploit gives the adversary total control over the behavior of the software, or it gives total disclosure of all information on the system that contains the vulnerability.", ) -PARTIAL = SsvcDecisionPointValue( +PARTIAL = DecisionPointValue( name="Partial", key="P", description="The exploit gives the adversary limited control over, or information exposure about, the behavior of the software that contains the vulnerability. Or the exploit gives the adversary an importantly low stochastic opportunity for total control.", diff --git a/src/ssvc/decision_points/utility.py b/src/ssvc/decision_points/utility.py index 10b43924..db3a6264 100644 --- a/src/ssvc/decision_points/utility.py +++ b/src/ssvc/decision_points/utility.py @@ -5,6 +5,7 @@ """ # Copyright (c) 2024-2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project # Stakeholder Specific Vulnerability Categorization (SSVC) is # licensed under a MIT (SEI)-style license, please see LICENSE.md distributed @@ -16,40 +17,41 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -SUPER_EFFECTIVE_2 = SsvcDecisionPointValue( +SUPER_EFFECTIVE_2 = DecisionPointValue( name="Super Effective", key="S", description="Automatable:Yes AND Value Density:Concentrated", ) -EFFICIENT_2 = SsvcDecisionPointValue( +EFFICIENT_2 = DecisionPointValue( name="Efficient", key="E", description="(Automatable:Yes AND Value Density:Diffuse) OR (Automatable:No AND Value Density:Concentrated)", ) -LABORIOUS_2 = SsvcDecisionPointValue( +LABORIOUS_2 = DecisionPointValue( name="Laborious", key="L", description="Automatable:No AND Value Density:Diffuse", ) -SUPER_EFFECTIVE = SsvcDecisionPointValue( +SUPER_EFFECTIVE = DecisionPointValue( name="Super Effective", key="S", description="Virulence:Rapid and Value Density:Concentrated", ) -EFFICIENT = SsvcDecisionPointValue( +EFFICIENT = DecisionPointValue( name="Efficient", key="E", description="Virulence:Rapid and Value Density:Diffuse OR Virulence:Slow and Value Density:Concentrated", ) -LABORIOUS = SsvcDecisionPointValue( +LABORIOUS = DecisionPointValue( name="Laborious", key="L", description="Virulence:Slow and Value Density:Diffuse", diff --git a/src/ssvc/decision_points/value_density.py b/src/ssvc/decision_points/value_density.py index 81b9fd14..2c8b10ad 100644 --- a/src/ssvc/decision_points/value_density.py +++ b/src/ssvc/decision_points/value_density.py @@ -16,16 +16,17 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -CONCENTRATED = SsvcDecisionPointValue( +CONCENTRATED = DecisionPointValue( name="Concentrated", key="C", description="The system that contains the vulnerable component is rich in resources. Heuristically, such systems are often the direct responsibility of “system operators” rather than users.", ) -DIFFUSE = SsvcDecisionPointValue( +DIFFUSE = DecisionPointValue( name="Diffuse", key="D", description="The system that contains the vulnerable component has limited resources. That is, the resources that the adversary will gain control over with a single exploitation event are relatively small.", diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 5073ebb4..838a5f99 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -22,9 +22,9 @@ import pandas as pd from pydantic import BaseModel, model_validator -from ssvc._mixins import _Base, _Commented, _Namespaced, _Versioned +from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned from ssvc.csv_analyzer import check_topological_order -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue, SsvcDecisionPoint from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.base import OutcomeGroup, OutcomeValue from ssvc.policy_generator import PolicyGenerator @@ -41,7 +41,7 @@ def name_to_key(name: str) -> str: return new_name -class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): +class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): """ The DecisionTable class is a model for decisions in SSVC. @@ -54,7 +54,8 @@ class DecisionTable(_Versioned, _Namespaced, _Base, _Commented, BaseModel): decision_point_group: SsvcDecisionPointGroup outcome_group: OutcomeGroup - mapping: list = None + mapping: list[dict[str, str]] = None + _df: pd.DataFrame = None @property @@ -82,7 +83,7 @@ def dp_lookup(self) -> dict[str, SsvcDecisionPoint]: } @property - def dp_value_lookup(self) -> dict[str, dict[str, SsvcDecisionPointValue]]: + def dp_value_lookup(self) -> dict[str, dict[str, DecisionPointValue]]: """ Return a lookup table for decision point values. Returns: @@ -145,7 +146,7 @@ def as_df(self) -> pd.DataFrame: """ Convert the mapping to a pandas DataFrame. """ - return self.generate_df() + raise NotImplementedError # stub for validating mapping def generate_df(self) -> pd.DataFrame: @@ -160,33 +161,29 @@ def generate_df(self) -> pd.DataFrame: return df - def table_to_mapping(self, df: pd.DataFrame) -> list[tuple[str, ...]]: + def table_to_mapping(self, df: pd.DataFrame) -> list[dict[str, str]]: # copy dataframe df = pd.DataFrame(df) + columns = [dp.key for dp in self.decision_point_group.decision_points] + columns.append(self.outcome_group.key) - columns = [name_to_key(col) for col in df.columns] df.columns = columns data = [] - for index, row in df.iterrows(): - row_data = [] + for _, row in df.iterrows(): + row_data = {} + outcome_value = None for column in columns: - value = None - ovalue = None - value_name = name_to_key(row[column]) + value_name = row[column] try: value = self.dp_value_lookup[column][value_name] + row_data[column] = value.key except KeyError: - ovalue = self.outcome_lookup[value_name] - - if value is not None: - row_data.append(value.key) - - if ovalue is None: + outcome_value = self.outcome_lookup[value_name] + if outcome_value is None: raise ValueError("Outcome value not found") - row_data = tuple(row_data) - t = tuple([row_data, ovalue.key]) - data.append(t) + row_data["outcome"] = outcome_value.key + data.append(row_data) return data @@ -202,16 +199,17 @@ def main(): logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) - dfw = DecisionTable( + dt = DecisionTable( name="Example Prioritization Framework", description="The description for an Example Prioritization Framework", + namespace="x_test", version="1.0.0", decision_point_group=dpg, outcome_group=og, ) - print(dfw.model_dump_json(indent=2)) + print(dt.model_dump_json(indent=2)) - print(dfw._df) + print(dt._df) if __name__ == "__main__": diff --git a/src/ssvc/doc_helpers.py b/src/ssvc/doc_helpers.py index 16a48bc8..9035db1a 100644 --- a/src/ssvc/doc_helpers.py +++ b/src/ssvc/doc_helpers.py @@ -17,7 +17,7 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint MD_TABLE_ROW_TEMPLATE = "| {value.name} | {value.description} |" diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index 59c4b2b5..f16323c6 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -33,9 +33,9 @@ import ssvc.dp_groups.cvss.collections # noqa import ssvc.dp_groups.ssvc.collections # noqa +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import ( REGISTERED_DECISION_POINTS, - SsvcDecisionPoint, ) logger = logging.getLogger(__name__) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index a2546635..e43bd063 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -22,16 +22,19 @@ from pydantic import BaseModel -from ssvc._mixins import _Base, _Versioned -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc._mixins import _Base, _SchemaVersioned +from ssvc.decision_points.base import ( + DecisionPoint, ValueSummary, +) +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -class SsvcDecisionPointGroup(_Base, _Versioned, BaseModel): +class SsvcDecisionPointGroup(_Base, _SchemaVersioned, BaseModel): """ Models a group of decision points. """ - decision_points: tuple[SsvcDecisionPoint, ...] + decision_points: tuple[DecisionPoint, ...] def __iter__(self): """ @@ -47,29 +50,27 @@ def __len__(self): l = len(dplist) return l - def combinations( - self, - ) -> Generator[tuple[SsvcDecisionPointValue, ...], None, None]: - # Generator[yield_type, send_type, return_type] - """ - Produce all possible combinations of decision point values in the group. - """ - # for each decision point, get the values - # then take the product of all the values - # and yield each combination - values_list: list[list[SsvcDecisionPointValue]] = [ - dp.values for dp in self.decision_points - ] - for combination in product(*values_list): + def combination_summaries(self) -> Generator[tuple[ValueSummary, ...], None, None]: + # get the value summaries for each decision point + value_summaries = [dp.value_summaries for dp in self.decision_points] + + for combination in product(*value_summaries): yield combination - def combo_strings(self) -> Generator[tuple[str, ...], None, None]: + def combination_strings(self) -> Generator[tuple[str, ...], None, None]: """ Produce all possible combinations of decision point values in the group as strings. """ - for combo in self.combinations(): + for combo in self.combination_summaries(): yield tuple(str(v) for v in combo) + def combination_dicts(self): + """ + Produce all possible combinations of decision point values in the group as a dictionary. + """ + for combo in self.combination_summaries(): + yield tuple(v.model_dump() for v in combo) + def get_all_decision_points_from( *groups: list[SsvcDecisionPointGroup], diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index f4316359..fabc5aca 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -17,7 +17,7 @@ from pydantic import BaseModel -from ssvc._mixins import _Base, _Keyed, _Valued, _Versioned +from ssvc._mixins import _Base, _Keyed, _SchemaVersioned, _Valued class OutcomeValue(_Base, _Keyed, BaseModel): @@ -26,7 +26,7 @@ class OutcomeValue(_Base, _Keyed, BaseModel): """ -class OutcomeGroup(_Valued, _Base, _Keyed, _Versioned, BaseModel): +class OutcomeGroup(_Valued, _Base, _Keyed, _SchemaVersioned, BaseModel): """ Models an outcome group. """ diff --git a/src/test/test_cvss_helpers.py b/src/test/test_cvss_helpers.py index 3101efff..4c15d04f 100644 --- a/src/test/test_cvss_helpers.py +++ b/src/test/test_cvss_helpers.py @@ -14,7 +14,7 @@ import unittest import ssvc.decision_points.cvss.helpers as h -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint @@ -27,17 +27,17 @@ def fake_ms_impacts() -> list[CvssDecisionPoint]: version="1.0.0", key=key, values=( - SsvcDecisionPointValue( + DecisionPointValue( name="None", key="N", description="No impact", ), - SsvcDecisionPointValue( + DecisionPointValue( name="Low", key="L", description="Low impact", ), - SsvcDecisionPointValue( + DecisionPointValue( name="High", key="H", description="High impact", @@ -58,12 +58,12 @@ def setUp(self) -> None: version="1.0", key=f"TDP{i}", values=( - SsvcDecisionPointValue( + DecisionPointValue( name=f"yes_{i}", description=f"yes_{i}", key=f"Y{i}", ), - SsvcDecisionPointValue( + DecisionPointValue( name=f"no_{i}", description=f"no_{i}", key=f"N{i}", diff --git a/src/test/test_decision_table.py b/src/test/test_decision_table.py index f22eb4a1..841be8eb 100644 --- a/src/test/test_decision_table.py +++ b/src/test/test_decision_table.py @@ -16,6 +16,7 @@ import pandas as pd +import ssvc.decision_points.ssvc_.base from ssvc.decision_tables import base from ssvc.decision_tables.base import name_to_key from ssvc.dp_groups.base import SsvcDecisionPointGroup @@ -31,14 +32,14 @@ def setUp(self): for i in range(3): dpvs = [] for j in range(3): - dpv = base.SsvcDecisionPointValue( + dpv = base.DecisionPointValue( name=f"Value {i}{j}", key=f"DP{i}V{j}", description=f"Decision Point {i} Value {j} Description", ) dpvs.append(dpv) - dp = base.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name=f"Decision Point {i}", key=f"DP{i}", description=f"Decision Point {i} Description", @@ -138,7 +139,7 @@ def test_validate_mapping(self): with self.subTest("problems"): # set one of the outcomes out of order - self.dt._df.iloc[0, -1] = "ov2" + self.dt._df.iloc[0, -1] = self.og.values[-1].name with self.assertRaises(ValueError): self.dt.validate_mapping() diff --git a/src/test/test_doc_helpers.py b/src/test/test_doc_helpers.py index fbbb7f45..380244af 100644 --- a/src/test/test_doc_helpers.py +++ b/src/test/test_doc_helpers.py @@ -13,21 +13,21 @@ import unittest -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.doc_helpers import example_block, markdown_table class MyTestCase(unittest.TestCase): def setUp(self): - self.dp = SsvcDecisionPoint( + self.dp = DecisionPoint( namespace="x_test", name="test name", description="test description", key="TK", version="1.0.0", values=( - SsvcDecisionPointValue(name="A", key="A", description="A Definition"), - SsvcDecisionPointValue(name="B", key="B", description="B Definition"), + DecisionPointValue(name="A", key="A", description="A Definition"), + DecisionPointValue(name="B", key="B", description="B Definition"), ), ) diff --git a/src/test/test_doctools.py b/src/test/test_doctools.py index 70fba2f9..33e2924b 100644 --- a/src/test/test_doctools.py +++ b/src/test/test_doctools.py @@ -16,7 +16,6 @@ import tempfile import unittest -from ssvc.decision_points import SsvcDecisionPoint from ssvc.doctools import ( EnsureDirExists, _filename_friendly, diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index 09412802..79da6013 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -14,6 +14,7 @@ import unittest import ssvc.decision_points.base as base +import ssvc.decision_points.ssvc_.base class MyTestCase(unittest.TestCase): @@ -24,12 +25,12 @@ def setUp(self) -> None: self.values = [] for i in range(3): self.values.append( - base.SsvcDecisionPointValue( + base.DecisionPointValue( name=f"foo{i}", key=f"bar{i}", description=f"baz{i}" ) ) - self.dp = base.SsvcDecisionPoint( + self.dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="foo", key="bar", description="baz", @@ -54,7 +55,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -67,7 +68,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -101,30 +102,13 @@ def test_ssvc_decision_point(self): self.assertEqual(obj.namespace, "x_test") self.assertEqual(len(self.values), len(obj.values)) - def test_ssvc_decision_point_value_dict(self): - obj = self.dp - # should have values_dict - self.assertTrue(hasattr(obj, "value_dict")) - self.assertEqual(len(obj.value_dict), len(self.values)) - # keys of value dict should be namespace:key:value.key - for value in self.values: - key = f"{obj.namespace}:{obj.key}:{value.key}" - self.assertIn(key, obj.value_dict) - self.assertEqual(obj.value_dict[key], value) - - # values_dict should NOT appear in serialization - # not in the data structure - self.assertNotIn("value_dict", obj.model_dump()) - # not in the json - self.assertNotIn("value_dict", obj.model_dump_json()) - def test_ssvc_value_json_roundtrip(self): for i, obj in enumerate(self.values): json = obj.model_dump_json() self.assertIsInstance(json, str) self.assertGreater(len(json), 0) - obj2 = base.SsvcDecisionPointValue.model_validate_json(json) + obj2 = base.DecisionPointValue.model_validate_json(json) self.assertEqual(obj, obj2) def test_ssvc_decision_point_json_roundtrip(self): @@ -134,7 +118,9 @@ def test_ssvc_decision_point_json_roundtrip(self): self.assertIsInstance(json, str) self.assertGreater(len(json), 0) - obj2 = base.SsvcDecisionPoint.model_validate_json(json) + obj2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint.model_validate_json( + json + ) # the objects should be equal self.assertEqual(obj, obj2) diff --git a/src/test/test_dp_groups.py b/src/test/test_dp_groups.py index 46fec87f..a02ef25e 100644 --- a/src/test/test_dp_groups.py +++ b/src/test/test_dp_groups.py @@ -13,23 +13,24 @@ import unittest +import ssvc.decision_points.ssvc_.base import ssvc.dp_groups.base as dpg -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue class MyTestCase(unittest.TestCase): def setUp(self) -> None: self.dps = [] for i in range(10): - dp = dpg.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name=f"Decision Point {i}", key=f"DP_{i}", description=f"Description of Decision Point {i}", version="1.0.0", values=( - SsvcDecisionPointValue(name="foo", key="FOO", description="foo"), - SsvcDecisionPointValue(name="bar", key="BAR", description="bar"), - SsvcDecisionPointValue(name="baz", key="BAZ", description="baz"), + DecisionPointValue(name="foo", key="FOO", description="foo"), + DecisionPointValue(name="bar", key="BAR", description="bar"), + DecisionPointValue(name="baz", key="BAZ", description="baz"), ), ) self.dps.append(dp) @@ -63,43 +64,6 @@ def test_len(self): self.assertEqual(len(self.dps), len(list(g.decision_points))) self.assertEqual(len(self.dps), len(g)) - def test_combinations(self): - # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( - name="Test Group", - description="Test Group", - decision_points=self.dps, - ) - - # get all the combinations - combos = list(g.combinations()) - - # assert that the number of combinations is the product of the number of values - # for each decision point - n_combos = 1 - for dp in self.dps: - n_combos *= len(dp.values) - self.assertEqual(n_combos, len(combos)) - - # assert that each combination is a tuple - for combo in combos: - self.assertIsInstance(combo, tuple) - # assert that each value in the combination is a decision point value - for value in combo: - self.assertIsInstance(value, SsvcDecisionPointValue) - - # foo, bar, and baz should be in each combination to some degree - foo_count = sum(1 for v in combo if v.name == "foo") - bar_count = sum(1 for v in combo if v.name == "bar") - baz_count = sum(1 for v in combo if v.name == "baz") - for count in (foo_count, bar_count, baz_count): - # each count should be greater than or equal to 0 - self.assertGreaterEqual(count, 0) - # the total count of foo, bar, and baz should be the same as the length of the combination - # indicating that no other values are present - total = sum((foo_count, bar_count, baz_count)) - self.assertEqual(len(combo), total) - def test_combo_strings(self): # add them to a decision point group g = dpg.SsvcDecisionPointGroup( @@ -109,7 +73,7 @@ def test_combo_strings(self): ) # get all the combinations - combos = list(g.combo_strings()) + combos = list(g.combination_strings()) # assert that the number of combinations is the product of the number of values # for each decision point @@ -126,12 +90,14 @@ def test_combo_strings(self): for value in combo: self.assertIsInstance(value, str) # foo, bar, and baz should be in each combination to some degree - foo_count = sum(1 for v in combo if v == "foo") - bar_count = sum(1 for v in combo if v == "bar") - baz_count = sum(1 for v in combo if v == "baz") + foo_count = sum(1 for v in combo if v.endswith("FOO")) + bar_count = sum(1 for v in combo if v.endswith("BAR")) + baz_count = sum(1 for v in combo if v.endswith("BAZ")) for count in (foo_count, bar_count, baz_count): # each count should be greater than or equal to 0 self.assertGreaterEqual(count, 0) + # each count should be less than or equal to the length of the combination + self.assertLessEqual(count, len(combo)) # the total count of foo, bar, and baz should be the same as the length of the combination # indicating that no other values are present total = sum((foo_count, bar_count, baz_count)) diff --git a/src/test/test_dp_helpers.py b/src/test/test_dp_helpers.py index 3502419c..d62811f2 100644 --- a/src/test/test_dp_helpers.py +++ b/src/test/test_dp_helpers.py @@ -14,24 +14,24 @@ import unittest from copy import deepcopy -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_points.helpers import dp_diff class MyTestCase(unittest.TestCase): def setUp(self) -> None: - self.dp1 = SsvcDecisionPoint( + self.dp1 = DecisionPoint( name="Test DP", key="test_dp", description="This is a test decision point", version="1.0.0", values=[ - SsvcDecisionPointValue( + DecisionPointValue( name="Yes", key="yes", description="Yes", ), - SsvcDecisionPointValue( + DecisionPointValue( name="No", key="no", description="No", @@ -89,7 +89,7 @@ def test_major_version(self): # add one self.dp2.values = list(self.dp1.values) self.dp2.values.append( - SsvcDecisionPointValue( + DecisionPointValue( name="Maybe", key="maybe", description="Maybe", @@ -107,7 +107,7 @@ def test_minor_version_when_new_option_added(self): # add one self.dp2.values = list(self.dp1.values) self.dp2.values.append( - SsvcDecisionPointValue( + DecisionPointValue( name="Maybe", key="maybe", description="Maybe", diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 12233c91..2be16f7b 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -18,7 +18,7 @@ import networkx as nx import pandas as pd -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.base import OutcomeGroup, OutcomeValue from ssvc.policy_generator import PolicyGenerator @@ -40,12 +40,12 @@ def setUp(self) -> None: name="test", description="test", decision_points=[ - SsvcDecisionPoint( + DecisionPoint( name=c, description=c, key=c, values=[ - SsvcDecisionPointValue(name=v, key=v, description=v) + DecisionPointValue(name=v, key=v, description=v) for v in self.dp_values ], ) From 12df1bc35ddd9443e793eff7997043e3ef5bce2b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Mar 2025 16:25:54 -0400 Subject: [PATCH 55/99] move ssvc decision points into namespace-based directory --- docs/howto/coordination_triage_decision.md | 14 +- docs/howto/deployer_tree.md | 10 +- docs/howto/publication_decision.md | 6 +- docs/howto/supplier_tree.md | 8 +- docs/reference/decision_points/automatable.md | 4 +- .../reference/decision_points/exploitation.md | 4 +- .../reference/decision_points/human_impact.md | 4 +- .../decision_points/mission_impact.md | 4 +- .../decision_points/public_safety_impact.md | 4 +- .../decision_points/public_value_added.md | 2 +- .../decision_points/report_credibility.md | 2 +- .../decision_points/report_public.md | 2 +- .../decision_points/safety_impact.md | 4 +- .../decision_points/supplier_cardinality.md | 2 +- .../decision_points/supplier_contacted.md | 2 +- .../decision_points/supplier_engagement.md | 2 +- .../decision_points/supplier_involvement.md | 2 +- .../decision_points/system_exposure.md | 4 +- .../decision_points/technical_impact.md | 2 +- docs/reference/decision_points/utility.md | 4 +- .../decision_points/value_density.md | 2 +- src/exploratory.ipynb | 165 ++++++++++++++++++ .../{ => ssvc_}/automatable.py | 0 .../{ => ssvc_}/critical_software.py | 0 .../{ => ssvc_}/exploitation.py | 0 .../{ => ssvc_}/high_value_asset.py | 0 .../{ => ssvc_}/human_impact.py | 0 .../decision_points/{ => ssvc_}/in_kev.py | 0 .../{ => ssvc_}/mission_impact.py | 0 .../{ => ssvc_}/mission_prevalence.py | 0 .../{ => ssvc_}/public_safety_impact.py | 0 .../{ => ssvc_}/public_value_added.py | 0 .../{ => ssvc_}/report_credibility.py | 0 .../{ => ssvc_}/report_public.py | 0 .../{ => ssvc_}/safety_impact.py | 0 .../{ => ssvc_}/supplier_cardinality.py | 0 .../{ => ssvc_}/supplier_contacted.py | 0 .../{ => ssvc_}/supplier_engagement.py | 0 .../{ => ssvc_}/supplier_involvement.py | 0 .../{ => ssvc_}/system_exposure.py | 0 .../{ => ssvc_}/technical_impact.py | 0 .../decision_points/{ => ssvc_}/utility.py | 0 .../{ => ssvc_}/value_density.py | 0 .../dp_groups/ssvc/coordinator_publication.py | 6 +- src/ssvc/dp_groups/ssvc/coordinator_triage.py | 21 ++- src/ssvc/dp_groups/ssvc/deployer.py | 16 +- src/ssvc/dp_groups/ssvc/supplier.py | 12 +- src/ssvc/policy_generator.py | 8 +- src/test/test_prioritization_framework.py | 6 +- src/test/test_schema.py | 8 +- 50 files changed, 246 insertions(+), 84 deletions(-) create mode 100644 src/exploratory.ipynb rename src/ssvc/decision_points/{ => ssvc_}/automatable.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/critical_software.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/exploitation.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/high_value_asset.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/human_impact.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/in_kev.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/mission_impact.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/mission_prevalence.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/public_safety_impact.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/public_value_added.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/report_credibility.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/report_public.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/safety_impact.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/supplier_cardinality.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/supplier_contacted.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/supplier_engagement.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/supplier_involvement.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/system_exposure.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/technical_impact.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/utility.py (100%) rename src/ssvc/decision_points/{ => ssvc_}/value_density.py (100%) diff --git a/docs/howto/coordination_triage_decision.md b/docs/howto/coordination_triage_decision.md index 6ad72cdc..240572d3 100644 --- a/docs/howto/coordination_triage_decision.md +++ b/docs/howto/coordination_triage_decision.md @@ -82,13 +82,13 @@ The remaining five decision points are: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.report_public import LATEST as RP -from ssvc.decision_points.supplier_contacted import LATEST as SC -from ssvc.decision_points.report_credibility import LATEST as RC -from ssvc.decision_points.supplier_cardinality import LATEST as SI -from ssvc.decision_points.supplier_engagement import LATEST as SE -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc_.report_public import LATEST as RP +from ssvc.decision_points.ssvc_.supplier_contacted import LATEST as SC +from ssvc.decision_points.ssvc_.report_credibility import LATEST as RC +from ssvc.decision_points.ssvc_.supplier_cardinality import LATEST as SI +from ssvc.decision_points.ssvc_.supplier_engagement import LATEST as SE +from ssvc.decision_points.ssvc_.utility import LATEST as U +from ssvc.decision_points.ssvc_.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block for dp in [RP, SC, RC, SI, SE, U, PSI]: diff --git a/docs/howto/deployer_tree.md b/docs/howto/deployer_tree.md index 961a475e..637b3888 100644 --- a/docs/howto/deployer_tree.md +++ b/docs/howto/deployer_tree.md @@ -113,14 +113,14 @@ The Deployer Patch Deployment Priority decision model uses the following decisio More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.system_exposure import LATEST as SE -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.human_impact import LATEST as HI +from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc_.system_exposure import LATEST as SE +from ssvc.decision_points.ssvc_.utility import LATEST as U +from ssvc.decision_points.ssvc_.human_impact import LATEST as HI from ssvc.doc_helpers import example_block for dp in [EXP, SE, U, HI]: - print(example_block(dp)) + print(example_block(dp)) ``` In the *Human Impact* table above, *MEF* stands for Mission Essential Function. diff --git a/docs/howto/publication_decision.md b/docs/howto/publication_decision.md index 1673302a..e8090709 100644 --- a/docs/howto/publication_decision.md +++ b/docs/howto/publication_decision.md @@ -133,9 +133,9 @@ and adds two new ones ([*Supplier Involvement*](../reference/decision_points/sup More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_involvement import LATEST as SI -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.public_value_added import LATEST as PVA +from ssvc.decision_points.ssvc_.supplier_involvement import LATEST as SI +from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc_.public_value_added import LATEST as PVA from ssvc.doc_helpers import example_block diff --git a/docs/howto/supplier_tree.md b/docs/howto/supplier_tree.md index e714e907..aa12d0e6 100644 --- a/docs/howto/supplier_tree.md +++ b/docs/howto/supplier_tree.md @@ -72,10 +72,10 @@ The decision to create a patch is based on the following decision points: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.technical_impact import LATEST as TI -from ssvc.decision_points.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc_.utility import LATEST as U +from ssvc.decision_points.ssvc_.technical_impact import LATEST as TI +from ssvc.decision_points.ssvc_.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block diff --git a/docs/reference/decision_points/automatable.md b/docs/reference/decision_points/automatable.md index 69259cfa..44ef71b1 100644 --- a/docs/reference/decision_points/automatable.md +++ b/docs/reference/decision_points/automatable.md @@ -1,7 +1,7 @@ # Automatable (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.automatable import LATEST +from ssvc.decision_points.ssvc_.automatable import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -62,7 +62,7 @@ Due to vulnerability chaining, there is some nuance as to whether reconnaissance ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.automatable import VERSIONS +from ssvc.decision_points.ssvc_.automatable import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/exploitation.md b/docs/reference/decision_points/exploitation.md index b4f93bb3..44fa49cb 100644 --- a/docs/reference/decision_points/exploitation.md +++ b/docs/reference/decision_points/exploitation.md @@ -1,7 +1,7 @@ # Exploitation ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST +from ssvc.decision_points.ssvc_.exploitation import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -49,7 +49,7 @@ The table below lists CWE-IDs that could be used to mark a vulnerability as *PoC ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import VERSIONS +from ssvc.decision_points.ssvc_.exploitation import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/human_impact.md b/docs/reference/decision_points/human_impact.md index 04057d11..dc802e9d 100644 --- a/docs/reference/decision_points/human_impact.md +++ b/docs/reference/decision_points/human_impact.md @@ -1,7 +1,7 @@ # Human Impact ```python exec="true" idprefix="" -from ssvc.decision_points.human_impact import LATEST +from ssvc.decision_points.ssvc_.human_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -47,7 +47,7 @@ see [Guidance on Communicating Results](../../howto/bootstrap/use.md). ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.human_impact import VERSIONS +from ssvc.decision_points.ssvc_.human_impact import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/mission_impact.md b/docs/reference/decision_points/mission_impact.md index f8b80503..617a0327 100644 --- a/docs/reference/decision_points/mission_impact.md +++ b/docs/reference/decision_points/mission_impact.md @@ -1,7 +1,7 @@ # Mission Impact ```python exec="true" idprefix="" -from ssvc.decision_points.mission_impact import LATEST +from ssvc.decision_points.ssvc_.mission_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -42,7 +42,7 @@ It should require the vulnerability management team to interact with more senior ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.mission_impact import VERSIONS +from ssvc.decision_points.ssvc_.mission_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_safety_impact.md b/docs/reference/decision_points/public_safety_impact.md index 44b774b0..4fc7df48 100644 --- a/docs/reference/decision_points/public_safety_impact.md +++ b/docs/reference/decision_points/public_safety_impact.md @@ -1,7 +1,7 @@ # Public Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.public_safety_impact import LATEST +from ssvc.decision_points.ssvc_.public_safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -21,7 +21,7 @@ Therefore we simplify the above into a binary categorization: ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.public_safety_impact import VERSIONS +from ssvc.decision_points.ssvc_.public_safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_value_added.md b/docs/reference/decision_points/public_value_added.md index 0284c0da..4a085f25 100644 --- a/docs/reference/decision_points/public_value_added.md +++ b/docs/reference/decision_points/public_value_added.md @@ -1,7 +1,7 @@ # Public Value Added ```python exec="true" idprefix="" -from ssvc.decision_points.public_value_added import LATEST +from ssvc.decision_points.ssvc_.public_value_added import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_credibility.md b/docs/reference/decision_points/report_credibility.md index acce744c..fc648de8 100644 --- a/docs/reference/decision_points/report_credibility.md +++ b/docs/reference/decision_points/report_credibility.md @@ -1,7 +1,7 @@ # Report Credibility ```python exec="true" idprefix="" -from ssvc.decision_points.report_credibility import LATEST +from ssvc.decision_points.ssvc_.report_credibility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_public.md b/docs/reference/decision_points/report_public.md index aa795f2e..ea60fda9 100644 --- a/docs/reference/decision_points/report_public.md +++ b/docs/reference/decision_points/report_public.md @@ -1,7 +1,7 @@ # Report Public ```python exec="true" idprefix="" -from ssvc.decision_points.report_public import LATEST +from ssvc.decision_points.ssvc_.report_public import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/safety_impact.md b/docs/reference/decision_points/safety_impact.md index 2c9418c4..18f25e7d 100644 --- a/docs/reference/decision_points/safety_impact.md +++ b/docs/reference/decision_points/safety_impact.md @@ -1,7 +1,7 @@ # Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.safety_impact import LATEST +from ssvc.decision_points.ssvc_.safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -217,7 +217,7 @@ We defer this topic for now because we combine it with [*Mission Impact*](missio ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.safety_impact import VERSIONS +from ssvc.decision_points.ssvc_.safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/supplier_cardinality.md b/docs/reference/decision_points/supplier_cardinality.md index ccd088fa..332633ab 100644 --- a/docs/reference/decision_points/supplier_cardinality.md +++ b/docs/reference/decision_points/supplier_cardinality.md @@ -1,7 +1,7 @@ # Supplier Cardinality ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_cardinality import LATEST +from ssvc.decision_points.ssvc_.supplier_cardinality import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_contacted.md b/docs/reference/decision_points/supplier_contacted.md index f75e1615..5a863768 100644 --- a/docs/reference/decision_points/supplier_contacted.md +++ b/docs/reference/decision_points/supplier_contacted.md @@ -1,7 +1,7 @@ # Supplier Contacted ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_contacted import LATEST +from ssvc.decision_points.ssvc_.supplier_contacted import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_engagement.md b/docs/reference/decision_points/supplier_engagement.md index c8a7426b..f97a917f 100644 --- a/docs/reference/decision_points/supplier_engagement.md +++ b/docs/reference/decision_points/supplier_engagement.md @@ -1,7 +1,7 @@ # Supplier Engagement ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_engagement import LATEST +from ssvc.decision_points.ssvc_.supplier_engagement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_involvement.md b/docs/reference/decision_points/supplier_involvement.md index d4fb9d70..bfc45eab 100644 --- a/docs/reference/decision_points/supplier_involvement.md +++ b/docs/reference/decision_points/supplier_involvement.md @@ -1,7 +1,7 @@ # Supplier Involvement ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_involvement import LATEST +from ssvc.decision_points.ssvc_.supplier_involvement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/system_exposure.md b/docs/reference/decision_points/system_exposure.md index 9a2f52dd..a50c131c 100644 --- a/docs/reference/decision_points/system_exposure.md +++ b/docs/reference/decision_points/system_exposure.md @@ -1,7 +1,7 @@ # System Exposure ```python exec="true" idprefix="" -from ssvc.decision_points.system_exposure import LATEST +from ssvc.decision_points.ssvc_.system_exposure import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -44,7 +44,7 @@ If you have suggestions for further heuristics, or potential counterexamples to ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.system_exposure import VERSIONS +from ssvc.decision_points.ssvc_.system_exposure import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/technical_impact.md b/docs/reference/decision_points/technical_impact.md index 4b1dcaf6..6bc02a33 100644 --- a/docs/reference/decision_points/technical_impact.md +++ b/docs/reference/decision_points/technical_impact.md @@ -1,7 +1,7 @@ # Technical Impact ```python exec="true" idprefix="" -from ssvc.decision_points.technical_impact import LATEST +from ssvc.decision_points.ssvc_.technical_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/utility.md b/docs/reference/decision_points/utility.md index 1c465d41..90ac80ca 100644 --- a/docs/reference/decision_points/utility.md +++ b/docs/reference/decision_points/utility.md @@ -1,7 +1,7 @@ # Utility ```python exec="true" idprefix="" -from ssvc.decision_points.utility import LATEST +from ssvc.decision_points.ssvc_.utility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -46,7 +46,7 @@ However, future work should look for and prevent large mismatches between the ou ## Previous Versions ```python exec="true" idprefix="" -from ssvc.decision_points.utility import VERSIONS +from ssvc.decision_points.ssvc_.utility import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/value_density.md b/docs/reference/decision_points/value_density.md index 11b02a3b..2cc7bb8c 100644 --- a/docs/reference/decision_points/value_density.md +++ b/docs/reference/decision_points/value_density.md @@ -1,7 +1,7 @@ # Value Density (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.value_density import LATEST +from ssvc.decision_points.ssvc_.value_density import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/src/exploratory.ipynb b/src/exploratory.ipynb new file mode 100644 index 00000000..b0099b55 --- /dev/null +++ b/src/exploratory.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-21T13:31:21.532792Z", + "start_time": "2025-03-21T13:31:21.520299Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Duplicate decision point (, 'Report Public', 'RP', '1.0.0')\n", + "Duplicate decision point (, 'Supplier Contacted', 'SC', '1.0.0')\n", + "Duplicate decision point (, 'Report Credibility', 'RC', '1.0.0')\n", + "Duplicate decision point (, 'Supplier Cardinality', 'SC', '1.0.0')\n", + "Duplicate decision point (, 'Supplier Engagement', 'SE', '1.0.0')\n", + "Duplicate decision point (, 'Utility', 'U', '1.0.1')\n", + "Duplicate decision point (, 'Automatable', 'A', '2.0.0')\n", + "Duplicate decision point (, 'Value Density', 'VD', '1.0.0')\n", + "Duplicate decision point (, 'Public Safety Impact', 'PSI', '2.0.0')\n", + "Duplicate decision point (, 'Safety Impact', 'SI', '1.0.0')\n" + ] + } + ], + "source": [ + "from ssvc.dp_groups.ssvc.coordinator_triage import COORDINATOR_TRIAGE_1 as dpg" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "72c7764cb46593cc", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-21T13:33:19.287836Z", + "start_time": "2025-03-21T13:33:19.284542Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='N'))\n", + "1 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='M'))\n", + "2 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='J'))\n", + "3 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='H'))\n", + "4 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='C'))\n", + "5 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='S'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='N'))\n", + "6 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='S'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='M'))\n" + ] + } + ], + "source": [ + "for i,c in enumerate(dpg.combination_summaries()):\n", + " print(i,c)\n", + " if i>5:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f04f44540b0c9c2b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:N')\n", + "1 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:M')\n", + "2 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:J')\n", + "3 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:H')\n", + "4 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:C')\n", + "5 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:S', 'ssvc:SI:1.0.0:N')\n", + "6 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:S', 'ssvc:SI:1.0.0:M')\n" + ] + } + ], + "source": [ + "for i,s in enumerate(dpg.combination_strings()):\n", + " print(i,s)\n", + " if i > 5:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "635de87b-52a2-4c5e-9a90-5581448cb28e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'N'})\n", + "\n", + "\n", + "1 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'M'})\n", + "\n", + "\n", + "2 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'J'})\n", + "\n", + "\n", + "3 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'H'})\n", + "\n", + "\n", + "4 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'C'})\n", + "\n", + "\n", + "5 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'S'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'N'})\n", + "\n", + "\n" + ] + } + ], + "source": [ + "for i,t in enumerate(dpg.combination_dicts()):\n", + " if i>5:\n", + " break\n", + " print(i,t)\n", + " print()\n", + " print()\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "129afda1-f243-4b09-a0e3-8d9a8c8a9baa", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/ssvc/decision_points/automatable.py b/src/ssvc/decision_points/ssvc_/automatable.py similarity index 100% rename from src/ssvc/decision_points/automatable.py rename to src/ssvc/decision_points/ssvc_/automatable.py diff --git a/src/ssvc/decision_points/critical_software.py b/src/ssvc/decision_points/ssvc_/critical_software.py similarity index 100% rename from src/ssvc/decision_points/critical_software.py rename to src/ssvc/decision_points/ssvc_/critical_software.py diff --git a/src/ssvc/decision_points/exploitation.py b/src/ssvc/decision_points/ssvc_/exploitation.py similarity index 100% rename from src/ssvc/decision_points/exploitation.py rename to src/ssvc/decision_points/ssvc_/exploitation.py diff --git a/src/ssvc/decision_points/high_value_asset.py b/src/ssvc/decision_points/ssvc_/high_value_asset.py similarity index 100% rename from src/ssvc/decision_points/high_value_asset.py rename to src/ssvc/decision_points/ssvc_/high_value_asset.py diff --git a/src/ssvc/decision_points/human_impact.py b/src/ssvc/decision_points/ssvc_/human_impact.py similarity index 100% rename from src/ssvc/decision_points/human_impact.py rename to src/ssvc/decision_points/ssvc_/human_impact.py diff --git a/src/ssvc/decision_points/in_kev.py b/src/ssvc/decision_points/ssvc_/in_kev.py similarity index 100% rename from src/ssvc/decision_points/in_kev.py rename to src/ssvc/decision_points/ssvc_/in_kev.py diff --git a/src/ssvc/decision_points/mission_impact.py b/src/ssvc/decision_points/ssvc_/mission_impact.py similarity index 100% rename from src/ssvc/decision_points/mission_impact.py rename to src/ssvc/decision_points/ssvc_/mission_impact.py diff --git a/src/ssvc/decision_points/mission_prevalence.py b/src/ssvc/decision_points/ssvc_/mission_prevalence.py similarity index 100% rename from src/ssvc/decision_points/mission_prevalence.py rename to src/ssvc/decision_points/ssvc_/mission_prevalence.py diff --git a/src/ssvc/decision_points/public_safety_impact.py b/src/ssvc/decision_points/ssvc_/public_safety_impact.py similarity index 100% rename from src/ssvc/decision_points/public_safety_impact.py rename to src/ssvc/decision_points/ssvc_/public_safety_impact.py diff --git a/src/ssvc/decision_points/public_value_added.py b/src/ssvc/decision_points/ssvc_/public_value_added.py similarity index 100% rename from src/ssvc/decision_points/public_value_added.py rename to src/ssvc/decision_points/ssvc_/public_value_added.py diff --git a/src/ssvc/decision_points/report_credibility.py b/src/ssvc/decision_points/ssvc_/report_credibility.py similarity index 100% rename from src/ssvc/decision_points/report_credibility.py rename to src/ssvc/decision_points/ssvc_/report_credibility.py diff --git a/src/ssvc/decision_points/report_public.py b/src/ssvc/decision_points/ssvc_/report_public.py similarity index 100% rename from src/ssvc/decision_points/report_public.py rename to src/ssvc/decision_points/ssvc_/report_public.py diff --git a/src/ssvc/decision_points/safety_impact.py b/src/ssvc/decision_points/ssvc_/safety_impact.py similarity index 100% rename from src/ssvc/decision_points/safety_impact.py rename to src/ssvc/decision_points/ssvc_/safety_impact.py diff --git a/src/ssvc/decision_points/supplier_cardinality.py b/src/ssvc/decision_points/ssvc_/supplier_cardinality.py similarity index 100% rename from src/ssvc/decision_points/supplier_cardinality.py rename to src/ssvc/decision_points/ssvc_/supplier_cardinality.py diff --git a/src/ssvc/decision_points/supplier_contacted.py b/src/ssvc/decision_points/ssvc_/supplier_contacted.py similarity index 100% rename from src/ssvc/decision_points/supplier_contacted.py rename to src/ssvc/decision_points/ssvc_/supplier_contacted.py diff --git a/src/ssvc/decision_points/supplier_engagement.py b/src/ssvc/decision_points/ssvc_/supplier_engagement.py similarity index 100% rename from src/ssvc/decision_points/supplier_engagement.py rename to src/ssvc/decision_points/ssvc_/supplier_engagement.py diff --git a/src/ssvc/decision_points/supplier_involvement.py b/src/ssvc/decision_points/ssvc_/supplier_involvement.py similarity index 100% rename from src/ssvc/decision_points/supplier_involvement.py rename to src/ssvc/decision_points/ssvc_/supplier_involvement.py diff --git a/src/ssvc/decision_points/system_exposure.py b/src/ssvc/decision_points/ssvc_/system_exposure.py similarity index 100% rename from src/ssvc/decision_points/system_exposure.py rename to src/ssvc/decision_points/ssvc_/system_exposure.py diff --git a/src/ssvc/decision_points/technical_impact.py b/src/ssvc/decision_points/ssvc_/technical_impact.py similarity index 100% rename from src/ssvc/decision_points/technical_impact.py rename to src/ssvc/decision_points/ssvc_/technical_impact.py diff --git a/src/ssvc/decision_points/utility.py b/src/ssvc/decision_points/ssvc_/utility.py similarity index 100% rename from src/ssvc/decision_points/utility.py rename to src/ssvc/decision_points/ssvc_/utility.py diff --git a/src/ssvc/decision_points/value_density.py b/src/ssvc/decision_points/ssvc_/value_density.py similarity index 100% rename from src/ssvc/decision_points/value_density.py rename to src/ssvc/decision_points/ssvc_/value_density.py diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index cd731cd6..d46eccae 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -17,9 +17,9 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.public_value_added import PUBLIC_VALUE_ADDED_1 -from ssvc.decision_points.supplier_involvement import SUPPLIER_INVOLVEMENT_1 +from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc_.public_value_added import PUBLIC_VALUE_ADDED_1 +from ssvc.decision_points.ssvc_.supplier_involvement import SUPPLIER_INVOLVEMENT_1 from ssvc.dp_groups.base import SsvcDecisionPointGroup diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index 2d08fe7a..af09a56b 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -17,19 +17,18 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.automatable import AUTOMATABLE_2 -from ssvc.decision_points.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 -from ssvc.decision_points.report_credibility import REPORT_CREDIBILITY_1 -from ssvc.decision_points.report_public import REPORT_PUBLIC_1 -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.supplier_cardinality import SUPPLIER_CARDINALITY_1 -from ssvc.decision_points.supplier_contacted import SUPPLIER_CONTACTED_1 -from ssvc.decision_points.supplier_engagement import SUPPLIER_ENGAGEMENT_1 -from ssvc.decision_points.utility import UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc_.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 +from ssvc.decision_points.ssvc_.report_credibility import REPORT_CREDIBILITY_1 +from ssvc.decision_points.ssvc_.report_public import REPORT_PUBLIC_1 +from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc_.supplier_cardinality import SUPPLIER_CARDINALITY_1 +from ssvc.decision_points.ssvc_.supplier_contacted import SUPPLIER_CONTACTED_1 +from ssvc.decision_points.ssvc_.supplier_engagement import SUPPLIER_ENGAGEMENT_1 +from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import SsvcDecisionPointGroup - COORDINATOR_TRIAGE_1 = SsvcDecisionPointGroup( name="Coordinator Triage", description="The decision points used by the coordinator during triage.", diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index 7f937479..341026fa 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -18,20 +18,20 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.automatable import AUTOMATABLE_2 -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.human_impact import HUMAN_IMPACT_2 -from ssvc.decision_points.mission_impact import ( +from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc_.human_impact import HUMAN_IMPACT_2 +from ssvc.decision_points.ssvc_.mission_impact import ( MISSION_IMPACT_1, MISSION_IMPACT_2, ) -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.system_exposure import ( +from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc_.system_exposure import ( SYSTEM_EXPOSURE_1, SYSTEM_EXPOSURE_1_0_1, ) -from ssvc.decision_points.utility import UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import SsvcDecisionPointGroup PATCH_APPLIER_1 = SsvcDecisionPointGroup( diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index b9e42ebb..1268fd8f 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -18,12 +18,12 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.automatable import AUTOMATABLE_2, VIRULENCE_1 -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.technical_impact import TECHNICAL_IMPACT_1 -from ssvc.decision_points.utility import UTILITY_1, UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2, VIRULENCE_1 +from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc_.technical_impact import TECHNICAL_IMPACT_1 +from ssvc.decision_points.ssvc_.utility import UTILITY_1, UTILITY_1_0_1 +from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import SsvcDecisionPointGroup PATCH_DEVELOPER_1 = SsvcDecisionPointGroup( diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 8d119b51..847ab963 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -330,10 +330,10 @@ def _is_topological_order(self, node_order: list) -> bool: def main(): - from ssvc.decision_points.automatable import AUTOMATABLE_2 - from ssvc.decision_points.exploitation import EXPLOITATION_1 - from ssvc.decision_points.human_impact import HUMAN_IMPACT_2 - from ssvc.decision_points.system_exposure import SYSTEM_EXPOSURE_1_0_1 + from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 + from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 + from ssvc.decision_points.ssvc_.human_impact import HUMAN_IMPACT_2 + from ssvc.decision_points.ssvc_.system_exposure import SYSTEM_EXPOSURE_1_0_1 from ssvc.outcomes.groups import DSOI # set up logging diff --git a/src/test/test_prioritization_framework.py b/src/test/test_prioritization_framework.py index eccc75b8..a8ec7dba 100644 --- a/src/test/test_prioritization_framework.py +++ b/src/test/test_prioritization_framework.py @@ -15,9 +15,9 @@ import pandas as pd -from ssvc.decision_points.exploitation import LATEST as exploitation_dp -from ssvc.decision_points.safety_impact import LATEST as safety_dp -from ssvc.decision_points.system_exposure import LATEST as exposure_dp +from ssvc.decision_points.ssvc_.exploitation import LATEST as exploitation_dp +from ssvc.decision_points.ssvc_.safety_impact import LATEST as safety_dp +from ssvc.decision_points.ssvc_.system_exposure import LATEST as exposure_dp from ssvc.decision_tables.base import DecisionTable from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.groups import DSOI as dsoi_og diff --git a/src/test/test_schema.py b/src/test/test_schema.py index a8835bf8..5e8fdcdb 100644 --- a/src/test/test_schema.py +++ b/src/test/test_schema.py @@ -22,18 +22,16 @@ import ssvc.decision_points # noqa F401 from ssvc.decision_points.base import REGISTERED_DECISION_POINTS - # importing these causes the decision points to register themselves -from ssvc.decision_points.critical_software import CRITICAL_SOFTWARE_1 # noqa -from ssvc.decision_points.high_value_asset import HIGH_VALUE_ASSET_1 # noqa -from ssvc.decision_points.in_kev import IN_KEV_1 +from ssvc.decision_points.ssvc_.critical_software import CRITICAL_SOFTWARE_1 # noqa +from ssvc.decision_points.ssvc_.high_value_asset import HIGH_VALUE_ASSET_1 # noqa +from ssvc.decision_points.ssvc_.in_kev import IN_KEV_1 from ssvc.dp_groups.cvss.collections import ( CVSSv1, CVSSv2, CVSSv3, CVSSv4, ) # noqa - # importing these causes the decision points to register themselves from ssvc.dp_groups.ssvc.collections import SSVCv1, SSVCv2, SSVCv2_1 # noqa From dc38efc496603239ae0ac1b40fe1f54bf5013756 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Mar 2025 16:27:56 -0400 Subject: [PATCH 56/99] move json examples to namespace dir -- fixes #751 --- data/json/decision_points/{ => ssvc}/automatable_2_0_0.json | 0 data/json/decision_points/{ => ssvc}/exploitation_1_0_0.json | 0 data/json/decision_points/{ => ssvc}/exploitation_1_1_0.json | 0 data/json/decision_points/{ => ssvc}/human_impact_1_0_0.json | 0 data/json/decision_points/{ => ssvc}/human_impact_2_0_0.json | 0 data/json/decision_points/{ => ssvc}/human_impact_2_0_1.json | 0 .../{ => ssvc}/mission_and_well-being_impact_1_0_0.json | 0 .../decision_points/{ => ssvc}/mission_impact_1_0_0.json | 0 .../decision_points/{ => ssvc}/mission_impact_2_0_0.json | 0 .../{ => ssvc}/public_safety_impact_1_0_0.json | 0 .../{ => ssvc}/public_safety_impact_2_0_0.json | 0 .../{ => ssvc}/public_safety_impact_2_0_1.json | 0 .../decision_points/{ => ssvc}/public_value_added_1_0_0.json | 0 .../{ => ssvc}/public_well-being_impact_1_0_0.json | 0 .../decision_points/{ => ssvc}/report_credibility_1_0_0.json | 0 .../json/decision_points/{ => ssvc}/report_public_1_0_0.json | 0 .../json/decision_points/{ => ssvc}/safety_impact_1_0_0.json | 0 .../json/decision_points/{ => ssvc}/safety_impact_2_0_0.json | 0 .../{ => ssvc}/supplier_cardinality_1_0_0.json | 0 .../decision_points/{ => ssvc}/supplier_contacted_1_0_0.json | 0 .../{ => ssvc}/supplier_engagement_1_0_0.json | 0 .../{ => ssvc}/supplier_involvement_1_0_0.json | 0 .../decision_points/{ => ssvc}/system_exposure_1_0_0.json | 0 .../decision_points/{ => ssvc}/system_exposure_1_0_1.json | 0 .../decision_points/{ => ssvc}/technical_impact_1_0_0.json | 0 data/json/decision_points/{ => ssvc}/utility_1_0_0.json | 0 data/json/decision_points/{ => ssvc}/utility_1_0_1.json | 0 .../json/decision_points/{ => ssvc}/value_density_1_0_0.json | 0 data/json/decision_points/{ => ssvc}/virulence_1_0_0.json | 0 src/ssvc/doctools.py | 5 ++--- 30 files changed, 2 insertions(+), 3 deletions(-) rename data/json/decision_points/{ => ssvc}/automatable_2_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/exploitation_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/exploitation_1_1_0.json (100%) rename data/json/decision_points/{ => ssvc}/human_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/human_impact_2_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/human_impact_2_0_1.json (100%) rename data/json/decision_points/{ => ssvc}/mission_and_well-being_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/mission_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/mission_impact_2_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/public_safety_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/public_safety_impact_2_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/public_safety_impact_2_0_1.json (100%) rename data/json/decision_points/{ => ssvc}/public_value_added_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/public_well-being_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/report_credibility_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/report_public_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/safety_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/safety_impact_2_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/supplier_cardinality_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/supplier_contacted_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/supplier_engagement_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/supplier_involvement_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/system_exposure_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/system_exposure_1_0_1.json (100%) rename data/json/decision_points/{ => ssvc}/technical_impact_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/utility_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/utility_1_0_1.json (100%) rename data/json/decision_points/{ => ssvc}/value_density_1_0_0.json (100%) rename data/json/decision_points/{ => ssvc}/virulence_1_0_0.json (100%) diff --git a/data/json/decision_points/automatable_2_0_0.json b/data/json/decision_points/ssvc/automatable_2_0_0.json similarity index 100% rename from data/json/decision_points/automatable_2_0_0.json rename to data/json/decision_points/ssvc/automatable_2_0_0.json diff --git a/data/json/decision_points/exploitation_1_0_0.json b/data/json/decision_points/ssvc/exploitation_1_0_0.json similarity index 100% rename from data/json/decision_points/exploitation_1_0_0.json rename to data/json/decision_points/ssvc/exploitation_1_0_0.json diff --git a/data/json/decision_points/exploitation_1_1_0.json b/data/json/decision_points/ssvc/exploitation_1_1_0.json similarity index 100% rename from data/json/decision_points/exploitation_1_1_0.json rename to data/json/decision_points/ssvc/exploitation_1_1_0.json diff --git a/data/json/decision_points/human_impact_1_0_0.json b/data/json/decision_points/ssvc/human_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/human_impact_1_0_0.json rename to data/json/decision_points/ssvc/human_impact_1_0_0.json diff --git a/data/json/decision_points/human_impact_2_0_0.json b/data/json/decision_points/ssvc/human_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/human_impact_2_0_0.json rename to data/json/decision_points/ssvc/human_impact_2_0_0.json diff --git a/data/json/decision_points/human_impact_2_0_1.json b/data/json/decision_points/ssvc/human_impact_2_0_1.json similarity index 100% rename from data/json/decision_points/human_impact_2_0_1.json rename to data/json/decision_points/ssvc/human_impact_2_0_1.json diff --git a/data/json/decision_points/mission_and_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/mission_and_well-being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/mission_and_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/mission_and_well-being_impact_1_0_0.json diff --git a/data/json/decision_points/mission_impact_1_0_0.json b/data/json/decision_points/ssvc/mission_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/mission_impact_1_0_0.json rename to data/json/decision_points/ssvc/mission_impact_1_0_0.json diff --git a/data/json/decision_points/mission_impact_2_0_0.json b/data/json/decision_points/ssvc/mission_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/mission_impact_2_0_0.json rename to data/json/decision_points/ssvc/mission_impact_2_0_0.json diff --git a/data/json/decision_points/public_safety_impact_1_0_0.json b/data/json/decision_points/ssvc/public_safety_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/public_safety_impact_1_0_0.json rename to data/json/decision_points/ssvc/public_safety_impact_1_0_0.json diff --git a/data/json/decision_points/public_safety_impact_2_0_0.json b/data/json/decision_points/ssvc/public_safety_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/public_safety_impact_2_0_0.json rename to data/json/decision_points/ssvc/public_safety_impact_2_0_0.json diff --git a/data/json/decision_points/public_safety_impact_2_0_1.json b/data/json/decision_points/ssvc/public_safety_impact_2_0_1.json similarity index 100% rename from data/json/decision_points/public_safety_impact_2_0_1.json rename to data/json/decision_points/ssvc/public_safety_impact_2_0_1.json diff --git a/data/json/decision_points/public_value_added_1_0_0.json b/data/json/decision_points/ssvc/public_value_added_1_0_0.json similarity index 100% rename from data/json/decision_points/public_value_added_1_0_0.json rename to data/json/decision_points/ssvc/public_value_added_1_0_0.json diff --git a/data/json/decision_points/public_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/public_well-being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/public_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/public_well-being_impact_1_0_0.json diff --git a/data/json/decision_points/report_credibility_1_0_0.json b/data/json/decision_points/ssvc/report_credibility_1_0_0.json similarity index 100% rename from data/json/decision_points/report_credibility_1_0_0.json rename to data/json/decision_points/ssvc/report_credibility_1_0_0.json diff --git a/data/json/decision_points/report_public_1_0_0.json b/data/json/decision_points/ssvc/report_public_1_0_0.json similarity index 100% rename from data/json/decision_points/report_public_1_0_0.json rename to data/json/decision_points/ssvc/report_public_1_0_0.json diff --git a/data/json/decision_points/safety_impact_1_0_0.json b/data/json/decision_points/ssvc/safety_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/safety_impact_1_0_0.json rename to data/json/decision_points/ssvc/safety_impact_1_0_0.json diff --git a/data/json/decision_points/safety_impact_2_0_0.json b/data/json/decision_points/ssvc/safety_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/safety_impact_2_0_0.json rename to data/json/decision_points/ssvc/safety_impact_2_0_0.json diff --git a/data/json/decision_points/supplier_cardinality_1_0_0.json b/data/json/decision_points/ssvc/supplier_cardinality_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_cardinality_1_0_0.json rename to data/json/decision_points/ssvc/supplier_cardinality_1_0_0.json diff --git a/data/json/decision_points/supplier_contacted_1_0_0.json b/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_contacted_1_0_0.json rename to data/json/decision_points/ssvc/supplier_contacted_1_0_0.json diff --git a/data/json/decision_points/supplier_engagement_1_0_0.json b/data/json/decision_points/ssvc/supplier_engagement_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_engagement_1_0_0.json rename to data/json/decision_points/ssvc/supplier_engagement_1_0_0.json diff --git a/data/json/decision_points/supplier_involvement_1_0_0.json b/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_involvement_1_0_0.json rename to data/json/decision_points/ssvc/supplier_involvement_1_0_0.json diff --git a/data/json/decision_points/system_exposure_1_0_0.json b/data/json/decision_points/ssvc/system_exposure_1_0_0.json similarity index 100% rename from data/json/decision_points/system_exposure_1_0_0.json rename to data/json/decision_points/ssvc/system_exposure_1_0_0.json diff --git a/data/json/decision_points/system_exposure_1_0_1.json b/data/json/decision_points/ssvc/system_exposure_1_0_1.json similarity index 100% rename from data/json/decision_points/system_exposure_1_0_1.json rename to data/json/decision_points/ssvc/system_exposure_1_0_1.json diff --git a/data/json/decision_points/technical_impact_1_0_0.json b/data/json/decision_points/ssvc/technical_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/technical_impact_1_0_0.json rename to data/json/decision_points/ssvc/technical_impact_1_0_0.json diff --git a/data/json/decision_points/utility_1_0_0.json b/data/json/decision_points/ssvc/utility_1_0_0.json similarity index 100% rename from data/json/decision_points/utility_1_0_0.json rename to data/json/decision_points/ssvc/utility_1_0_0.json diff --git a/data/json/decision_points/utility_1_0_1.json b/data/json/decision_points/ssvc/utility_1_0_1.json similarity index 100% rename from data/json/decision_points/utility_1_0_1.json rename to data/json/decision_points/ssvc/utility_1_0_1.json diff --git a/data/json/decision_points/value_density_1_0_0.json b/data/json/decision_points/ssvc/value_density_1_0_0.json similarity index 100% rename from data/json/decision_points/value_density_1_0_0.json rename to data/json/decision_points/ssvc/value_density_1_0_0.json diff --git a/data/json/decision_points/virulence_1_0_0.json b/data/json/decision_points/ssvc/virulence_1_0_0.json similarity index 100% rename from data/json/decision_points/virulence_1_0_0.json rename to data/json/decision_points/ssvc/virulence_1_0_0.json diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index f16323c6..1cdbe8e9 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -33,10 +33,10 @@ import ssvc.dp_groups.cvss.collections # noqa import ssvc.dp_groups.ssvc.collections # noqa -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import ( REGISTERED_DECISION_POINTS, ) +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint logger = logging.getLogger(__name__) @@ -134,8 +134,7 @@ def dump_json( parts = [ jsondir, ] - if dp.namespace != "ssvc": - parts.append(_filename_friendly(dp.namespace)) + parts.append(_filename_friendly(dp.namespace)) parts.append(filename) json_file = os.path.join(*parts) From a97d5cafa97bcd79ea70c8f670fe05300e01a443 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 21 Mar 2025 16:42:40 -0400 Subject: [PATCH 57/99] fix tests --- src/ssvc/decision_tables/base.py | 3 ++- src/ssvc/doctools.py | 8 +++++--- src/test/test_decision_table.py | 2 +- src/test/test_doctools.py | 13 ++++++++++--- src/test/test_dp_base.py | 8 ++++---- src/test/test_dp_groups.py | 2 +- src/test/test_dp_helpers.py | 5 +++-- src/test/test_policy_generator.py | 1 + 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 838a5f99..15871beb 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -24,7 +24,8 @@ from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned from ssvc.csv_analyzer import check_topological_order -from ssvc.decision_points.base import DecisionPointValue, SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.dp_groups.base import SsvcDecisionPointGroup from ssvc.outcomes.base import OutcomeGroup, OutcomeValue from ssvc.policy_generator import PolicyGenerator diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index 1cdbe8e9..7ab79d02 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -34,7 +34,7 @@ import ssvc.dp_groups.cvss.collections # noqa import ssvc.dp_groups.ssvc.collections # noqa from ssvc.decision_points.base import ( - REGISTERED_DECISION_POINTS, + DecisionPoint, REGISTERED_DECISION_POINTS, ) from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint @@ -115,7 +115,7 @@ def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool) -> def dump_json( - basename: str, dp: SsvcDecisionPoint, jsondir: str, overwrite: bool + basename: str, dp: DecisionPoint, jsondir: str, overwrite: bool ) -> str: """ Generate the json example for a decision point. @@ -135,13 +135,15 @@ def dump_json( jsondir, ] parts.append(_filename_friendly(dp.namespace)) + dirname = os.path.join(*parts) + parts.append(filename) json_file = os.path.join(*parts) if overwrite: remove_if_exists(json_file) - with EnsureDirExists(jsondir): + with EnsureDirExists(dirname): try: with open(json_file, "x") as f: f.write(dp.model_dump_json(indent=2)) diff --git a/src/test/test_decision_table.py b/src/test/test_decision_table.py index 841be8eb..d923a64a 100644 --- a/src/test/test_decision_table.py +++ b/src/test/test_decision_table.py @@ -39,7 +39,7 @@ def setUp(self): ) dpvs.append(dpv) - dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( name=f"Decision Point {i}", key=f"DP{i}", description=f"Decision Point {i} Description", diff --git a/src/test/test_doctools.py b/src/test/test_doctools.py index 33e2924b..0c218805 100644 --- a/src/test/test_doctools.py +++ b/src/test/test_doctools.py @@ -16,6 +16,7 @@ import tempfile import unittest +from ssvc.decision_points.base import DecisionPoint from ssvc.doctools import ( EnsureDirExists, _filename_friendly, @@ -39,7 +40,7 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: - self.dp = SsvcDecisionPoint.model_validate(_dp_dict) + self.dp = DecisionPoint.model_validate(_dp_dict) # create a temp working dir self.tempdir = tempfile.TemporaryDirectory() @@ -96,7 +97,11 @@ def test_dump_decision_point(self): self.assertIn("json", os.listdir(self.tempdir.name)) self.assertEqual(1, len(os.listdir(jsondir))) - file_created = os.listdir(jsondir)[0] + nsdir = os.path.join(jsondir, dp.namespace) + self.assertTrue(os.path.exists(nsdir)) + self.assertEqual(1, len(os.listdir(nsdir))) + + file_created = os.listdir(nsdir)[0] for word in dp.name.split(): self.assertIn(word.lower(), file_created) @@ -109,12 +114,14 @@ def test_dump_json(self): dp = self.dp jsondir = self.tempdir.name overwrite = False + nsdir = os.path.join(jsondir, dp.namespace) - _jsonfile = os.path.join(jsondir, f"{basename}.json") + _jsonfile = os.path.join(nsdir, f"{basename}.json") self.assertFalse(os.path.exists(_jsonfile)) # should create the file in the expected place json_file = dump_json(basename, dp, jsondir, overwrite) + self.assertEqual(_jsonfile, json_file) self.assertTrue(os.path.exists(json_file)) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py index 79da6013..175a3d73 100644 --- a/src/test/test_dp_base.py +++ b/src/test/test_dp_base.py @@ -30,7 +30,7 @@ def setUp(self) -> None: ) ) - self.dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + self.dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( name="foo", key="bar", description="baz", @@ -55,7 +55,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -68,7 +68,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -118,7 +118,7 @@ def test_ssvc_decision_point_json_roundtrip(self): self.assertIsInstance(json, str) self.assertGreater(len(json), 0) - obj2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint.model_validate_json( + obj2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint.model_validate_json( json ) diff --git a/src/test/test_dp_groups.py b/src/test/test_dp_groups.py index a02ef25e..ad8b9a73 100644 --- a/src/test/test_dp_groups.py +++ b/src/test/test_dp_groups.py @@ -22,7 +22,7 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: self.dps = [] for i in range(10): - dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( name=f"Decision Point {i}", key=f"DP_{i}", description=f"Description of Decision Point {i}", diff --git a/src/test/test_dp_helpers.py b/src/test/test_dp_helpers.py index d62811f2..493bdb81 100644 --- a/src/test/test_dp_helpers.py +++ b/src/test/test_dp_helpers.py @@ -25,7 +25,8 @@ def setUp(self) -> None: key="test_dp", description="This is a test decision point", version="1.0.0", - values=[ + namespace='x_test', + values=( DecisionPointValue( name="Yes", key="yes", @@ -36,7 +37,7 @@ def setUp(self) -> None: key="no", description="No", ), - ], + ), ) self.dp2 = deepcopy(self.dp1) diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 2be16f7b..b68fb757 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -44,6 +44,7 @@ def setUp(self) -> None: name=c, description=c, key=c, + namespace='x_test', values=[ DecisionPointValue(name=v, key=v, description=v) for v in self.dp_values From d107c586e08fd3ca66bf461eff8cc0f18d2c68a5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 10:01:12 -0400 Subject: [PATCH 58/99] reorganize test cases to parallel `ssvc` package --- src/test/decision_points/__init__.py | 15 +++ .../test_cvss_helpers.py | 0 .../{ => decision_points}/test_dp_base.py | 0 .../{ => decision_points}/test_dp_helpers.py | 0 src/test/decision_tables/__init__.py | 15 +++ .../test_decision_table.py | 0 src/test/dp_groups/__init__.py | 15 +++ src/test/{ => dp_groups}/test_dp_groups.py | 0 src/test/outcomes/__init__.py | 15 +++ src/test/{ => outcomes}/test_outcomes.py | 0 src/test/test_prioritization_framework.py | 99 ------------------- 11 files changed, 60 insertions(+), 99 deletions(-) create mode 100644 src/test/decision_points/__init__.py rename src/test/{ => decision_points}/test_cvss_helpers.py (100%) rename src/test/{ => decision_points}/test_dp_base.py (100%) rename src/test/{ => decision_points}/test_dp_helpers.py (100%) create mode 100644 src/test/decision_tables/__init__.py rename src/test/{ => decision_tables}/test_decision_table.py (100%) create mode 100644 src/test/dp_groups/__init__.py rename src/test/{ => dp_groups}/test_dp_groups.py (100%) create mode 100644 src/test/outcomes/__init__.py rename src/test/{ => outcomes}/test_outcomes.py (100%) delete mode 100644 src/test/test_prioritization_framework.py diff --git a/src/test/decision_points/__init__.py b/src/test/decision_points/__init__.py new file mode 100644 index 00000000..5c215061 --- /dev/null +++ b/src/test/decision_points/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.decision_points. +""" diff --git a/src/test/test_cvss_helpers.py b/src/test/decision_points/test_cvss_helpers.py similarity index 100% rename from src/test/test_cvss_helpers.py rename to src/test/decision_points/test_cvss_helpers.py diff --git a/src/test/test_dp_base.py b/src/test/decision_points/test_dp_base.py similarity index 100% rename from src/test/test_dp_base.py rename to src/test/decision_points/test_dp_base.py diff --git a/src/test/test_dp_helpers.py b/src/test/decision_points/test_dp_helpers.py similarity index 100% rename from src/test/test_dp_helpers.py rename to src/test/decision_points/test_dp_helpers.py diff --git a/src/test/decision_tables/__init__.py b/src/test/decision_tables/__init__.py new file mode 100644 index 00000000..3a081023 --- /dev/null +++ b/src/test/decision_tables/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.decision_tables. +""" diff --git a/src/test/test_decision_table.py b/src/test/decision_tables/test_decision_table.py similarity index 100% rename from src/test/test_decision_table.py rename to src/test/decision_tables/test_decision_table.py diff --git a/src/test/dp_groups/__init__.py b/src/test/dp_groups/__init__.py new file mode 100644 index 00000000..bb9ae345 --- /dev/null +++ b/src/test/dp_groups/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.dp_groups. +""" diff --git a/src/test/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py similarity index 100% rename from src/test/test_dp_groups.py rename to src/test/dp_groups/test_dp_groups.py diff --git a/src/test/outcomes/__init__.py b/src/test/outcomes/__init__.py new file mode 100644 index 00000000..3c4a8bfe --- /dev/null +++ b/src/test/outcomes/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.outcomes. +""" diff --git a/src/test/test_outcomes.py b/src/test/outcomes/test_outcomes.py similarity index 100% rename from src/test/test_outcomes.py rename to src/test/outcomes/test_outcomes.py diff --git a/src/test/test_prioritization_framework.py b/src/test/test_prioritization_framework.py deleted file mode 100644 index a8ec7dba..00000000 --- a/src/test/test_prioritization_framework.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University - -import unittest - -import pandas as pd - -from ssvc.decision_points.ssvc_.exploitation import LATEST as exploitation_dp -from ssvc.decision_points.ssvc_.safety_impact import LATEST as safety_dp -from ssvc.decision_points.ssvc_.system_exposure import LATEST as exposure_dp -from ssvc.decision_tables.base import DecisionTable -from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.groups import DSOI as dsoi_og - - -class MyTestCase(unittest.TestCase): - def setUp(self): - self.framework = DecisionTable( - name="Test Prioritization Framework", - description="Test Prioritization Framework Description", - version="1.0.0", - namespace="x_test", - decision_point_group=SsvcDecisionPointGroup( - name="Test Decision Point Group", - description="Test Decision Point Group Description", - decision_points=[exploitation_dp, exposure_dp, safety_dp], - ), - outcome_group=dsoi_og, - mapping=[], - ) - - pass - - def tearDown(self): - pass - - def test_create(self): - self.assertEqual(self.framework.name, "Test Prioritization Framework") - self.assertEqual(3, len(self.framework.decision_point_group)) - # mapping should not be empty - self.assertGreater(len(self.framework.mapping), 0) - - def test_generate_mapping(self): - result = self.framework.generate_df() - - # there should be one row in result for each combination of decision points - combo_count = len(list(self.framework.decision_point_group.combinations())) - self.assertEqual(len(result), combo_count) - - # the length of each key should be the number of decision points - for key in result.keys(): - parts = key.split(",") - self.assertEqual(len(parts), 3) - for i, keypart in enumerate(parts): - dp_namespace, dp_key, dp_value_key = keypart.split(":") - - dp = self.framework.decision_point_group.decision_points[i] - self.assertEqual(dp_namespace, dp.namespace) - self.assertEqual(dp_key, dp.key) - value_keys = [v.key for v in dp.values] - self.assertIn(dp_value_key, value_keys) - - def test_mapping_to_table(self): - d = { - "ssvc:One:A,ssvc:Two:B,ssvc:Three:C": "og:One", - "ssvc:One:A,ssvc:Two:B,ssvc:Three:D": "og:Two", - } - table = self.framework.mapping_to_table(d) - - # is it a DataFrame? - self.assertIsInstance(table, pd.DataFrame) - self.assertEqual(2, len(table)) - self.assertEqual(4, len(table.columns)) - - # does it have the right columns? - self.assertEqual(["one", "two", "three", "og"], list(table.columns)) - # does it have the right values? - for i, (k, v) in enumerate(d.items()): - k = k.lower() - v = v.lower() - - parts = k.split(",") - for part in parts: - (ns, dp, val) = part.split(":") - self.assertEqual(val, table.iloc[i][dp]) - - -if __name__ == "__main__": - unittest.main() From 99f874ca575a8a411093502896cb8143c3107359 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 12:02:26 -0400 Subject: [PATCH 59/99] add str property to both ValueSummary and DecisionPoint for use in lookup tables --- src/ssvc/decision_points/base.py | 52 +++++++++++++++++-- src/test/decision_points/test_dp_base.py | 65 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 3d09b215..c1781faa 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -35,6 +35,7 @@ _RDP = {} REGISTERED_DECISION_POINTS = [] +FIELD_DELIMITER = ":" def register(dp): @@ -89,9 +90,20 @@ class ValueSummary(_Versioned, _Keyed, _Namespaced, BaseModel): value: str def __str__(self): - s = ":".join([self.namespace, self.key, self.version, self.value]) + s = FIELD_DELIMITER.join([self.namespace, self.key, self.version, self.value]) return s + @property + def str(self): + """ + Return the ValueSummary as a string. + + Returns: + str: A string representation of the ValueSummary, in the format "namespace:key:version:value". + + """ + return self.__str__() + class DecisionPoint( _Valued, _Keyed, _SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel @@ -111,6 +123,20 @@ class DecisionPoint( values: tuple[DecisionPointValue, ...] + def __str__(self): + return FIELD_DELIMITER.join([self.namespace, self.key, self.version]) + + @property + def str(self) -> str: + """ + Return the DecisionPoint represented as a short string. + + Returns: + str: A string representation of the DecisionPoint, in the format "namespace:key:version". + + """ + return self.__str__() + @model_validator(mode="after") def _register(self): """ @@ -124,7 +150,14 @@ def value_summaries(self) -> list[ValueSummary]: """ Return a list of value summaries. """ - summaries = [] + return list(self.value_summaries_dict.values()) + + @property + def value_summaries_dict(self) -> dict[str, ValueSummary]: + """ + Return a dictionary of value summaries keyed by the value key. + """ + summaries = {} for value in self.values: summary = ValueSummary( key=self.key, @@ -132,9 +165,22 @@ def value_summaries(self) -> list[ValueSummary]: namespace=self.namespace, value=value.key, ) - summaries.append(summary) + key = summary.str + summaries[key] = summary + return summaries + @property + def value_summaries_str(self): + """ + Return a list of value summaries as strings. + + Returns: + list: A list of strings, each representing a value summary in the format "namespace:key:version:value". + + """ + return list(self.value_summaries_dict.keys()) + def main(): opt_none = DecisionPointValue( diff --git a/src/test/decision_points/test_dp_base.py b/src/test/decision_points/test_dp_base.py index 175a3d73..e573d59f 100644 --- a/src/test/decision_points/test_dp_base.py +++ b/src/test/decision_points/test_dp_base.py @@ -126,6 +126,71 @@ def test_ssvc_decision_point_json_roundtrip(self): self.assertEqual(obj, obj2) self.assertEqual(obj.model_dump(), obj2.model_dump()) + def test_value_summaries_dict(self): + obj = self.dp + summaries = obj.value_summaries_dict + + # should be a dictionary + self.assertIsInstance(summaries, dict) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries dict should have str(ValueSummary) as the key + # and the ValueSummary as the value + for key, summary in summaries.items(): + # confirm the key is the string representation of the ValueSummary + self.assertEqual(key, str(summary)) + + # confirm the attributes of the ValueSummary + # key, version, and namespace come from the decision point + self.assertEqual(summary.key, obj.key) + self.assertEqual(summary.version, obj.version) + self.assertEqual(summary.namespace, obj.namespace) + # value comes from the list of values, and should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(summary.value, value_keys) + + def test_value_summaries_str(self): + obj = self.dp + summaries = obj.value_summaries_str + + # should be a list + self.assertIsInstance(summaries, list) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries list should have str(ValueSummary) as the key + for key in summaries: + # confirm the key is the string representation of the ValueSummary + self.assertIsInstance(key, str) + + # parse the key into its parts + (ns, k, v, val) = key.split(":") + # ns, k, v should come from the decision point + self.assertEqual(ns, obj.namespace) + self.assertEqual(k, obj.key) + self.assertEqual(v, obj.version) + # val should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(val, value_keys) + + def test_value_summaries(self): + obj = self.dp + summaries = obj.value_summaries + + # should be a list + self.assertIsInstance(summaries, list) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries list should be ValueSummary objects + for summary in summaries: + self.assertIsInstance(summary, base.ValueSummary) + # key, version, and namespace come from the decision point + self.assertEqual(summary.key, obj.key) + self.assertEqual(summary.version, obj.version) + self.assertEqual(summary.namespace, obj.namespace) + # value comes from the list of values, and should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(summary.value, value_keys) + if __name__ == "__main__": unittest.main() From 3ee0b1261410d15ef695883b1f0da113e32a3895 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 12:04:16 -0400 Subject: [PATCH 60/99] adds decision point lookup dict --- src/ssvc/dp_groups/base.py | 17 +++++++++++++- src/test/dp_groups/test_dp_groups.py | 34 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index e43bd063..664644f9 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -24,7 +24,8 @@ from ssvc._mixins import _Base, _SchemaVersioned from ssvc.decision_points.base import ( - DecisionPoint, ValueSummary, + DecisionPoint, + ValueSummary, ) from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint @@ -50,6 +51,20 @@ def __len__(self): l = len(dplist) return l + @property + def decision_points_dict(self) -> dict[str, DecisionPoint]: + """ + Return a dictionary of decision points keyed by their name. + """ + return {dp.str: dp for dp in self.decision_points} + + @property + def decision_points_str(self) -> list[str]: + """ + Return a list of decision point names. + """ + return list(self.decision_points_dict.keys()) + def combination_summaries(self) -> Generator[tuple[ValueSummary, ...], None, None]: # get the value summaries for each decision point value_summaries = [dp.value_summaries for dp in self.decision_points] diff --git a/src/test/dp_groups/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py index ad8b9a73..e7612efa 100644 --- a/src/test/dp_groups/test_dp_groups.py +++ b/src/test/dp_groups/test_dp_groups.py @@ -119,6 +119,40 @@ def test_json_roundtrip(self): # assert that the new group is the same as the old group self.assertEqual(g_json, g2.model_dump_json()) + def test_decision_points_dict(self): + # add them to a decision point group + g = dpg.SsvcDecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # get the decision points as a dictionary + dp_dict = g.decision_points_dict + + # assert that the dictionary is the correct length + self.assertEqual(len(self.dps), len(dp_dict)) + + # assert that each decision point is in the dictionary + for dp in self.dps: + self.assertIn(dp.str, dp_dict) + self.assertEqual(dp, dp_dict[dp.str]) + + def test_decision_points_str(self): + g = dpg.SsvcDecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + dp_str = g.decision_points_str + self.assertEqual(len(self.dps), len(dp_str)) + + for i, dp in enumerate(self.dps): + self.assertIn(dp.str, dp_str) + # check that the string is the same as the decision point's string representation + # and they are in the same order + self.assertEqual(dp.str, dp_str[i]) + if __name__ == "__main__": unittest.main() From 5b6424222bd008d596a2bc4c24eed275a1e4fbf7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 13:04:44 -0400 Subject: [PATCH 61/99] start converting outcome groups to decision points --- src/ssvc/outcomes/base.py | 30 ++---- src/ssvc/outcomes/cvss/__init__.py | 15 +++ src/ssvc/outcomes/cvss/lmhc.py | 51 ++++++++++ src/ssvc/outcomes/groups.py | 140 ++++++++------------------ src/ssvc/outcomes/ssvc_/__init__.py | 15 +++ src/ssvc/outcomes/ssvc_/coordinate.py | 41 ++++++++ src/ssvc/outcomes/ssvc_/dsoi.py | 49 +++++++++ src/ssvc/outcomes/ssvc_/publish.py | 46 +++++++++ src/ssvc/policy_generator.py | 2 +- src/test/outcomes/test_outcomes.py | 1 + src/test/test_policy_generator.py | 41 +++++--- 11 files changed, 295 insertions(+), 136 deletions(-) create mode 100644 src/ssvc/outcomes/cvss/__init__.py create mode 100644 src/ssvc/outcomes/cvss/lmhc.py create mode 100644 src/ssvc/outcomes/ssvc_/__init__.py create mode 100644 src/ssvc/outcomes/ssvc_/coordinate.py create mode 100644 src/ssvc/outcomes/ssvc_/dsoi.py create mode 100644 src/ssvc/outcomes/ssvc_/publish.py diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index fabc5aca..a1cb6c08 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python -""" -Provides outcome group and outcome value classes for SSVC. -""" # Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project @@ -14,23 +10,13 @@ # (“Third Party Software”). See LICENSE.md for more details. # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides outcome group and outcome value classes for SSVC. +""" -from pydantic import BaseModel - -from ssvc._mixins import _Base, _Keyed, _SchemaVersioned, _Valued - - -class OutcomeValue(_Base, _Keyed, BaseModel): - """ - Models a single value option for an SSVC outcome. - """ - - -class OutcomeGroup(_Valued, _Base, _Keyed, _SchemaVersioned, BaseModel): - """ - Models an outcome group. - """ - - values: tuple[OutcomeValue, ...] +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint - # register all instances +OutcomeValue = DecisionPointValue +OutcomeGroup = DecisionPoint +SsvcOutcomeGroup = SsvcDecisionPoint diff --git a/src/ssvc/outcomes/cvss/__init__.py b/src/ssvc/outcomes/cvss/__init__.py new file mode 100644 index 00000000..02ead02b --- /dev/null +++ b/src/ssvc/outcomes/cvss/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides outcome group objects in the cvss namespace +""" diff --git a/src/ssvc/outcomes/cvss/lmhc.py b/src/ssvc/outcomes/cvss/lmhc.py new file mode 100644 index 00000000..803be31c --- /dev/null +++ b/src/ssvc/outcomes/cvss/lmhc.py @@ -0,0 +1,51 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.cvss.base import CvssDecisionPoint +from ssvc.decision_points.helpers import print_versions_and_diffs + +CVSS_NONE = DecisionPointValue(name="None", key="N", description="None (0.0)") +CVSS_LOW = DecisionPointValue(name="Low", key="L", description="Low (0.1-3.9)") +CVSS_MEDIUM = DecisionPointValue(name="Medium", key="M", description="Medium (4.0-6.9)") +CVSS_HIGH = DecisionPointValue(name="High", key="H", description="High (7.0-8.9)") +CVSS_CRITICAL = DecisionPointValue( + name="Critical", key="C", description="Critical (9.0-10.0)" +) + +LMHC = CvssDecisionPoint( + name="CVSS Qualitative Severity Rating Scale", + key="CVSS", + description="The CVSS Qualitative Severity Rating Scale group.", + version="1.0.0", + values=( + CVSS_NONE, + CVSS_LOW, + CVSS_MEDIUM, + CVSS_HIGH, + CVSS_CRITICAL, + ), +) +""" +The CVSS Qualitative Severity (N,L,M,H,C) Rating Scale outcome group. +""" + +VERSIONS = (LMHC,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 73c6c91c..ee12cbf6 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -1,8 +1,5 @@ #!/usr/bin/env python -""" -Provides a set of outcome groups for use in SSVC. -""" -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. +# Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project # Stakeholder Specific Vulnerability Categorization (SSVC) is @@ -14,133 +11,78 @@ # (“Third Party Software”). See LICENSE.md for more details. # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University - -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue - -# Note: Outcome Groups must be defined in ascending order. - - -DSOI = OutcomeGroup( - name="Defer, Scheduled, Out-of-Cycle, Immediate", - key="DSOI", - description="The original SSVC outcome group.", - version="1.0.0", - values=( - OutcomeValue(name="Defer", key="D", description="Defer"), - OutcomeValue(name="Scheduled", key="S", description="Scheduled"), - OutcomeValue(name="Out-of-Cycle", key="O", description="Out-of-Cycle"), - OutcomeValue(name="Immediate", key="I", description="Immediate"), - ), -) """ -The original SSVC outcome group. +Provides a set of outcome groups for use in SSVC. """ -PUBLISH = OutcomeGroup( - name="Publish, Do Not Publish", - key="PUBLISH", - description="The publish outcome group.", - version="1.0.0", - values=( - OutcomeValue(name="Do Not Publish", key="N", description="Do Not Publish"), - OutcomeValue(name="Publish", key="P", description="Publish"), - ), -) -""" -The publish outcome group. -""" +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -COORDINATE = OutcomeGroup( - name="Decline, Track, Coordinate", - key="COORDINATE", - description="The coordinate outcome group.", - version="1.0.0", - values=( - OutcomeValue(name="Decline", key="D", description="Decline"), - OutcomeValue(name="Track", key="T", description="Track"), - OutcomeValue(name="Coordinate", key="C", description="Coordinate"), - ), -) -""" -The coordinate outcome group. -""" -MOSCOW = OutcomeGroup( +# Note: Outcome Groups must be defined in ascending order. + + +MOSCOW = SsvcDecisionPoint( name="MoSCoW", - key="MOSCOW", - description="The Moscow outcome group.", + key="MSCW", + description="The MoSCoW (Must, Should, Could, Won't) outcome group.", version="1.0.0", values=( - OutcomeValue(name="Won't", key="W", description="Won't"), - OutcomeValue(name="Could", key="C", description="Could"), - OutcomeValue(name="Should", key="S", description="Should"), - OutcomeValue(name="Must", key="M", description="Must"), + DecisionPointValue(name="Won't", key="W", description="Won't"), + DecisionPointValue(name="Could", key="C", description="Could"), + DecisionPointValue(name="Should", key="S", description="Should"), + DecisionPointValue(name="Must", key="M", description="Must"), ), ) """ The MoSCoW outcome group. """ -EISENHOWER = OutcomeGroup( +EISENHOWER = SsvcDecisionPoint( name="Do, Schedule, Delegate, Delete", key="EISENHOWER", description="The Eisenhower outcome group.", version="1.0.0", values=( - OutcomeValue(name="Delete", key="D", description="Delete"), - OutcomeValue(name="Delegate", key="G", description="Delegate"), - OutcomeValue(name="Schedule", key="S", description="Schedule"), - OutcomeValue(name="Do", key="O", description="Do"), + DecisionPointValue(name="Delete", key="D", description="Delete"), + DecisionPointValue(name="Delegate", key="G", description="Delegate"), + DecisionPointValue(name="Schedule", key="S", description="Schedule"), + DecisionPointValue(name="Do", key="O", description="Do"), ), ) """ The Eisenhower outcome group. """ -CVSS = OutcomeGroup( - name="CVSS Levels", - key="CVSS", - description="The CVSS outcome group.", - version="1.0.0", - values=( - OutcomeValue(name="Low", key="L", description="Low"), - OutcomeValue(name="Medium", key="M", description="Medium"), - OutcomeValue(name="High", key="H", description="High"), - OutcomeValue(name="Critical", key="C", description="Critical"), - ), -) -""" -The CVSS outcome group. -""" -CISA = OutcomeGroup( +CISA = SsvcDecisionPoint( name="CISA Levels", key="CISA", description="The CISA outcome group. " "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", version="1.0.0", values=( - OutcomeValue( + DecisionPointValue( name="Track", key="T", description="The vulnerability does not require action at this time. " "The organization would continue to track the vulnerability and reassess it if new information becomes available. " "CISA recommends remediating Track vulnerabilities within standard update timelines.", ), - OutcomeValue( + DecisionPointValue( name="Track*", key="T*", description="The vulnerability contains specific characteristics that may require closer monitoring for changes. " "CISA recommends remediating Track* vulnerabilities within standard update timelines.", ), - OutcomeValue( + DecisionPointValue( name="Attend", key="A", description="The vulnerability requires attention from the organization's internal, supervisory-level individuals. " "Necessary actions may include requesting assistance or information about the vulnerability and may involve publishing a notification, either internally and/or externally, about the vulnerability. " "CISA recommends remediating Attend vulnerabilities sooner than standard update timelines.", ), - OutcomeValue( + DecisionPointValue( name="Act", key="A", description="The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. " @@ -155,49 +97,53 @@ See https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc """ -YES_NO = OutcomeGroup( +YES_NO = SsvcDecisionPoint( name="Yes, No", key="YES_NO", description="The Yes/No outcome group.", version="1.0.0", values=( - OutcomeValue(name="No", key="N", description="No"), - OutcomeValue(name="Yes", key="Y", description="Yes"), + DecisionPointValue(name="No", key="N", description="No"), + DecisionPointValue(name="Yes", key="Y", description="Yes"), ), ) """ The Yes/No outcome group. """ -VALUE_COMPLEXITY = OutcomeGroup( +VALUE_COMPLEXITY = SsvcDecisionPoint( name="Value, Complexity", key="VALUE_COMPLEXITY", description="The Value/Complexity outcome group.", version="1.0.0", values=( # drop, reconsider later, easy win, do first - OutcomeValue(name="Drop", key="D", description="Drop"), - OutcomeValue(name="Reconsider Later", key="R", description="Reconsider Later"), - OutcomeValue(name="Easy Win", key="E", description="Easy Win"), - OutcomeValue(name="Do First", key="F", description="Do First"), + DecisionPointValue(name="Drop", key="D", description="Drop"), + DecisionPointValue( + name="Reconsider Later", key="R", description="Reconsider Later" + ), + DecisionPointValue(name="Easy Win", key="E", description="Easy Win"), + DecisionPointValue(name="Do First", key="F", description="Do First"), ), ) """ The Value/Complexity outcome group. """ -THE_PARANOIDS = OutcomeGroup( +THE_PARANOIDS = SsvcDecisionPoint( name="theParanoids", key="PARANOIDS", description="PrioritizedRiskRemediation outcome group based on TheParanoids.", version="1.0.0", values=( - OutcomeValue(name="Track 5", key="5", description="Track"), - OutcomeValue(name="Track Closely 4", key="4", description="Track Closely"), - OutcomeValue(name="Attend 3", key="3", description="Attend"), - OutcomeValue(name="Attend 2", key="2", description="Attend"), - OutcomeValue(name="Act 1", key="1", description="Act"), - OutcomeValue(name="Act ASAP 0", key="0", description="Act ASAP"), + DecisionPointValue(name="Track 5", key="5", description="Track"), + DecisionPointValue( + name="Track Closely 4", key="4", description="Track Closely" + ), + DecisionPointValue(name="Attend 3", key="3", description="Attend"), + DecisionPointValue(name="Attend 2", key="2", description="Attend"), + DecisionPointValue(name="Act 1", key="1", description="Act"), + DecisionPointValue(name="Act ASAP 0", key="0", description="Act ASAP"), ), ) """ diff --git a/src/ssvc/outcomes/ssvc_/__init__.py b/src/ssvc/outcomes/ssvc_/__init__.py new file mode 100644 index 00000000..f9dba018 --- /dev/null +++ b/src/ssvc/outcomes/ssvc_/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides outcome group objects in the ssvc namespace +""" diff --git a/src/ssvc/outcomes/ssvc_/coordinate.py b/src/ssvc/outcomes/ssvc_/coordinate.py new file mode 100644 index 00000000..1baacdd2 --- /dev/null +++ b/src/ssvc/outcomes/ssvc_/coordinate.py @@ -0,0 +1,41 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint + +COORDINATE = SsvcDecisionPoint( + name="Decline, Track, Coordinate", + key="COORDINATE", + description="The coordinate outcome group.", + version="1.0.0", + values=( + DecisionPointValue(name="Decline", key="D", description="Decline"), + DecisionPointValue(name="Track", key="T", description="Track"), + DecisionPointValue(name="Coordinate", key="C", description="Coordinate"), + ), +) +""" +The coordinate outcome group. +""" + +VERSIONS = (COORDINATE,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc_/dsoi.py b/src/ssvc/outcomes/ssvc_/dsoi.py new file mode 100644 index 00000000..510609c9 --- /dev/null +++ b/src/ssvc/outcomes/ssvc_/dsoi.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the defer, scheduled, out-of-cycle, immediate outcome group for use in SSVC. +""" +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint + + +DSOI = SsvcDecisionPoint( + name="Defer, Scheduled, Out-of-Cycle, Immediate", + key="DSOI", + description="The original SSVC outcome group.", + version="1.0.0", + values=( + DecisionPointValue(name="Defer", key="D", description="Defer"), + DecisionPointValue(name="Scheduled", key="S", description="Scheduled"), + DecisionPointValue(name="Out-of-Cycle", key="O", description="Out-of-Cycle"), + DecisionPointValue(name="Immediate", key="I", description="Immediate"), + ), +) +""" +The original SSVC outcome group. +""" + + +VERSIONS = (DSOI,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc_/publish.py b/src/ssvc/outcomes/ssvc_/publish.py new file mode 100644 index 00000000..ef90c278 --- /dev/null +++ b/src/ssvc/outcomes/ssvc_/publish.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint + + +PUBLISH = SsvcDecisionPoint( + name="Publish, Do Not Publish", + key="PUBLISH", + description="The publish outcome group.", + version="1.0.0", + values=( + DecisionPointValue( + name="Do Not Publish", key="N", description="Do Not Publish" + ), + DecisionPointValue(name="Publish", key="P", description="Publish"), + ), +) +""" +The publish outcome group. +""" + +VERSIONS = (PUBLISH,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 847ab963..d03d5ad8 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -334,7 +334,7 @@ def main(): from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 from ssvc.decision_points.ssvc_.human_impact import HUMAN_IMPACT_2 from ssvc.decision_points.ssvc_.system_exposure import SYSTEM_EXPOSURE_1_0_1 - from ssvc.outcomes.groups import DSOI + from ssvc.outcomes.ssvc_.dsoi import DSOI # set up logging logger = logging.getLogger() diff --git a/src/test/outcomes/test_outcomes.py b/src/test/outcomes/test_outcomes.py index 85bdece2..b9ff379c 100644 --- a/src/test/outcomes/test_outcomes.py +++ b/src/test/outcomes/test_outcomes.py @@ -35,6 +35,7 @@ def test_outcome_group(self): name="Outcome Group", key="OG", description="an outcome group", + namespace="x_test", values=tuple(values), ) diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index b68fb757..b7dbffcb 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -20,7 +20,6 @@ from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue from ssvc.policy_generator import PolicyGenerator @@ -30,28 +29,38 @@ def setUp(self) -> None: self.dp_values = ["Yes", "No"] self.dp_names = ["Who", "What", "When", "Where"] - self.og = OutcomeGroup( + self.og = DecisionPoint( name="test", description="test", key="TEST", - values=[OutcomeValue(key=c, name=c, description=c) for c in self.og_names], + namespace="x_test", + values=tuple( + [ + DecisionPointValue(key=c, name=c, description=c) + for c in self.og_names + ] + ), ) self.dpg = SsvcDecisionPointGroup( name="test", description="test", - decision_points=[ - DecisionPoint( - name=c, - description=c, - key=c, - namespace='x_test', - values=[ - DecisionPointValue(name=v, key=v, description=v) - for v in self.dp_values - ], - ) - for c in self.dp_names - ], + decision_points=tuple( + [ + DecisionPoint( + name=c, + description=c, + key=c, + namespace="x_test", + values=tuple( + [ + DecisionPointValue(name=v, key=v, description=v) + for v in self.dp_values + ] + ), + ) + for c in self.dp_names + ] + ), ) def test_pg_init(self): From daddce30166b5ab0fa1d8a526cba886abe0585b3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 13:18:58 -0400 Subject: [PATCH 62/99] adds a cisa namespace, CisaDecisionPoint base class, and outcome groups for CISA-specific decision models --- src/ssvc/decision_points/cisa/__init__.py | 15 +++++ src/ssvc/decision_points/cisa/base.py | 23 +++++++ src/ssvc/namespaces.py | 1 + src/ssvc/outcomes/cisa/__init__.py | 15 +++++ src/ssvc/outcomes/cisa/scoring.py | 81 +++++++++++++++++++++++ src/ssvc/outcomes/groups.py | 42 ------------ 6 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 src/ssvc/decision_points/cisa/__init__.py create mode 100644 src/ssvc/decision_points/cisa/base.py create mode 100644 src/ssvc/outcomes/cisa/__init__.py create mode 100644 src/ssvc/outcomes/cisa/scoring.py diff --git a/src/ssvc/decision_points/cisa/__init__.py b/src/ssvc/decision_points/cisa/__init__.py new file mode 100644 index 00000000..e9f8bc40 --- /dev/null +++ b/src/ssvc/decision_points/cisa/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides Decision Point objects in the `cisa` namespace +""" diff --git a/src/ssvc/decision_points/cisa/base.py b/src/ssvc/decision_points/cisa/base.py new file mode 100644 index 00000000..739c5b0d --- /dev/null +++ b/src/ssvc/decision_points/cisa/base.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a base class for CISA-specific decision points. +""" +from pydantic import BaseModel + +from ssvc.decision_points.base import DecisionPoint + + +class CisaDecisionPoint(DecisionPoint, BaseModel): + namespace = "cisa" diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 74fc921b..08e58d62 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -73,6 +73,7 @@ class NameSpace(StrEnum): # when used in a StrEnum, auto() assigns the lowercase name of the member as the value SSVC = auto() CVSS = auto() + CISA = auto() @classmethod def validate(cls, value: str) -> str: diff --git a/src/ssvc/outcomes/cisa/__init__.py b/src/ssvc/outcomes/cisa/__init__.py new file mode 100644 index 00000000..8ad5f951 --- /dev/null +++ b/src/ssvc/outcomes/cisa/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the cisa namespace +""" diff --git a/src/ssvc/outcomes/cisa/scoring.py b/src/ssvc/outcomes/cisa/scoring.py new file mode 100644 index 00000000..6b64bac1 --- /dev/null +++ b/src/ssvc/outcomes/cisa/scoring.py @@ -0,0 +1,81 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the CISA Levels outcome group for use in SSVC. +""" + +from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint +from ssvc.decision_points.helpers import print_versions_and_diffs + +_TRACK = DecisionPointValue( + name="Track", + key="T", + description="The vulnerability does not require action at this time. " + "The organization would continue to track the vulnerability and reassess it if new information becomes available. " + "CISA recommends remediating Track vulnerabilities within standard update timelines.", +) + +_TRACK_STAR = DecisionPointValue( + name="Track*", + key="T*", + description="The vulnerability contains specific characteristics that may require closer monitoring for changes. " + "CISA recommends remediating Track* vulnerabilities within standard update timelines.", +) + +_ATTEND = DecisionPointValue( + name="Attend", + key="A", + description="The vulnerability requires attention from the organization's internal, supervisory-level individuals. " + "Necessary actions may include requesting assistance or information about the vulnerability and may involve publishing a notification, either internally and/or externally, about the vulnerability. " + "CISA recommends remediating Attend vulnerabilities sooner than standard update timelines.", +) + +_ACT = DecisionPointValue( + name="Act", + key="A", + description="The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. " + "Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. " + "Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. " + "CISA recommends remediating Act vulnerabilities as soon as possible.", +) + +CISA = CisaDecisionPoint( + name="CISA Levels", + key="CISA", + description="The CISA outcome group. " + "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", + version="1.0.0", + values=( + _TRACK, + _TRACK_STAR, + _ATTEND, + _ACT, + ), +) +""" +The CISA outcome group. Based on CISA's customizations of the SSVC model. +See https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc +""" + + +VERSIONS = (CISA,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index ee12cbf6..0df4121d 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -55,48 +55,6 @@ """ -CISA = SsvcDecisionPoint( - name="CISA Levels", - key="CISA", - description="The CISA outcome group. " - "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", - version="1.0.0", - values=( - DecisionPointValue( - name="Track", - key="T", - description="The vulnerability does not require action at this time. " - "The organization would continue to track the vulnerability and reassess it if new information becomes available. " - "CISA recommends remediating Track vulnerabilities within standard update timelines.", - ), - DecisionPointValue( - name="Track*", - key="T*", - description="The vulnerability contains specific characteristics that may require closer monitoring for changes. " - "CISA recommends remediating Track* vulnerabilities within standard update timelines.", - ), - DecisionPointValue( - name="Attend", - key="A", - description="The vulnerability requires attention from the organization's internal, supervisory-level individuals. " - "Necessary actions may include requesting assistance or information about the vulnerability and may involve publishing a notification, either internally and/or externally, about the vulnerability. " - "CISA recommends remediating Attend vulnerabilities sooner than standard update timelines.", - ), - DecisionPointValue( - name="Act", - key="A", - description="The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. " - "Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. " - "Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. " - "CISA recommends remediating Act vulnerabilities as soon as possible.", - ), - ), -) -""" -The CISA outcome group. Based on CISA's customizations of the SSVC model. -See https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc -""" - YES_NO = SsvcDecisionPoint( name="Yes, No", key="YES_NO", From 0bfb50ab949fc9b8dc472007204439e6b8ec1733 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 14:53:08 -0400 Subject: [PATCH 63/99] move basic outcome groups to the `x_basic` namespace --- src/ssvc/outcomes/groups.py | 84 +++---------------- src/ssvc/outcomes/x_basic/__init__.py | 18 ++++ src/ssvc/outcomes/x_basic/ike.py | 59 +++++++++++++ src/ssvc/outcomes/x_basic/mscw.py | 58 +++++++++++++ src/ssvc/outcomes/x_basic/value_complexity.py | 61 ++++++++++++++ src/ssvc/outcomes/x_basic/yn.py | 49 +++++++++++ 6 files changed, 258 insertions(+), 71 deletions(-) create mode 100644 src/ssvc/outcomes/x_basic/__init__.py create mode 100644 src/ssvc/outcomes/x_basic/ike.py create mode 100644 src/ssvc/outcomes/x_basic/mscw.py create mode 100644 src/ssvc/outcomes/x_basic/value_complexity.py create mode 100644 src/ssvc/outcomes/x_basic/yn.py diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 0df4121d..50b70b39 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -1,4 +1,5 @@ #!/usr/bin/env python + # Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project @@ -15,83 +16,20 @@ Provides a set of outcome groups for use in SSVC. """ -from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint - - -# Note: Outcome Groups must be defined in ascending order. - - -MOSCOW = SsvcDecisionPoint( - name="MoSCoW", - key="MSCW", - description="The MoSCoW (Must, Should, Could, Won't) outcome group.", - version="1.0.0", - values=( - DecisionPointValue(name="Won't", key="W", description="Won't"), - DecisionPointValue(name="Could", key="C", description="Could"), - DecisionPointValue(name="Should", key="S", description="Should"), - DecisionPointValue(name="Must", key="M", description="Must"), - ), +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, ) -""" -The MoSCoW outcome group. -""" - -EISENHOWER = SsvcDecisionPoint( - name="Do, Schedule, Delegate, Delete", - key="EISENHOWER", - description="The Eisenhower outcome group.", - version="1.0.0", - values=( - DecisionPointValue(name="Delete", key="D", description="Delete"), - DecisionPointValue(name="Delegate", key="G", description="Delegate"), - DecisionPointValue(name="Schedule", key="S", description="Schedule"), - DecisionPointValue(name="Do", key="O", description="Do"), - ), -) -""" -The Eisenhower outcome group. -""" - +from ssvc.decision_points.helpers import print_versions_and_diffs -YES_NO = SsvcDecisionPoint( - name="Yes, No", - key="YES_NO", - description="The Yes/No outcome group.", - version="1.0.0", - values=( - DecisionPointValue(name="No", key="N", description="No"), - DecisionPointValue(name="Yes", key="Y", description="Yes"), - ), -) -""" -The Yes/No outcome group. -""" +# Note: Outcome Groups must be defined in ascending order. -VALUE_COMPLEXITY = SsvcDecisionPoint( - name="Value, Complexity", - key="VALUE_COMPLEXITY", - description="The Value/Complexity outcome group.", - version="1.0.0", - values=( - # drop, reconsider later, easy win, do first - DecisionPointValue(name="Drop", key="D", description="Drop"), - DecisionPointValue( - name="Reconsider Later", key="R", description="Reconsider Later" - ), - DecisionPointValue(name="Easy Win", key="E", description="Easy Win"), - DecisionPointValue(name="Do First", key="F", description="Do First"), - ), -) -""" -The Value/Complexity outcome group. -""" -THE_PARANOIDS = SsvcDecisionPoint( +THE_PARANOIDS = DecisionPoint( name="theParanoids", key="PARANOIDS", description="PrioritizedRiskRemediation outcome group based on TheParanoids.", + namespace="x_community", version="1.0.0", values=( DecisionPointValue(name="Track 5", key="5", description="Track"), @@ -112,7 +50,11 @@ def main(): - pass + print_versions_and_diffs( + [ + THE_PARANOIDS, + ] + ) if __name__ == "__main__": diff --git a/src/ssvc/outcomes/x_basic/__init__.py b/src/ssvc/outcomes/x_basic/__init__.py new file mode 100644 index 00000000..f3f12820 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the `x_basic` namespace +""" + +from .mscw import MSCW +from .yn import YES_NO diff --git a/src/ssvc/outcomes/x_basic/ike.py b/src/ssvc/outcomes/x_basic/ike.py new file mode 100644 index 00000000..0a56e9ee --- /dev/null +++ b/src/ssvc/outcomes/x_basic/ike.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the Eisenhower outcome group for the `x_basic` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_DELETE = DecisionPointValue(name="Delete", key="D", description="Delete") + +_DELEGATE = DecisionPointValue(name="Delegate", key="G", description="Delegate") + +_SCHEDULE = DecisionPointValue(name="Schedule", key="S", description="Schedule") + +_DO = DecisionPointValue(name="Do", key="O", description="Do") + +EISENHOWER = DecisionPoint( + name="Do, Schedule, Delegate, Delete", + key="IKE", + description="The Eisenhower outcome group.", + namespace="x_basic", + version="1.0.0", + values=( + _DELETE, + _DELEGATE, + _SCHEDULE, + _DO, + ), +) +""" +The Eisenhower outcome group. +""" + +VERSIONS = (EISENHOWER,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/mscw.py b/src/ssvc/outcomes/x_basic/mscw.py new file mode 100644 index 00000000..b74ef04e --- /dev/null +++ b/src/ssvc/outcomes/x_basic/mscw.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the MSCW (Must, Should, Could, Won't) outcome group for the `x_basic` namespace +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_WONT = DecisionPointValue(name="Won't", key="W", description="Won't") + +_COULD = DecisionPointValue(name="Could", key="C", description="Could") + +_SHOULD = DecisionPointValue(name="Should", key="S", description="Should") + +_MUST = DecisionPointValue(name="Must", key="M", description="Must") + +MSCW = DecisionPoint( + name="MoSCoW", + key="MSCW", + description="The MoSCoW (Must, Should, Could, Won't) outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _WONT, + _COULD, + _SHOULD, + _MUST, + ), +) +""" +The MoSCoW outcome group. +""" + +VERSIONS = (MSCW,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/value_complexity.py b/src/ssvc/outcomes/x_basic/value_complexity.py new file mode 100644 index 00000000..08a61eb6 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/value_complexity.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the Value/Complexity outcome group for the `x_basic` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_DROP = DecisionPointValue(name="Drop", key="D", description="Drop") + +_RECONSIDER = DecisionPointValue( + name="Reconsider Later", key="R", description="Reconsider Later" +) + +_EASY_WIN = DecisionPointValue(name="Easy Win", key="E", description="Easy Win") + +_DO_FIRST = DecisionPointValue(name="Do First", key="F", description="Do First") + +VALUE_COMPLEXITY = DecisionPoint( + name="Value, Complexity", + key="VALUE_COMPLEXITY", + description="The Value/Complexity outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _DROP, + _RECONSIDER, + _EASY_WIN, + _DO_FIRST, + ), +) +""" +The Value/Complexity outcome group. +""" + +VERSIONS = (VALUE_COMPLEXITY,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/yn.py b/src/ssvc/outcomes/x_basic/yn.py new file mode 100644 index 00000000..39dddfb2 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/yn.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_NO = DecisionPointValue(name="No", key="N", description="No") + +_YES = DecisionPointValue(name="Yes", key="Y", description="Yes") + +YES_NO = DecisionPoint( + name="Yes, No", + key="YN", + description="The Yes/No outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _NO, + _YES, + ), +) +""" +The Yes/No outcome group. +""" + +VERSIONS = (YES_NO,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() From 4d29de9c706cd26bb05358a52a171775e9278fcd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 14:53:38 -0400 Subject: [PATCH 64/99] clean up format --- src/ssvc/outcomes/cvss/lmhc.py | 24 ++++++++++++++---------- src/ssvc/outcomes/ssvc_/coordinate.py | 12 +++++++++--- src/ssvc/outcomes/ssvc_/dsoi.py | 17 +++++++++++++---- src/ssvc/outcomes/ssvc_/publish.py | 11 +++++++---- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/ssvc/outcomes/cvss/lmhc.py b/src/ssvc/outcomes/cvss/lmhc.py index 803be31c..673c9794 100644 --- a/src/ssvc/outcomes/cvss/lmhc.py +++ b/src/ssvc/outcomes/cvss/lmhc.py @@ -14,11 +14,15 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -CVSS_NONE = DecisionPointValue(name="None", key="N", description="None (0.0)") -CVSS_LOW = DecisionPointValue(name="Low", key="L", description="Low (0.1-3.9)") -CVSS_MEDIUM = DecisionPointValue(name="Medium", key="M", description="Medium (4.0-6.9)") -CVSS_HIGH = DecisionPointValue(name="High", key="H", description="High (7.0-8.9)") -CVSS_CRITICAL = DecisionPointValue( +_NONE = DecisionPointValue(name="None", key="N", description="None (0.0)") + +_LOW = DecisionPointValue(name="Low", key="L", description="Low (0.1-3.9)") + +_MEDIUM = DecisionPointValue(name="Medium", key="M", description="Medium (4.0-6.9)") + +_HIGH = DecisionPointValue(name="High", key="H", description="High (7.0-8.9)") + +_CRITICAL = DecisionPointValue( name="Critical", key="C", description="Critical (9.0-10.0)" ) @@ -28,11 +32,11 @@ description="The CVSS Qualitative Severity Rating Scale group.", version="1.0.0", values=( - CVSS_NONE, - CVSS_LOW, - CVSS_MEDIUM, - CVSS_HIGH, - CVSS_CRITICAL, + _NONE, + _LOW, + _MEDIUM, + _HIGH, + _CRITICAL, ), ) """ diff --git a/src/ssvc/outcomes/ssvc_/coordinate.py b/src/ssvc/outcomes/ssvc_/coordinate.py index 1baacdd2..f1c8259c 100644 --- a/src/ssvc/outcomes/ssvc_/coordinate.py +++ b/src/ssvc/outcomes/ssvc_/coordinate.py @@ -14,15 +14,21 @@ from ssvc.decision_points.helpers import print_versions_and_diffs from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +_DECLINE = DecisionPointValue(name="Decline", key="D", description="Decline") + +_TRACK = DecisionPointValue(name="Track", key="T", description="Track") + +_COORDINATE = DecisionPointValue(name="Coordinate", key="C", description="Coordinate") + COORDINATE = SsvcDecisionPoint( name="Decline, Track, Coordinate", key="COORDINATE", description="The coordinate outcome group.", version="1.0.0", values=( - DecisionPointValue(name="Decline", key="D", description="Decline"), - DecisionPointValue(name="Track", key="T", description="Track"), - DecisionPointValue(name="Coordinate", key="C", description="Coordinate"), + _DECLINE, + _TRACK, + _COORDINATE, ), ) """ diff --git a/src/ssvc/outcomes/ssvc_/dsoi.py b/src/ssvc/outcomes/ssvc_/dsoi.py index 510609c9..297c3f56 100644 --- a/src/ssvc/outcomes/ssvc_/dsoi.py +++ b/src/ssvc/outcomes/ssvc_/dsoi.py @@ -19,6 +19,15 @@ from ssvc.decision_points.helpers import print_versions_and_diffs from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +_DEFER = DecisionPointValue(name="Defer", key="D", description="Defer") + +_SCHEDULED = DecisionPointValue(name="Scheduled", key="S", description="Scheduled") + +_OUT_OF_CYCLE = DecisionPointValue( + name="Out-of-Cycle", key="O", description="Out-of-Cycle" +) + +_IMMEDIATE = DecisionPointValue(name="Immediate", key="I", description="Immediate") DSOI = SsvcDecisionPoint( name="Defer, Scheduled, Out-of-Cycle, Immediate", @@ -26,10 +35,10 @@ description="The original SSVC outcome group.", version="1.0.0", values=( - DecisionPointValue(name="Defer", key="D", description="Defer"), - DecisionPointValue(name="Scheduled", key="S", description="Scheduled"), - DecisionPointValue(name="Out-of-Cycle", key="O", description="Out-of-Cycle"), - DecisionPointValue(name="Immediate", key="I", description="Immediate"), + _DEFER, + _SCHEDULED, + _OUT_OF_CYCLE, + _IMMEDIATE, ), ) """ diff --git a/src/ssvc/outcomes/ssvc_/publish.py b/src/ssvc/outcomes/ssvc_/publish.py index ef90c278..58311b6e 100644 --- a/src/ssvc/outcomes/ssvc_/publish.py +++ b/src/ssvc/outcomes/ssvc_/publish.py @@ -17,6 +17,11 @@ from ssvc.decision_points.helpers import print_versions_and_diffs from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +_DO_NOT_PUBLISH = DecisionPointValue( + name="Do Not Publish", key="N", description="Do Not Publish" +) + +_PUBLISH = DecisionPointValue(name="Publish", key="P", description="Publish") PUBLISH = SsvcDecisionPoint( name="Publish, Do Not Publish", @@ -24,10 +29,8 @@ description="The publish outcome group.", version="1.0.0", values=( - DecisionPointValue( - name="Do Not Publish", key="N", description="Do Not Publish" - ), - DecisionPointValue(name="Publish", key="P", description="Publish"), + _DO_NOT_PUBLISH, + _PUBLISH, ), ) """ From 4a0f2deb985fbb939f3b68efa5a416706494479a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 15:06:20 -0400 Subject: [PATCH 65/99] add `x_community` namespace for theParanoids outcome group --- src/ssvc/outcomes/groups.py | 29 +-------- src/ssvc/outcomes/x_community/__init__.py | 17 ++++++ src/ssvc/outcomes/x_community/paranoids.py | 70 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 src/ssvc/outcomes/x_community/__init__.py create mode 100644 src/ssvc/outcomes/x_community/paranoids.py diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index 50b70b39..34f5a3f0 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -16,39 +16,12 @@ Provides a set of outcome groups for use in SSVC. """ -from ssvc.decision_points.base import ( - DecisionPoint, - DecisionPointValue as DecisionPointValue, -) from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.outcomes.x_community.paranoids import THE_PARANOIDS # Note: Outcome Groups must be defined in ascending order. -THE_PARANOIDS = DecisionPoint( - name="theParanoids", - key="PARANOIDS", - description="PrioritizedRiskRemediation outcome group based on TheParanoids.", - namespace="x_community", - version="1.0.0", - values=( - DecisionPointValue(name="Track 5", key="5", description="Track"), - DecisionPointValue( - name="Track Closely 4", key="4", description="Track Closely" - ), - DecisionPointValue(name="Attend 3", key="3", description="Attend"), - DecisionPointValue(name="Attend 2", key="2", description="Attend"), - DecisionPointValue(name="Act 1", key="1", description="Act"), - DecisionPointValue(name="Act ASAP 0", key="0", description="Act ASAP"), - ), -) -""" -Outcome group based on TheParanoids' PrioritizedRiskRemediation. -Their model is a 6-point scale, with 0 being the most urgent and 5 being the least. -See https://github.com/theparanoids/PrioritizedRiskRemediation -""" - - def main(): print_versions_and_diffs( [ diff --git a/src/ssvc/outcomes/x_community/__init__.py b/src/ssvc/outcomes/x_community/__init__.py new file mode 100644 index 00000000..215c5355 --- /dev/null +++ b/src/ssvc/outcomes/x_community/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the `x_community` namespace. +""" + +from .paranoids import LATEST as THE_PARANOIDS diff --git a/src/ssvc/outcomes/x_community/paranoids.py b/src/ssvc/outcomes/x_community/paranoids.py new file mode 100644 index 00000000..2a0f7f8d --- /dev/null +++ b/src/ssvc/outcomes/x_community/paranoids.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +""" +Provides a decision point for the `x_community` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_TRACK_5 = DecisionPointValue(name="Track 5", key="5", description="Track") + +_TRACK_CLOSELY_4 = DecisionPointValue( + name="Track Closely 4", key="4", description="Track Closely" +) + +_ATTEND_3 = DecisionPointValue(name="Attend 3", key="3", description="Attend") + +_ATTEND_2 = DecisionPointValue(name="Attend 2", key="2", description="Attend") + +_ACT_1 = DecisionPointValue(name="Act 1", key="1", description="Act") + +_ACT_ASAP_0 = DecisionPointValue(name="Act ASAP 0", key="0", description="Act ASAP") + +THE_PARANOIDS = DecisionPoint( + name="theParanoids", + key="PARANOIDS", + description="PrioritizedRiskRemediation outcome group based on TheParanoids.", + namespace="x_community", + version="1.0.0", + values=( + _TRACK_5, + _TRACK_CLOSELY_4, + _ATTEND_3, + _ATTEND_2, + _ACT_1, + _ACT_ASAP_0, + ), +) +""" +Outcome group based on TheParanoids' PrioritizedRiskRemediation. +Their model is a 6-point scale, with 0 being the most urgent and 5 being the least. +See https://github.com/theparanoids/PrioritizedRiskRemediation +""" + +VERSIONS = (THE_PARANOIDS,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() From 9be0d3cf344a54c58137a57807a2cd0b0f52b1df Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 15:07:03 -0400 Subject: [PATCH 66/99] add LATEST imports to module `__init__.py` files --- src/ssvc/outcomes/__init__.py | 12 ++++++++++++ src/ssvc/outcomes/cisa/__init__.py | 1 + src/ssvc/outcomes/cvss/__init__.py | 1 + src/ssvc/outcomes/ssvc_/__init__.py | 4 ++++ src/ssvc/outcomes/x_basic/__init__.py | 6 ++++-- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ssvc/outcomes/__init__.py b/src/ssvc/outcomes/__init__.py index 37d9487d..a918a5bd 100644 --- a/src/ssvc/outcomes/__init__.py +++ b/src/ssvc/outcomes/__init__.py @@ -10,3 +10,15 @@ # (“Third Party Software”). See LICENSE.md for more details. # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome group objects. + +SSVC outcome groups are functionally equivalent to Decision Points. +The only difference is that Outcome Groups are primarily intended to be used +as the outputs of a decision, whereas Decision Points are the inputs to a decision. +However, there are use cases where an outcome of one decision may feed into another +decision, so the distinction is somewhat arbitrary. Hence, we chose to use the same +data structure for both. + +Outcome groups are organized by namespace. +""" diff --git a/src/ssvc/outcomes/cisa/__init__.py b/src/ssvc/outcomes/cisa/__init__.py index 8ad5f951..ae8acaa4 100644 --- a/src/ssvc/outcomes/cisa/__init__.py +++ b/src/ssvc/outcomes/cisa/__init__.py @@ -13,3 +13,4 @@ """ Provides SSVC outcome groups for the cisa namespace """ +from .scoring import LATEST as CISA_SCORING diff --git a/src/ssvc/outcomes/cvss/__init__.py b/src/ssvc/outcomes/cvss/__init__.py index 02ead02b..6f4e34fc 100644 --- a/src/ssvc/outcomes/cvss/__init__.py +++ b/src/ssvc/outcomes/cvss/__init__.py @@ -13,3 +13,4 @@ """ Provides outcome group objects in the cvss namespace """ +from .lmhc import LATEST as LMHC diff --git a/src/ssvc/outcomes/ssvc_/__init__.py b/src/ssvc/outcomes/ssvc_/__init__.py index f9dba018..60041bf3 100644 --- a/src/ssvc/outcomes/ssvc_/__init__.py +++ b/src/ssvc/outcomes/ssvc_/__init__.py @@ -13,3 +13,7 @@ """ Provides outcome group objects in the ssvc namespace """ + +from .coordinate import LATEST as COORDINATE +from .dsoi import LATEST as DSOI +from .publish import LATEST as PUBLISH diff --git a/src/ssvc/outcomes/x_basic/__init__.py b/src/ssvc/outcomes/x_basic/__init__.py index f3f12820..73ac8ef9 100644 --- a/src/ssvc/outcomes/x_basic/__init__.py +++ b/src/ssvc/outcomes/x_basic/__init__.py @@ -14,5 +14,7 @@ Provides SSVC outcome groups for the `x_basic` namespace """ -from .mscw import MSCW -from .yn import YES_NO +from .ike import LATEST as EISENHOWER +from .mscw import LATEST as MSCW +from .value_complexity import LATEST as VALUE_COMPLEXITY +from .yn import LATEST as YES_NO From 45bb8b58f219e57cb36539a250265d55ed543c69 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 15:48:52 -0400 Subject: [PATCH 67/99] simplify dp_groups --- src/ssvc/dp_groups/base.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index 664644f9..133122bc 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -17,15 +17,11 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University -from itertools import product -from typing import Generator - from pydantic import BaseModel from ssvc._mixins import _Base, _SchemaVersioned from ssvc.decision_points.base import ( DecisionPoint, - ValueSummary, ) from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint @@ -47,9 +43,7 @@ def __len__(self): """ Allow len() to be called on the group. """ - dplist = list(self.decision_points) - l = len(dplist) - return l + return len(self.decision_points) @property def decision_points_dict(self) -> dict[str, DecisionPoint]: @@ -65,30 +59,9 @@ def decision_points_str(self) -> list[str]: """ return list(self.decision_points_dict.keys()) - def combination_summaries(self) -> Generator[tuple[ValueSummary, ...], None, None]: - # get the value summaries for each decision point - value_summaries = [dp.value_summaries for dp in self.decision_points] - - for combination in product(*value_summaries): - yield combination - - def combination_strings(self) -> Generator[tuple[str, ...], None, None]: - """ - Produce all possible combinations of decision point values in the group as strings. - """ - for combo in self.combination_summaries(): - yield tuple(str(v) for v in combo) - - def combination_dicts(self): - """ - Produce all possible combinations of decision point values in the group as a dictionary. - """ - for combo in self.combination_summaries(): - yield tuple(v.model_dump() for v in combo) - def get_all_decision_points_from( - *groups: list[SsvcDecisionPointGroup], + *groups: list[DecisionPointGroup], ) -> tuple[SsvcDecisionPoint, ...]: """ Given a list of SsvcDecisionPointGroup objects, return a list of all From 1584e56279eb4c8bdbcaa06d2819de836e1927f1 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 15:49:40 -0400 Subject: [PATCH 68/99] rename SsvcDecisionPointGroup to DecisionPointGroup --- src/ssvc/dp_groups/base.py | 2 +- src/ssvc/dp_groups/cvss/collections.py | 32 +++++++++---------- src/ssvc/dp_groups/ssvc/collections.py | 8 ++--- .../dp_groups/ssvc/coordinator_publication.py | 4 +-- src/ssvc/dp_groups/ssvc/coordinator_triage.py | 4 +-- src/ssvc/dp_groups/ssvc/deployer.py | 8 ++--- src/ssvc/dp_groups/ssvc/supplier.py | 6 ++-- src/ssvc/policy_generator.py | 8 ++--- src/test/dp_groups/test_dp_groups.py | 14 ++++---- src/test/test_policy_generator.py | 4 +-- 10 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index 133122bc..1698d9cf 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -26,7 +26,7 @@ from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -class SsvcDecisionPointGroup(_Base, _SchemaVersioned, BaseModel): +class DecisionPointGroup(_Base, _SchemaVersioned, BaseModel): """ Models a group of decision points. """ diff --git a/src/ssvc/dp_groups/cvss/collections.py b/src/ssvc/dp_groups/cvss/collections.py index 55e3c300..7fb31fdb 100644 --- a/src/ssvc/dp_groups/cvss/collections.py +++ b/src/ssvc/dp_groups/cvss/collections.py @@ -123,7 +123,7 @@ USER_INTERACTION_1, USER_INTERACTION_2, ) -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup # Instantiate the CVSS v1 decision point group _BASE_1 = [ @@ -145,21 +145,21 @@ TARGET_DISTRIBUTION_1, ] -CVSSv1_B = SsvcDecisionPointGroup( +CVSSv1_B = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", decision_points=tuple(_BASE_1), ) -CVSSv1_BT = SsvcDecisionPointGroup( +CVSSv1_BT = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", decision_points=tuple(_BASE_1 + _TEMPORAL_1), ) -CVSSv1_BTE = SsvcDecisionPointGroup( +CVSSv1_BTE = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", @@ -191,21 +191,21 @@ AVAILABILITY_REQUIREMENT_1, ] -CVSSv2_B = SsvcDecisionPointGroup( +CVSSv2_B = DecisionPointGroup( name="CVSS Version 2 Base Metrics", description="Base metrics for CVSS v2", version="2.0", decision_points=tuple(_BASE_2), ) -CVSSv2_BT = SsvcDecisionPointGroup( +CVSSv2_BT = DecisionPointGroup( name="CVSS Version 2 Base and Temporal Metrics", description="Base and Temporal metrics for CVSS v2", version="2.0", decision_points=tuple(_BASE_2 + _TEMPORAL_2), ) -CVSSv2_BTE = SsvcDecisionPointGroup( +CVSSv2_BTE = DecisionPointGroup( name="CVSS Version 2 Base, Temporal, and Environmental Metrics", description="Base, Temporal, and Environmental metrics for CVSS v2", version="2.0", @@ -240,21 +240,21 @@ ] ) -CVSSv3_B = SsvcDecisionPointGroup( +CVSSv3_B = DecisionPointGroup( name="CVSS Version 3 Base Metrics", description="Base metrics for CVSS v3", version="3.0", decision_points=tuple(_BASE_3), ) -CVSSv3_BT = SsvcDecisionPointGroup( +CVSSv3_BT = DecisionPointGroup( name="CVSS Version 3 Base and Temporal Metrics", description="Base and Temporal metrics for CVSS v3", version="3.0", decision_points=tuple(_BASE_3 + _TEMPORAL_3), ) -CVSSv3_BTE = SsvcDecisionPointGroup( +CVSSv3_BTE = DecisionPointGroup( name="CVSS Version 3 Base, Temporal, and Environmental Metrics", description="Base, Temporal, and Environmental metrics for CVSS v3", version="3.0", @@ -304,7 +304,7 @@ VULNERABILITY_RESPONSE_EFFORT_1, ] # CVSS-B Base metrics -CVSSv4_B = SsvcDecisionPointGroup( +CVSSv4_B = DecisionPointGroup( name="CVSSv4 Base Metrics", description="Base metrics for CVSS v4", version="1.0.0", @@ -312,7 +312,7 @@ ) # CVSS-BE Base and Environmental metrics -CVSSv4_BE = SsvcDecisionPointGroup( +CVSSv4_BE = DecisionPointGroup( name="CVSSv4 Base and Environmental Metrics", description="Base and Environmental metrics for CVSS v4", version="1.0.0", @@ -320,7 +320,7 @@ ) # CVSS-BT Base and Threat metrics -CVSSv4_BT = SsvcDecisionPointGroup( +CVSSv4_BT = DecisionPointGroup( name="CVSSv4 Base and Threat Metrics", description="Base and Threat metrics for CVSS v4", version="1.0.0", @@ -328,21 +328,21 @@ ) # CVSS-BTE -CVSSv4_BTE = SsvcDecisionPointGroup( +CVSSv4_BTE = DecisionPointGroup( name="CVSSv4 Base, Threat, and Environmental Metrics", description="Base, Threat, and Environmental metrics for CVSS v4", version="1.0.0", decision_points=tuple(_BASE_4 + _THREAT_4 + _ENVIRONMENTAL_4), ) -CVSSv4 = SsvcDecisionPointGroup( +CVSSv4 = DecisionPointGroup( name="CVSSv4", description="All decision points for CVSS v4 (including supplemental metrics)", version="1.0.0", decision_points=tuple(_BASE_4 + _THREAT_4 + _ENVIRONMENTAL_4 + _SUPPLEMENTAL_4), ) -CVSSv4_Equivalence_Sets = SsvcDecisionPointGroup( +CVSSv4_Equivalence_Sets = DecisionPointGroup( name="CVSSv4 EQ Sets", description="Equivalence Sets for CVSS v4", version="1.0.0", diff --git a/src/ssvc/dp_groups/ssvc/collections.py b/src/ssvc/dp_groups/ssvc/collections.py index e106ff5a..f4475652 100644 --- a/src/ssvc/dp_groups/ssvc/collections.py +++ b/src/ssvc/dp_groups/ssvc/collections.py @@ -17,7 +17,7 @@ from ssvc.dp_groups.base import ( - SsvcDecisionPointGroup, + DecisionPointGroup, get_all_decision_points_from, ) from ssvc.dp_groups.ssvc.coordinator_publication import ( @@ -32,7 +32,7 @@ from ssvc.dp_groups.ssvc.supplier import PATCH_DEVELOPER_1, SUPPLIER_2 -SSVCv1 = SsvcDecisionPointGroup( +SSVCv1 = DecisionPointGroup( name="SSVCv1", description="The first version of the SSVC.", version="1.0.0", @@ -40,7 +40,7 @@ PATCH_APPLIER_1, PATCH_DEVELOPER_1 ), ) -SSVCv2 = SsvcDecisionPointGroup( +SSVCv2 = DecisionPointGroup( name="SSVCv2", description="The second version of the SSVC.", version="2.0.0", @@ -48,7 +48,7 @@ COORDINATOR_PUBLICATION_1, COORDINATOR_TRIAGE_1, DEPLOYER_2, SUPPLIER_2 ), ) -SSVCv2_1 = SsvcDecisionPointGroup( +SSVCv2_1 = DecisionPointGroup( name="SSVCv2.1", description="The second version of the SSVC.", version="2.1.0", diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index d46eccae..d290c686 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -20,10 +20,10 @@ from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 from ssvc.decision_points.ssvc_.public_value_added import PUBLIC_VALUE_ADDED_1 from ssvc.decision_points.ssvc_.supplier_involvement import SUPPLIER_INVOLVEMENT_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup -COORDINATOR_PUBLICATION_1 = SsvcDecisionPointGroup( +COORDINATOR_PUBLICATION_1 = DecisionPointGroup( name="Coordinator Publication", description="The decision points used by the coordinator during publication.", version="1.0.0", diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index af09a56b..d7d96ceb 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -27,9 +27,9 @@ from ssvc.decision_points.ssvc_.supplier_engagement import SUPPLIER_ENGAGEMENT_1 from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup -COORDINATOR_TRIAGE_1 = SsvcDecisionPointGroup( +COORDINATOR_TRIAGE_1 = DecisionPointGroup( name="Coordinator Triage", description="The decision points used by the coordinator during triage.", version="1.0.0", diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index 341026fa..bc84c23b 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -32,9 +32,9 @@ ) from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup -PATCH_APPLIER_1 = SsvcDecisionPointGroup( +PATCH_APPLIER_1 = DecisionPointGroup( name="SSVC Patch Applier", description="The decision points used by the patch applier.", version="1.0.0", @@ -60,7 +60,7 @@ DEPLOYER_1 = PATCH_APPLIER_1 # SSVC v2 -DEPLOYER_2 = SsvcDecisionPointGroup( +DEPLOYER_2 = DecisionPointGroup( name="SSVC Deployer", description="The decision points used by the deployer.", version="2.0.0", @@ -94,7 +94,7 @@ - Human Impact v1.0.0 is added, which depends on Mission Impact v1.0.0 and Safety Impact v1.0.0 """ -DEPLOYER_3 = SsvcDecisionPointGroup( +DEPLOYER_3 = DecisionPointGroup( name="SSVC Deployer", description="The decision points used by the deployer.", version="3.0.0", diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index 1268fd8f..eddeafce 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -24,9 +24,9 @@ from ssvc.decision_points.ssvc_.technical_impact import TECHNICAL_IMPACT_1 from ssvc.decision_points.ssvc_.utility import UTILITY_1, UTILITY_1_0_1 from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup -PATCH_DEVELOPER_1 = SsvcDecisionPointGroup( +PATCH_DEVELOPER_1 = DecisionPointGroup( name="SSVC Patch Developer", description="The decision points used by the patch developer.", version="1.0.0", @@ -56,7 +56,7 @@ SUPPLIER_1 = PATCH_DEVELOPER_1 # SSVC v2 renamed to SSVC Supplier -SUPPLIER_2 = SsvcDecisionPointGroup( +SUPPLIER_2 = DecisionPointGroup( name="SSVC Supplier", description="The decision points used by the supplier.", version="2.0.0", diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index d03d5ad8..8e235ab4 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -24,7 +24,7 @@ import pandas as pd from ssvc import csv_analyzer -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup from ssvc.outcomes.base import OutcomeGroup logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class PolicyGenerator: def __init__( self, - dp_group: SsvcDecisionPointGroup, + dp_group: DecisionPointGroup, outcomes: OutcomeGroup, outcome_weights: list[float] = None, validate: bool = False, @@ -66,7 +66,7 @@ def __init__( if dp_group is None: raise ValueError("dp_group is required") else: - self.dpg: SsvcDecisionPointGroup = dp_group + self.dpg: DecisionPointGroup = dp_group if outcomes is None: raise ValueError("outcomes is required") @@ -342,7 +342,7 @@ def main(): hdlr = logging.StreamHandler() logger.addHandler(hdlr) - dpg = SsvcDecisionPointGroup( + dpg = DecisionPointGroup( name="Dummy Decision Point Group", description="Dummy decision point group", version="1.0.0", diff --git a/src/test/dp_groups/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py index e7612efa..fed3dc76 100644 --- a/src/test/dp_groups/test_dp_groups.py +++ b/src/test/dp_groups/test_dp_groups.py @@ -40,7 +40,7 @@ def tearDown(self) -> None: def test_iter(self): # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, @@ -54,7 +54,7 @@ def test_iter(self): def test_len(self): # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, @@ -66,7 +66,7 @@ def test_len(self): def test_combo_strings(self): # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, @@ -105,7 +105,7 @@ def test_combo_strings(self): def test_json_roundtrip(self): # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, @@ -115,13 +115,13 @@ def test_json_roundtrip(self): g_json = g.model_dump_json() # deserialize the json to a new group - g2 = dpg.SsvcDecisionPointGroup.model_validate_json(g_json) + g2 = dpg.DecisionPointGroup.model_validate_json(g_json) # assert that the new group is the same as the old group self.assertEqual(g_json, g2.model_dump_json()) def test_decision_points_dict(self): # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, @@ -139,7 +139,7 @@ def test_decision_points_dict(self): self.assertEqual(dp, dp_dict[dp.str]) def test_decision_points_str(self): - g = dpg.SsvcDecisionPointGroup( + g = dpg.DecisionPointGroup( name="Test Group", description="Test Group", decision_points=self.dps, diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index b7dbffcb..77246271 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -19,7 +19,7 @@ import pandas as pd from ssvc.decision_points.base import DecisionPoint, DecisionPointValue -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup from ssvc.policy_generator import PolicyGenerator @@ -41,7 +41,7 @@ def setUp(self) -> None: ] ), ) - self.dpg = SsvcDecisionPointGroup( + self.dpg = DecisionPointGroup( name="test", description="test", decision_points=tuple( From 6605ca697ffe4c2ad28acdbb49c716571f26462a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 15:50:07 -0400 Subject: [PATCH 69/99] simplify table in prep for using new decision point and group features --- src/ssvc/decision_tables/base.py | 153 ++---------------- .../decision_tables/test_decision_table.py | 90 ++--------- 2 files changed, 27 insertions(+), 216 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 15871beb..20419528 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -17,18 +17,12 @@ """ import logging import re -from typing import Self -import pandas as pd -from pydantic import BaseModel, model_validator +from pydantic import BaseModel from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned -from ssvc.csv_analyzer import check_topological_order -from ssvc.decision_points.base import DecisionPointValue -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint -from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue -from ssvc.policy_generator import PolicyGenerator +from ssvc.dp_groups.base import DecisionPointGroup +from ssvc.outcomes.base import OutcomeGroup logger = logging.getLogger(__name__) @@ -53,139 +47,12 @@ class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel) The mapping dict values are outcomes. """ - decision_point_group: SsvcDecisionPointGroup + decision_point_group: DecisionPointGroup outcome_group: OutcomeGroup - mapping: list[dict[str, str]] = None - - _df: pd.DataFrame = None - - @property - def outcome_lookup(self) -> dict[str, OutcomeValue]: - """ - Return a lookup table for outcomes. - - Returns: - dict: A dictionary of outcomes keyed by outcome value name - """ - return { - name_to_key(outcome.name): outcome for outcome in self.outcome_group.values - } - - @property - def dp_lookup(self) -> dict[str, SsvcDecisionPoint]: - """ - Return a lookup table for decision points. - - Returns: - dict: A dictionary of decision points keyed by decision point name - """ - return { - name_to_key(dp.name): dp for dp in self.decision_point_group.decision_points - } - - @property - def dp_value_lookup(self) -> dict[str, dict[str, DecisionPointValue]]: - """ - Return a lookup table for decision point values. - Returns: - dict: A dictionary of decision point values keyed by decision point name and value name - """ - dp_value_lookup = {} - for dp in self.decision_point_group.decision_points: - key1 = name_to_key(dp.name) - dp_value_lookup[key1] = {} - for dp_value in dp.values: - key2 = name_to_key(dp_value.name) - dp_value_lookup[key1][key2] = dp_value - return dp_value_lookup - - @model_validator(mode="after") - def _populate_df(self) -> Self: - if self._df is None: - self._df = self.generate_df() - return self - - @model_validator(mode="after") - def validate_mapping(self): - """ - Placeholder for validating the mapping. - """ - df = self._df - target = name_to_key(df.columns[-1]) - - problems: list = check_topological_order(df, target) - - if problems: - raise ValueError(f"Mapping has problems: {problems}") - else: - logger.debug("Mapping passes topological order check") - - return self - - @model_validator(mode="after") - def _populate_mapping(self) -> Self: - """ - Populate the mapping if it is not provided. - Args: - data: - - Returns: - - """ - if not self.mapping: - mapping = self.table_to_mapping(self._df) - self.mapping = mapping - return self - - def as_csv(self) -> str: - """ - Convert the mapping to a CSV string. - """ - raise NotImplementedError - - def as_df(self) -> pd.DataFrame: - """ - Convert the mapping to a pandas DataFrame. - """ - raise NotImplementedError - - # stub for validating mapping - def generate_df(self) -> pd.DataFrame: - """ - Populate the mapping with all possible combinations of decision points. - """ - with PolicyGenerator( - dp_group=self.decision_point_group, - outcomes=self.outcome_group, - ) as policy: - df: pd.DataFrame = policy.clean_policy() - - return df - - def table_to_mapping(self, df: pd.DataFrame) -> list[dict[str, str]]: - # copy dataframe - df = pd.DataFrame(df) - columns = [dp.key for dp in self.decision_point_group.decision_points] - columns.append(self.outcome_group.key) - - df.columns = columns - data = [] - for _, row in df.iterrows(): - row_data = {} - outcome_value = None - for column in columns: - value_name = row[column] - try: - value = self.dp_value_lookup[column][value_name] - row_data[column] = value.key - except KeyError: - outcome_value = self.outcome_lookup[value_name] - if outcome_value is None: - raise ValueError("Outcome value not found") - - row_data["outcome"] = outcome_value.key - data.append(row_data) - return data + + def combinations(self): + """Generate possible decision point values""" + return self.decision_point_group.combination_strings() # convenience alias @@ -194,7 +61,7 @@ def table_to_mapping(self, df: pd.DataFrame) -> list[dict[str, str]]: def main(): from ssvc.dp_groups.ssvc.supplier import LATEST as dpg - from ssvc.outcomes.groups import MOSCOW as og + from ssvc.outcomes.x_basic.mscw import MSCW as og logger = logging.getLogger() logger.setLevel(logging.DEBUG) @@ -210,7 +77,7 @@ def main(): ) print(dt.model_dump_json(indent=2)) - print(dt._df) + print(list(dt.combinations())) if __name__ == "__main__": diff --git a/src/test/decision_tables/test_decision_table.py b/src/test/decision_tables/test_decision_table.py index d923a64a..5557b0ce 100644 --- a/src/test/decision_tables/test_decision_table.py +++ b/src/test/decision_tables/test_decision_table.py @@ -12,18 +12,14 @@ # U.S. Patent and Trademark Office by Carnegie Mellon University import tempfile import unittest -from itertools import product -import pandas as pd - -import ssvc.decision_points.ssvc_.base +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_tables import base -from ssvc.decision_tables.base import name_to_key -from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue +from ssvc.dp_groups.base import DecisionPointGroup +from ssvc.outcomes.base import OutcomeValue -class MyTestCase(unittest.TestCase): +class TestDecisionTables(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.tempdir_path = self.tempdir.name @@ -32,14 +28,14 @@ def setUp(self): for i in range(3): dpvs = [] for j in range(3): - dpv = base.DecisionPointValue( + dpv = DecisionPointValue( name=f"Value {i}{j}", key=f"DP{i}V{j}", description=f"Decision Point {i} Value {j} Description", ) dpvs.append(dpv) - dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( + dp = DecisionPoint( name=f"Decision Point {i}", key=f"DP{i}", description=f"Decision Point {i} Description", @@ -49,7 +45,7 @@ def setUp(self): ) dps.append(dp) - self.dpg = SsvcDecisionPointGroup( + self.dpg = DecisionPointGroup( name="Decision Point Group", description="Decision Point Group description", decision_points=tuple(dps), @@ -59,15 +55,16 @@ def setUp(self): for i in range(3): ogv = OutcomeValue( name=f"Outcome Value {i}", - key=f"ov{i}", + key=f"OV{i}", description=f"Outcome Value {i} description", ) ogvs.append(ogv) - self.og = OutcomeGroup( + self.og = DecisionPoint( name="Outcome Group", key="OG", description="Outcome Group description", + namespace="x_test", values=tuple(ogvs), ) @@ -82,66 +79,13 @@ def setUp(self): def tearDown(self): self.tempdir.cleanup() - def test_outcome_lookup(self): - d = self.dt.outcome_lookup - self.assertEqual(len(d), len(self.og.values)) - - for i, v in enumerate(self.og.values): - vname = name_to_key(v.name) - self.assertEqual(d[vname], v) - - def test_dp_lookup(self): - d = self.dt.dp_lookup - self.assertEqual(len(d), len(self.dpg.decision_points)) - - for i, dp in enumerate(self.dpg.decision_points): - dpname = name_to_key(dp.name) - self.assertEqual(d[dpname], dp) - - def test_dp_value_lookup(self): - d = self.dt.dp_value_lookup - for dp in self.dpg.decision_points: - dpname = name_to_key(dp.name) - self.assertEqual(len(d[dpname]), len(dp.values)) - - for i, v in enumerate(dp.values): - vname = name_to_key(v.name) - self.assertEqual(d[dpname][vname], v) - - def test_populate_df(self): - with self.subTest("df is set, no change"): - data = { - "a": [1, 2, 3], - "b": [4, 5, 6], - "c": [7, 8, 9], - } - df = pd.DataFrame(data) - self.dt._df = df - self.dt._populate_df() - self.assertTrue(df.equals(self.dt._df)) - - with self.subTest("df is None, populate"): - self.dt._df = None - self.dt._populate_df() - self.assertFalse(df.equals(self.dt._df)) - self.assertIsNotNone(self.dt._df) - self.assertIsInstance(self.dt._df, pd.DataFrame) - - with self.subTest("check df contents"): - nrows = len(list(product(*[dp.values for dp in self.dpg.decision_points]))) - self.assertEqual(len(self.dt._df), nrows) - ncols = len(self.dpg.decision_points) + 1 - self.assertEqual(len(self.dt._df.columns), ncols) - - def test_validate_mapping(self): - with self.subTest("no problems"): - self.dt.validate_mapping() - - with self.subTest("problems"): - # set one of the outcomes out of order - self.dt._df.iloc[0, -1] = self.og.values[-1].name - with self.assertRaises(ValueError): - self.dt.validate_mapping() + def test_create(self): + # self.dt exists in setUp + self.assertEqual(self.dt.name, "foo") + self.assertEqual(self.dt.description, "foo description") + self.assertEqual(self.dt.namespace, "x_test") + self.assertEqual(self.dt.decision_point_group, self.dpg) + self.assertEqual(self.dt.outcome_group, self.og) if __name__ == "__main__": From 0752ad94bfb49b0a4d8f5d74d24af55b081a88f3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 16:16:53 -0400 Subject: [PATCH 70/99] black reformat --- src/ssvc/dp_groups/ssvc/coordinator_publication.py | 4 +--- src/ssvc/dp_groups/ssvc/coordinator_triage.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index d290c686..5dded08b 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -43,9 +43,7 @@ - Public Value Added v1.0.0 """ -VERSIONS = ( - COORDINATOR_PUBLICATION_1, -) +VERSIONS = (COORDINATOR_PUBLICATION_1,) LATEST = VERSIONS[-1] diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index d7d96ceb..87867370 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -63,11 +63,10 @@ - Safety Impact v1.0.0 """ -VERSIONS = ( - COORDINATOR_TRIAGE_1, -) +VERSIONS = (COORDINATOR_TRIAGE_1,) LATEST = VERSIONS[-1] + def main(): for version in VERSIONS: print(version.model_dump_json(indent=2)) From 0fb279f17987338dfc769392918fcc249c99a61f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 24 Mar 2025 16:17:07 -0400 Subject: [PATCH 71/99] add combination strings back to dpg --- src/ssvc/dp_groups/base.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index 1698d9cf..5789e5ac 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -1,9 +1,5 @@ #!/usr/bin/env python -""" -file: base -author: adh -created_at: 9/20/23 4:47 PM -""" + # Copyright (c) 2025 Carnegie Mellon University and Contributors. # - see Contributors.md for a full list of Contributors # - see ContributionInstructions.md for information on how you can Contribute to this project @@ -17,13 +13,19 @@ # Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the # U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides a DecisionPointGroup object for use in SSVC. +""" + +import itertools +from typing import Generator + from pydantic import BaseModel from ssvc._mixins import _Base, _SchemaVersioned from ssvc.decision_points.base import ( DecisionPoint, ) -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint class DecisionPointGroup(_Base, _SchemaVersioned, BaseModel): @@ -59,13 +61,21 @@ def decision_points_str(self) -> list[str]: """ return list(self.decision_points_dict.keys()) + def combination_strings(self) -> Generator[tuple[str, ...], None, None]: + """ + Return a list of tuples of the value short strings for all combinations of the decision points. + """ + value_tuples = [dp.value_summaries_str for dp in self.decision_points] + for combo in itertools.product(*value_tuples): + yield combo + def get_all_decision_points_from( *groups: list[DecisionPointGroup], -) -> tuple[SsvcDecisionPoint, ...]: +) -> tuple[DecisionPoint, ...]: """ - Given a list of SsvcDecisionPointGroup objects, return a list of all - the unique SsvcDecisionPoint objects contained in those groups. + Given a list of DecisionPointGroup objects, return a list of all + the unique DecisionPoint objects contained in those groups. Args: groups (list): A list of SsvcDecisionPointGroup objects. From 8f1e98c6037582a639d859719743da8211601d68 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 10:03:55 -0400 Subject: [PATCH 72/99] remove separate outcome groups files --- src/outcomes_to_json.py | 29 ----------------------------- src/ssvc/outcomes/groups.py | 34 ---------------------------------- 2 files changed, 63 deletions(-) delete mode 100644 src/outcomes_to_json.py delete mode 100644 src/ssvc/outcomes/groups.py diff --git a/src/outcomes_to_json.py b/src/outcomes_to_json.py deleted file mode 100644 index 192c8169..00000000 --- a/src/outcomes_to_json.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python3 - -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University - -from ssvc.outcomes import groups -from ssvc.outcomes.base import OutcomeGroup - - -def main(): - for x in dir(groups): - outcome = getattr(groups, x) - if type(outcome) == OutcomeGroup: - with open(f"../data/json/outcomes/{x}.json", "w") as f: - f.write(outcome.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py deleted file mode 100644 index 34f5a3f0..00000000 --- a/src/ssvc/outcomes/groups.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -""" -Provides a set of outcome groups for use in SSVC. -""" - -from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.outcomes.x_community.paranoids import THE_PARANOIDS - -# Note: Outcome Groups must be defined in ascending order. - - -def main(): - print_versions_and_diffs( - [ - THE_PARANOIDS, - ] - ) - - -if __name__ == "__main__": - main() From 239eb096a620588f08e383a13aea45b61e6cdf5e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 10:04:18 -0400 Subject: [PATCH 73/99] revise decision point registry, add value summary registry --- src/ssvc/decision_points/base.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index dab1f208..5c063fbf 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -29,28 +29,37 @@ _Valued, _Versioned, ) -from ssvc.namespaces import NameSpace logger = logging.getLogger(__name__) -_RDP = {} +_DECISION_POINT_REGISTRY = {} + REGISTERED_DECISION_POINTS = [] + FIELD_DELIMITER = ":" +_VALUES_REGISTRY = {} + def register(dp): """ Register a decision point. """ - global _RDP + global _DECISION_POINT_REGISTRY - key = (dp.namespace, dp.name, dp.key, dp.version) + key = dp.str - if key in _RDP: + for value_str, value_summary in dp.value_summaries_dict.items(): + if value_str in _VALUES_REGISTRY: + logger.warning(f"Duplicate value summary {value_str}") + + _VALUES_REGISTRY[value_str] = value_summary + + if key in _DECISION_POINT_REGISTRY: logger.warning(f"Duplicate decision point {key}") - _RDP[key] = dp + _DECISION_POINT_REGISTRY[key] = dp REGISTERED_DECISION_POINTS.append(dp) @@ -58,10 +67,13 @@ def _reset_registered(): """ Reset the registered decision points. """ - global _RDP + global _DECISION_POINT_REGISTRY global REGISTERED_DECISION_POINTS - _RDP = {} + global _VALUES_REGISTRY + + _DECISION_POINT_REGISTRY = {} + _VALUES_REGISTRY = {} REGISTERED_DECISION_POINTS = [] From b3cb57f01e197669eee4628b28d5003cebe90df7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 10:47:15 -0400 Subject: [PATCH 74/99] remove temp notebook --- src/exploratory.ipynb | 165 ------------------------------------------ 1 file changed, 165 deletions(-) delete mode 100644 src/exploratory.ipynb diff --git a/src/exploratory.ipynb b/src/exploratory.ipynb deleted file mode 100644 index b0099b55..00000000 --- a/src/exploratory.ipynb +++ /dev/null @@ -1,165 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "initial_id", - "metadata": { - "ExecuteTime": { - "end_time": "2025-03-21T13:31:21.532792Z", - "start_time": "2025-03-21T13:31:21.520299Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Duplicate decision point (, 'Report Public', 'RP', '1.0.0')\n", - "Duplicate decision point (, 'Supplier Contacted', 'SC', '1.0.0')\n", - "Duplicate decision point (, 'Report Credibility', 'RC', '1.0.0')\n", - "Duplicate decision point (, 'Supplier Cardinality', 'SC', '1.0.0')\n", - "Duplicate decision point (, 'Supplier Engagement', 'SE', '1.0.0')\n", - "Duplicate decision point (, 'Utility', 'U', '1.0.1')\n", - "Duplicate decision point (, 'Automatable', 'A', '2.0.0')\n", - "Duplicate decision point (, 'Value Density', 'VD', '1.0.0')\n", - "Duplicate decision point (, 'Public Safety Impact', 'PSI', '2.0.0')\n", - "Duplicate decision point (, 'Safety Impact', 'SI', '1.0.0')\n" - ] - } - ], - "source": [ - "from ssvc.dp_groups.ssvc.coordinator_triage import COORDINATOR_TRIAGE_1 as dpg" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "72c7764cb46593cc", - "metadata": { - "ExecuteTime": { - "end_time": "2025-03-21T13:33:19.287836Z", - "start_time": "2025-03-21T13:33:19.284542Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='N'))\n", - "1 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='M'))\n", - "2 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='J'))\n", - "3 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='H'))\n", - "4 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='M'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='C'))\n", - "5 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='S'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='N'))\n", - "6 (ValueSummary(namespace='ssvc', key='RP', version='1.0.0', value='Y'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='N'), ValueSummary(namespace='ssvc', key='RC', version='1.0.0', value='NC'), ValueSummary(namespace='ssvc', key='SC', version='1.0.0', value='O'), ValueSummary(namespace='ssvc', key='SE', version='1.0.0', value='A'), ValueSummary(namespace='ssvc', key='U', version='1.0.1', value='L'), ValueSummary(namespace='ssvc', key='A', version='2.0.0', value='N'), ValueSummary(namespace='ssvc', key='VD', version='1.0.0', value='D'), ValueSummary(namespace='ssvc', key='PSI', version='2.0.0', value='S'), ValueSummary(namespace='ssvc', key='SI', version='1.0.0', value='M'))\n" - ] - } - ], - "source": [ - "for i,c in enumerate(dpg.combination_summaries()):\n", - " print(i,c)\n", - " if i>5:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "f04f44540b0c9c2b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:N')\n", - "1 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:M')\n", - "2 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:J')\n", - "3 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:H')\n", - "4 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:M', 'ssvc:SI:1.0.0:C')\n", - "5 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:S', 'ssvc:SI:1.0.0:N')\n", - "6 ('ssvc:RP:1.0.0:Y', 'ssvc:SC:1.0.0:N', 'ssvc:RC:1.0.0:NC', 'ssvc:SC:1.0.0:O', 'ssvc:SE:1.0.0:A', 'ssvc:U:1.0.1:L', 'ssvc:A:2.0.0:N', 'ssvc:VD:1.0.0:D', 'ssvc:PSI:2.0.0:S', 'ssvc:SI:1.0.0:M')\n" - ] - } - ], - "source": [ - "for i,s in enumerate(dpg.combination_strings()):\n", - " print(i,s)\n", - " if i > 5:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "635de87b-52a2-4c5e-9a90-5581448cb28e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'N'})\n", - "\n", - "\n", - "1 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'M'})\n", - "\n", - "\n", - "2 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'J'})\n", - "\n", - "\n", - "3 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'H'})\n", - "\n", - "\n", - "4 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'M'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'C'})\n", - "\n", - "\n", - "5 ({'namespace': 'ssvc', 'key': 'RP', 'version': '1.0.0', 'value': 'Y'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'RC', 'version': '1.0.0', 'value': 'NC'}, {'namespace': 'ssvc', 'key': 'SC', 'version': '1.0.0', 'value': 'O'}, {'namespace': 'ssvc', 'key': 'SE', 'version': '1.0.0', 'value': 'A'}, {'namespace': 'ssvc', 'key': 'U', 'version': '1.0.1', 'value': 'L'}, {'namespace': 'ssvc', 'key': 'A', 'version': '2.0.0', 'value': 'N'}, {'namespace': 'ssvc', 'key': 'VD', 'version': '1.0.0', 'value': 'D'}, {'namespace': 'ssvc', 'key': 'PSI', 'version': '2.0.0', 'value': 'S'}, {'namespace': 'ssvc', 'key': 'SI', 'version': '1.0.0', 'value': 'N'})\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for i,t in enumerate(dpg.combination_dicts()):\n", - " if i>5:\n", - " break\n", - " print(i,t)\n", - " print()\n", - " print()\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "129afda1-f243-4b09-a0e3-8d9a8c8a9baa", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From b4cfd27cddb4a68cb98bf9402c825c3a3170fcbd Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 13:14:21 -0400 Subject: [PATCH 75/99] use new features of objects --- src/ssvc/policy_generator.py | 42 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 8e235ab4..c99d3cf8 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -3,18 +3,24 @@ Provides a Policy Generator class for SSVC decision point groups. """ -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# Copyright (c) 2023-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 import itertools import logging @@ -163,14 +169,14 @@ def _create_policy(self): row = {} for i in range(len(node)): # turn the numerical indexes back into decision point names - col1 = f"{self.dpg.decision_points[i].name}" - row[col1] = self.dpg.decision_points[i].values[node[i]].name + col1 = f"{self.dpg.decision_points[i].str}" + row[col1] = self.dpg.decision_points[i].value_summaries_str[node[i]] # numerical values - col2 = f"idx_{self.dpg.decision_points[i].name}" + col2 = f"idx_{self.dpg.decision_points[i].str}" row[col2] = node[i] oc_idx = self.G.nodes[node]["outcome"] - row["outcome"] = self.outcomes.values[oc_idx].name + row["outcome"] = self.outcomes.value_summaries_str[oc_idx] row["idx_outcome"] = oc_idx rows.append(row) @@ -183,8 +189,8 @@ def clean_policy(self) -> pd.DataFrame: df = df.rename(columns={"outcome": self.outcomes.name}) print_cols = [c for c in df.columns if not c.startswith("idx_")] - for c in print_cols: - df[c] = df[c].str.lower() + # for c in print_cols: + # df[c] = df[c].str.lower() return pd.DataFrame(df[print_cols]) From 059f9480e6d11e0bf2e3b3c0e5e8b19aceb52e4e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 13:14:44 -0400 Subject: [PATCH 76/99] add policy generator to decision table object --- src/ssvc/decision_tables/base.py | 119 +++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 29 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 20419528..5251f5a2 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -1,41 +1,40 @@ #!/usr/bin/env python -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# 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 a DecisionTable class that can be used to model decisions in SSVC """ import logging -import re +from typing import Optional -from pydantic import BaseModel +import pandas as pd +from pydantic import BaseModel, Field from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned from ssvc.dp_groups.base import DecisionPointGroup from ssvc.outcomes.base import OutcomeGroup +from ssvc.policy_generator import PolicyGenerator logger = logging.getLogger(__name__) -def name_to_key(name: str) -> str: - """ - Convert a name to a key by converting to lowercase and replacing spaces with underscores. - """ - # replace non-alphanumeric characters with underscores - new_name = re.sub(r"[^a-z0-9]+", "_", name.lower()) - return new_name - - class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): """ The DecisionTable class is a model for decisions in SSVC. @@ -49,10 +48,70 @@ class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel) decision_point_group: DecisionPointGroup outcome_group: OutcomeGroup - - def combinations(self): - """Generate possible decision point values""" - return self.decision_point_group.combination_strings() + mapping: list[tuple[tuple[str, ...], tuple[str, ...]]] = Field(default_factory=list) + + def get_mapping_df(self, weights: Optional[list[float]] = None) -> pd.DataFrame: + # create a policy generator object and extract a mapping from it + with PolicyGenerator( + dp_group=self.decision_point_group, + outcomes=self.outcome_group, + outcome_weights=weights, + ) as pg: + df = pg.clean_policy() + + return df + + def dataframe_to_tuple_list( + self, df: pd.DataFrame, n_outcols: int = 1 + ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Converts a DataFrame into a list of tuples where each tuple contains: + - A tuple of input values (all columns except the last n_outcols) + - A tuple containing the outcome value (last n_outcols columns) + + Note: + In every decision we've modeled to date, there is only one outcome column. + We have not yet encountered a decision with multiple outcome columns, + however, it has come up in some discussions, so we're allowing for it here as + a future-proofing measure. + + Attributes: + df: pandas DataFrame + n_outcols: int, default=1 + + Returns: + list[tuple[tuple[str,...],tuple[str,...]]]: A list of tuples + + """ + input_columns = df.columns[:-n_outcols] # All columns except the last one + output_column = df.columns[-n_outcols] # The last column + + return [ + (tuple(row[input_columns]), (row[output_column],)) + for _, row in df.iterrows() + ] + + def set_mapping( + self, df: pd.DataFrame + ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Sets the mapping attribute to the output of dataframe_to_tuple_list + + :param df: pandas DataFrame + """ + self.mapping = self.dataframe_to_tuple_list(df) + return self.mapping + + def generate_mapping(self) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Generates a mapping from the decision point group to the outcome group using the PolicyGenerator class + and sets the mapping attribute to the output of dataframe_to_tuple_list + + Returns: + list[tuple[tuple[str,...],tuple[str,...]]]: The generated mapping + """ + df = self.get_mapping_df() + return self.set_mapping(df) # convenience alias @@ -75,9 +134,11 @@ def main(): decision_point_group=dpg, outcome_group=og, ) - print(dt.model_dump_json(indent=2)) - print(list(dt.combinations())) + df = dt.get_mapping_df() + dt.set_mapping(df) + + print(dt.model_dump_json(indent=2)) if __name__ == "__main__": From eb59f46f0becb2da275929c00246bfc6f4f7798b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 16:45:27 -0400 Subject: [PATCH 77/99] update tests --- .../{test_decision_table.py => test_base.py} | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) rename src/test/decision_tables/{test_decision_table.py => test_base.py} (55%) diff --git a/src/test/decision_tables/test_decision_table.py b/src/test/decision_tables/test_base.py similarity index 55% rename from src/test/decision_tables/test_decision_table.py rename to src/test/decision_tables/test_base.py index 5557b0ce..2fba129b 100644 --- a/src/test/decision_tables/test_decision_table.py +++ b/src/test/decision_tables/test_base.py @@ -1,25 +1,37 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# 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 import tempfile import unittest -from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from pandas import DataFrame + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue, + _DECISION_POINT_REGISTRY, +) from ssvc.decision_tables import base from ssvc.dp_groups.base import DecisionPointGroup from ssvc.outcomes.base import OutcomeValue -class TestDecisionTables(unittest.TestCase): +class TestDecisionTable(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.tempdir_path = self.tempdir.name @@ -87,6 +99,20 @@ def test_create(self): self.assertEqual(self.dt.decision_point_group, self.dpg) self.assertEqual(self.dt.outcome_group, self.og) + def test_get_mapping_df(self): + df = self.dt.get_mapping_df() + self.assertIsInstance(df, DataFrame) + # columns are the decision point strings and the outcome group string + for dp in self.dpg.decision_points: + self.assertIn(dp.str, df.columns[:-1]) + self.assertEqual(self.og.str, df.columns[-1]) + + for col in df.columns: + dp = _DECISION_POINT_REGISTRY[col] + # all values in the decision point should be in the column at least once + for vsum in dp.value_summaries_str: + self.assertIn(vsum, df[col].unique()) + if __name__ == "__main__": unittest.main() From 5cc590e47f2f838a51f4b67c9df9cbefe7bb2df3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 25 Mar 2025 16:46:01 -0400 Subject: [PATCH 78/99] minor clean up items --- src/ssvc/decision_points/cisa/base.py | 32 +++++++++++++++---------- src/ssvc/outcomes/base.py | 34 +++++++++++++++++---------- src/ssvc/policy_generator.py | 2 +- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/ssvc/decision_points/cisa/base.py b/src/ssvc/decision_points/cisa/base.py index 739c5b0d..76e10368 100644 --- a/src/ssvc/decision_points/cisa/base.py +++ b/src/ssvc/decision_points/cisa/base.py @@ -1,16 +1,22 @@ #!/usr/bin/env python3 -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# 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 a base class for CISA-specific decision points. """ @@ -20,4 +26,4 @@ class CisaDecisionPoint(DecisionPoint, BaseModel): - namespace = "cisa" + namespace: str = "cisa" diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index a1cb6c08..f2074f4f 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -1,22 +1,32 @@ -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# Copyright (c) 2023-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 outcome group and outcome value classes for SSVC. """ from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint +from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint OutcomeValue = DecisionPointValue OutcomeGroup = DecisionPoint SsvcOutcomeGroup = SsvcDecisionPoint +CvssOutcomeGroup = CvssDecisionPoint +CisaOutcomeGroup = CisaDecisionPoint diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index c99d3cf8..c77e649d 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -186,7 +186,7 @@ def _create_policy(self): def clean_policy(self) -> pd.DataFrame: df = self.policy.copy() # rename "outcome" column to outcome group name - df = df.rename(columns={"outcome": self.outcomes.name}) + df = df.rename(columns={"outcome": self.outcomes.str}) print_cols = [c for c in df.columns if not c.startswith("idx_")] # for c in print_cols: From a73f3ec6f810908af1b634021015e77311d18533 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 26 Mar 2025 12:33:30 -0400 Subject: [PATCH 79/99] update test --- src/ssvc/decision_points/base.py | 105 ++++++++++++++++++-------- src/test/decision_tables/test_base.py | 30 ++++++-- src/test/test_policy_generator.py | 42 +++++++---- 3 files changed, 122 insertions(+), 55 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 5c063fbf..2fc494ff 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -3,22 +3,28 @@ """ Defines the formatting for SSVC Decision Points. """ -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# Copyright (c) 2023-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 import logging -from pydantic import BaseModel, model_validator +from pydantic import BaseModel, Field, model_validator from ssvc._mixins import ( _Base, @@ -33,33 +39,63 @@ logger = logging.getLogger(__name__) -_DECISION_POINT_REGISTRY = {} - REGISTERED_DECISION_POINTS = [] - FIELD_DELIMITER = ":" -_VALUES_REGISTRY = {} +class Registry(BaseModel): + registry: dict[str, object] = Field(default_factory=dict) -def register(dp): + def __iter__(self): + return iter(self.registry.values()) + + def __getitem__(self, key: str) -> object: + return self.registry[key] + + def __setitem__(self, key: str, value: object): + if key in self.registry: + logger.warning(f"Duplicate key {key}") + + self.registry[key] = value + + def __contains__(self, key: str) -> bool: + return key in self.registry + + def reset_registry(self): + self.registry = {} + + # convenience alias + def clear(self): + self.reset_registry() + + +class DecisionPointRegistry(Registry, BaseModel): """ - Register a decision point. + A dictionary of decision points. """ - global _DECISION_POINT_REGISTRY - key = dp.str + registry: dict[str, "DecisionPoint"] = Field(default_factory=dict) - for value_str, value_summary in dp.value_summaries_dict.items(): - if value_str in _VALUES_REGISTRY: - logger.warning(f"Duplicate value summary {value_str}") - _VALUES_REGISTRY[value_str] = value_summary +class DecisionPointValueRegistry(Registry, BaseModel): + """ + A dictionary of decision point values. + """ + + registry: dict[str, "DecisionPointValue"] = Field(default_factory=dict) + + +def register(dp): + """ + Register a decision point. + """ - if key in _DECISION_POINT_REGISTRY: - logger.warning(f"Duplicate decision point {key}") + # register the values + for value_str, value_summary in dp.value_summaries_dict.items(): + DPV_REGISTRY[value_str] = value_summary - _DECISION_POINT_REGISTRY[key] = dp + key = dp.str + DP_REGISTRY[key] = dp REGISTERED_DECISION_POINTS.append(dp) @@ -67,13 +103,12 @@ def _reset_registered(): """ Reset the registered decision points. """ - global _DECISION_POINT_REGISTRY + global DPV_REGISTRY + global DP_REGISTRY global REGISTERED_DECISION_POINTS - global _VALUES_REGISTRY - - _DECISION_POINT_REGISTRY = {} - _VALUES_REGISTRY = {} + DPV_REGISTRY.reset_registry() + DP_REGISTRY.reset_registry() REGISTERED_DECISION_POINTS = [] @@ -195,6 +230,10 @@ def value_summaries_str(self): return list(self.value_summaries_dict.keys()) +DP_REGISTRY = DecisionPointRegistry() +DPV_REGISTRY = DecisionPointRegistry() + + def main(): opt_none = DecisionPointValue( name="None", key="N", description="No exploit available" diff --git a/src/test/decision_tables/test_base.py b/src/test/decision_tables/test_base.py index 2fba129b..471c76d3 100644 --- a/src/test/decision_tables/test_base.py +++ b/src/test/decision_tables/test_base.py @@ -22,9 +22,9 @@ from pandas import DataFrame from ssvc.decision_points.base import ( + DP_REGISTRY, DecisionPoint, DecisionPointValue, - _DECISION_POINT_REGISTRY, ) from ssvc.decision_tables import base from ssvc.dp_groups.base import DecisionPointGroup @@ -36,6 +36,8 @@ def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.tempdir_path = self.tempdir.name + DP_REGISTRY.clear() + dps = [] for i in range(3): dpvs = [] @@ -102,16 +104,32 @@ def test_create(self): def test_get_mapping_df(self): df = self.dt.get_mapping_df() self.assertIsInstance(df, DataFrame) - # columns are the decision point strings and the outcome group string - for dp in self.dpg.decision_points: - self.assertIn(dp.str, df.columns[:-1]) + + # df is not empty + self.assertFalse(df.empty) + # df has some rows + self.assertGreater(len(df), 0) + # df has the same number of rows as the product of the number of decision points and their values + combos = list(self.dpg.combination_strings()) + self.assertGreater(len(combos), 0) + self.assertEqual(len(df), len(combos)) + + # column names are the decision point strings and the outcome group string + for i, dp in enumerate(self.dpg.decision_points): + self.assertEqual(dp.str, df.columns[i]) self.assertEqual(self.og.str, df.columns[-1]) for col in df.columns: - dp = _DECISION_POINT_REGISTRY[col] + # col is in the registry + self.assertIn(col, DP_REGISTRY) + + dp = DP_REGISTRY[col] + + uniq = df[col].unique() + # all values in the decision point should be in the column at least once for vsum in dp.value_summaries_str: - self.assertIn(vsum, df[col].unique()) + self.assertIn(vsum, uniq) if __name__ == "__main__": diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 77246271..57f7b033 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -1,15 +1,21 @@ -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# Copyright (c) 2023-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 import unittest from collections import Counter @@ -243,7 +249,7 @@ def test_emit_policy(self): for dpg in pg.dpg.decision_points: self.assertIn(dpg.name, stdout) for og in pg.outcomes.values: - self.assertIn(og.name.lower(), stdout) + self.assertIn(og.name, stdout) def test_create_policy(self): pg = PolicyGenerator( @@ -265,15 +271,19 @@ def test_create_policy(self): self.assertIsInstance(pg.policy, pd.DataFrame) self.assertEqual(16, len(pg.policy)) + idx_cols = [col for col in pg.policy.columns if col.startswith("idx_")] + other_cols = [col for col in pg.policy.columns if not col.startswith("idx_")] + for c in self.dp_names: - self.assertIn(c, pg.policy.columns) - self.assertIn(f"idx_{c}", pg.policy.columns) + + self.assertTrue(any([c in col for col in other_cols])) + self.assertTrue(any([c in col for col in idx_cols])) self.assertIn("outcome", pg.policy.columns) self.assertIn("idx_outcome", pg.policy.columns) for outcome in self.og_names: - self.assertIn(outcome, pg.policy.outcome.values) + self.assertTrue(any([outcome in val for val in pg.policy.outcome.values])) def test_validate_paths(self): pg = PolicyGenerator( From 8bac303f65e876ca10a7b9f105da9c77dbc02f38 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2025 09:30:32 -0400 Subject: [PATCH 80/99] add test make target --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1345e143..387f1b9c 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,9 @@ dockerrun_docs: @echo "Running the docs Docker image..." $(DOCKER_RUN) --publish $(MKDOCS_PORT):8000 $(PROJECT_VOLUME) $(DOCS_IMAGE) +.PHONY: test +test: + pytest -v src/test docs: dockerbuild_docs dockerrun_docs docker_test: dockerbuild_test dockerrun_test @@ -51,8 +54,9 @@ help: @echo "Targets:" @echo " all - Display this help message" @echo " mdlint_fix - Run markdownlint with --fix" + @echo " test - Run the tests in a local shell" @echo " docs - Build and run the docs Docker image" - @echo " docker_test - Build and run the test Docker image" + @echo " docker_test - Build and run the tests in a Docker image" @echo "" @echo " dockerbuild_test - Build the test Docker image" @echo " dockerrun_test - Run the test Docker image" From 9ccfd4922cfa80a971833139540cf561861637f4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 27 Mar 2025 12:22:11 -0400 Subject: [PATCH 81/99] update example --- src/ssvc/decision_tables/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 5251f5a2..2ffb86ba 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -127,8 +127,8 @@ def main(): logger.addHandler(logging.StreamHandler()) dt = DecisionTable( - name="Example Prioritization Framework", - description="The description for an Example Prioritization Framework", + name="Example Decision Table", + description="The description for an Example Decision Table", namespace="x_test", version="1.0.0", decision_point_group=dpg, From fce30beff9a890043adad6eaccad9b7bef837251 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 31 Mar 2025 12:44:47 -0400 Subject: [PATCH 82/99] add type hints --- src/ssvc/decision_points/base.py | 15 +++++++++++---- src/ssvc/dp_groups/base.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 2fc494ff..9f38fd2b 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -46,13 +46,13 @@ class Registry(BaseModel): registry: dict[str, object] = Field(default_factory=dict) - def __iter__(self): + def __iter__(self) -> object: return iter(self.registry.values()) def __getitem__(self, key: str) -> object: return self.registry[key] - def __setitem__(self, key: str, value: object): + def __setitem__(self, key: str, value: object) -> None: if key in self.registry: logger.warning(f"Duplicate key {key}") @@ -61,11 +61,11 @@ def __setitem__(self, key: str, value: object): def __contains__(self, key: str) -> bool: return key in self.registry - def reset_registry(self): + def reset_registry(self) -> None: self.registry = {} # convenience alias - def clear(self): + def clear(self) -> None: self.reset_registry() @@ -229,6 +229,13 @@ def value_summaries_str(self): """ return list(self.value_summaries_dict.keys()) + @property + def enumerated_values(self) -> dict[int, str]: + """ + Return a list of enumerated values. + """ + return {i: v.str for i, v in enumerate(self.value_summaries)} + DP_REGISTRY = DecisionPointRegistry() DPV_REGISTRY = DecisionPointRegistry() diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index 2ea76528..daf282a7 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -41,13 +41,13 @@ class DecisionPointGroup(_Base, _SchemaVersioned, BaseModel): decision_points: tuple[DecisionPoint, ...] - def __iter__(self): + def __iter__(self) -> Generator[DecisionPoint, None, None]: """ Allow iteration over the decision points in the group. """ return iter(self.decision_points) - def __len__(self): + def __len__(self) -> int: """ Allow len() to be called on the group. """ From 5f5db6c379ea4c72700d8891b48d076b0a82e724 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 31 Mar 2025 12:45:39 -0400 Subject: [PATCH 83/99] add consistency checks to DecisionTable object --- src/ssvc/decision_tables/base.py | 251 ++++++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 2 deletions(-) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 2ffb86ba..2eccca42 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -21,9 +21,11 @@ """ Provides a DecisionTable class that can be used to model decisions in SSVC """ +import itertools import logging from typing import Optional +import networkx as nx import pandas as pd from pydantic import BaseModel, Field @@ -113,14 +115,252 @@ def generate_mapping(self) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: df = self.get_mapping_df() return self.set_mapping(df) + def consistency_check_mapping(self) -> bool: + """Checks the mapping attribute by ensuring that the contents are consistent with the partial order over the decision points and outcomes""" + + # convert the decision point values to a list of numerical tuples + # and create a lookup table for the values + # The end result will be that vector contains a list of tuples + # where each tuple is a list of integers that represent the values + # in a decision point. The dp_lookup list will contain a dictionary + # for each decision point that maps the integer values to the string values + vector = [] + dp_lookup = [] + + # create an inverted index for the outcomes + outcome_lookup = {v: k for k, v in self.outcome_group.enumerated_values.items()} + logger.debug(f"Outcome lookup: {outcome_lookup}") + + for dp in self.decision_point_group: + valuesdict = dp.enumerated_values + dp_lookup.append(valuesdict) + + _vlist = tuple(valuesdict.keys()) + vector.append(_vlist) + + # vector now looks like + # [(0, 1, 2), (0, 1), (0, 1, 2, 3)] + logger.debug(f"Vector: {vector}") + # dp_lookup looks like + # [{0: 'a', 1: 'b', 2: 'c'}, {0: 'x', 1: 'y'}, {0: 'i', 1: 'j', 2: 'k', 3: 'l'}] + logger.debug(f"DP lookup: {dp_lookup}") + + bottom = tuple([min(t) for t in vector]) + top = tuple([max(t) for t in vector]) + + logger.debug(f"Bottom node: {bottom}") + logger.debug(f"Top node: {top}") + + # construct a directed graph + G = nx.DiGraph() + G = self._add_nodes(G, vector) + G = self._add_edges(G) + + problems = self._check_graph(G, bottom, top) + + # if we already have problems, we should stop here + # because the graph is not valid + if problems: + for problem in problems: + logger.error(f"Problem detected: {problem}") + return False + + # the graph has a lot of edges where the outcome does not change between the nodes + # we need to find the decision boundaries where the outcome does change + decision_boundaries = self._find_decision_boundaries(G, dp_lookup) + + problems = self._check_decision_boundaries(decision_boundaries, outcome_lookup) + for problem in problems: + logger.error(f"Problem detected: {problem}") + + # return True if there are no problems + return len(problems) == 0 + + def _check_decision_boundaries( + self, decision_boundaries: list[dict[str, str]], outcome_lookup: dict[str, int] + ) -> list[str]: + + problems = [] + for db in decision_boundaries: + from_outcome = outcome_lookup[db["from_outcome"]] + to_outcome = outcome_lookup[db["to_outcome"]] + + if from_outcome < to_outcome: + # this is what we wanted, no problem found + continue + + problem = ( + f"{db['from']} < {db['to']} but {from_outcome} is not < {to_outcome}" + ) + problems.append(problem) + return problems + + def _find_decision_boundaries( + self, G: nx.DiGraph, dp_lookup: list[dict[int, str]] + ) -> list[dict[str, str]]: + decision_boundaries = [] + for edge in G.edges: + u, v = edge + + # we need to translate from a node int tuple to the strings so we can look it up in the mapping + u_str = self.int_node_to_str(u, dp_lookup) + v_str = self.int_node_to_str(v, dp_lookup) + + mapping_dict = dict(self.mapping) + try: + u_outcome_str = mapping_dict[u_str][0] + except KeyError: + print(f"Node {u_str} has no mapping") + + try: + v_outcome_str = mapping_dict[v_str][0] + except KeyError: + logger.error(f"Node {v_str} has no mapping") + raise ValueError + + if u_outcome_str == v_outcome_str: + # no decision boundary here + continue + + # if we got here, there is a decision boundary + # so we need to record it + row = { + "from": u_str, + "to": v_str, + "from_outcome": u_outcome_str, + "to_outcome": v_outcome_str, + } + decision_boundaries.append(row) + + return decision_boundaries + + def int_node_to_str( + self, node: tuple[int, ...], dp_lookup: list[dict[int, str]] + ) -> tuple[str, ...]: + return tuple([dp_lookup[i][node[i]] for i in range(len(node))]) + + def _check_graph( + self, G: nx.DiGraph, bottom: tuple[int, ...], top: tuple[int, ...] + ) -> list[str]: + problems = [] + # check nodes for edges + for node in G.nodes: + if node != bottom and not G.in_degree(node): + # all nodes except bottom should have at least one incoming edge + problems.append(f"Node {node} has no incoming edges") + if node != top and not G.out_degree(node): + # all nodes except top should have at least one outgoing edge + problems.append(f"Node {node} has no outgoing edges") + return problems + + def _add_nodes(self, G: nx.DiGraph, vector: list[tuple[int, ...]]) -> nx.DiGraph: + + for node in itertools.product(*vector): + node = tuple(node) + # node is a tuple of integers + G.add_node(node) + + return G + + def _add_edges(self, G: nx.DiGraph) -> nx.DiGraph: + """ + Add edges to the graph G based on the nodes in G. + Node identities are tuples of integers. + Edges are added between nodes where one and only one element of the tuples differ by 1. + + Examples: + + | Node 1 | Node 2 | Edge? | + |--------|--------|-------| + | (0,0) | (0,1) | Yes | + | (0,0) | (1,0) | Yes | + | (0,0) | (1,1) | No | + | (0,0) | (0,0) | No | + | (0,0) | (0,2) | No | + | (0,1) | (0,2) | Yes | + + Args: + G: a networkx DiGraph object + + Returns: + a networkx DiGraph object with edges added + + """ + # add edges + for u, v in itertools.product(G.nodes, G.nodes): + # enforce that u and v are tuples of integers + if not isinstance(u, tuple) or any([not isinstance(i, int) for i in u]): + raise ValueError(f"Node {u} is not an integer tuple") + if not isinstance(v, tuple) or any([not isinstance(i, int) for i in v]): + raise ValueError(f"Node {v} is not an integer tuple") + + # add an edge if the difference between u and v is 1 + if u == v: + # do not create self-reflexive edges + continue + + if any(u[i] > v[i] for i in range(len(u))): + # skip the upper triangle of the connectivity matrix + continue + + # if you get here, we know that u < v, but it could be + # a gap larger than 1 from u to v. + # We only want to add edges where the gap is exactly 1. + + # compute the individual differences for each vector element + delta = [v[i] - u[i] for i in range(len(u))] + + if sum(delta) != 1: + # gap is too large + continue + + if not all([d in [0, 1] for d in delta]): + # more than one element is different + # this would be odd if it happened, but check for it anyway + continue + + # if you get here, then there is exactly one element that is different + # by 1, and the rest are the same + # add the edge + G.add_edge(u, v) + + # clean up the graph before we return it + # the transitive reduction of a graph is a graph with the same + # reachability properties, but with as few edges as possible + # https://en.wikipedia.org/wiki/Transitive_reduction + # in principle, our algorithm above shouldn't create any redundant edges + # so this is more of a belt-and-suspenders approach + before = len(G.edges) + G = nx.transitive_reduction(G) + after = len(G.edges) + if before != after: + logger.warning(f"Transitive reduction removed {before - after} edges") + logger.debug(f"Edge count: {after}") + return G + + def mapping_to_csv_str(self): + """ + Returns the mapping as a CSV string + """ + columns = [dp.str for dp in self.decision_point_group] + columns.append(self.outcome_group.str) + + rows = [] + for dpstrs, ostrs in self.mapping: + row = list(dpstrs) + row.append(ostrs[0]) + rows.append(row) + + return pd.DataFrame(rows, columns=columns).to_csv(index=False) + # convenience alias Policy = DecisionTable def main(): - from ssvc.dp_groups.ssvc.supplier import LATEST as dpg - from ssvc.outcomes.x_basic.mscw import MSCW as og + from ssvc.dp_groups.ssvc.coordinator_publication import LATEST as dpg + from ssvc.outcomes.ssvc_.publish import PUBLISH as og logger = logging.getLogger() logger.setLevel(logging.DEBUG) @@ -140,6 +380,13 @@ def main(): print(dt.model_dump_json(indent=2)) + with open("foo.json", "w") as f: + f.write(dt.model_dump_json()) + + dt.consistency_check_mapping() + + print(dt.mapping_to_csv_str()) + if __name__ == "__main__": main() From aad0511534c5afdafe1f91634c7d6e9bf4548531 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 31 Mar 2025 16:10:12 -0400 Subject: [PATCH 84/99] update unit tests --- src/ssvc/decision_tables/base.py | 7 ++ src/test/decision_tables/test_base.py | 110 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 2eccca42..7d3034df 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -353,6 +353,13 @@ def mapping_to_csv_str(self): return pd.DataFrame(rows, columns=columns).to_csv(index=False) + def load_from_csv_str(self, csv_str: str): + """ + Loads the mapping from a CSV string + """ + # TODO add a mechanism to read a mapping from a CSV file and create a DecisionTable object from it + raise NotImplementedError + # convenience alias Policy = DecisionTable diff --git a/src/test/decision_tables/test_base.py b/src/test/decision_tables/test_base.py index 471c76d3..760a3d29 100644 --- a/src/test/decision_tables/test_base.py +++ b/src/test/decision_tables/test_base.py @@ -19,6 +19,7 @@ import tempfile import unittest +import pandas as pd from pandas import DataFrame from ssvc.decision_points.base import ( @@ -131,6 +132,115 @@ def test_get_mapping_df(self): for vsum in dp.value_summaries_str: self.assertIn(vsum, uniq) + def test_dataframe_to_tuple_list(self): + df = pd.DataFrame( + [ + {"a": 1, "b": 2, "c": 3}, + {"a": 4, "b": 5, "c": 6}, + {"a": 7, "b": 8, "c": 9}, + ] + ) + + tuple_list = self.dt.dataframe_to_tuple_list(df) + + self.assertEqual(len(tuple_list), len(df)) + + for row in tuple_list: + self.assertIsInstance(row, tuple) + self.assertEqual(len(row), 2) + self.assertIsInstance(row[0], tuple) + self.assertIsInstance(row[1], tuple) + + # manually check the structure of the tuple list + self.assertEqual((1, 2), tuple_list[0][0]) + self.assertEqual((3,), tuple_list[0][1]) + self.assertEqual((4, 5), tuple_list[1][0]) + self.assertEqual((6,), tuple_list[1][1]) + self.assertEqual((7, 8), tuple_list[2][0]) + self.assertEqual((9,), tuple_list[2][1]) + + def test_set_mapping(self): + df = pd.DataFrame( + { + "a": ["x", "y", "z"], + "b": ["one", "two", "three"], + "c": ["apple", "orange", "pear"], + } + ) + + result = self.dt.set_mapping(df) + + self.assertIn((("x", "one"), ("apple",)), result) + self.assertIn((("y", "two"), ("orange",)), result) + self.assertIn((("z", "three"), ("pear",)), result) + + self.assertEqual(result, self.dt.mapping) + + @unittest.skip("Test not implemented") + def test_consistency_check_mapping(self): + pass + + @unittest.skip("Test not implemented") + def test_check_decision_boundaries(self): + pass + + @unittest.skip("Test not implemented") + def test_find_decision_boundaries(self): + pass + + @unittest.skip("Test not implemented") + def test_int_node_to_str(self): + pass + + @unittest.skip("Test not implemented") + def test_check_graph(self): + pass + + @unittest.skip("Test not implemented") + def test_add_nodes(self): + pass + + @unittest.skip("Test not implemented") + def test_add_edges(self): + pass + + def test_mapping_to_csv_str(self): + df = self.dt.get_mapping_df() + self.dt.set_mapping(df) + + csv_str = self.dt.mapping_to_csv_str() + self.assertIsInstance(csv_str, str) + + lines = csv_str.strip().split("\n") + + # first line is the header + self.assertEqual( + lines[0], + ",".join([dp.str for dp in self.dpg.decision_points] + [self.og.str]), + ) + + combinations = list(self.dpg.combination_strings()) + + # there should be one line for each combination after the header + self.assertEqual(len(lines[1:]), len(combinations)) + + # each line after the header starts with a combination of decision point values + for combo, line in zip(combinations, lines[1:]): + # there are as many commas in the line as there are combos + comma_count = line.count(",") + self.assertEqual(comma_count, len(combo)) + + for dpv_str in combo: + self.assertIn(dpv_str, line) + + # the last thing in the line is the outcome group value + og_value = line.split(",")[-1] + self.assertIn(og_value, self.og.value_summaries_str) + + @unittest.skip("Test not implemented") + def test_load_from_csv_str(self): + pass + if __name__ == "__main__": unittest.main() From da5d4fc9e888f7455cb092b219f42a6722c61d0c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 5 Jun 2025 16:03:27 -0400 Subject: [PATCH 85/99] simplify Makefile / Dockerfile interactions using docker-compose.yml --- Makefile | 68 +++++++++++---------------------- Dockerfile => docker/Dockerfile | 4 +- docker/docker-compose.yml | 36 +++++++++++++++++ 3 files changed, 61 insertions(+), 47 deletions(-) rename Dockerfile => docker/Dockerfile (91%) create mode 100644 docker/docker-compose.yml diff --git a/Makefile b/Makefile index 387f1b9c..af885c5c 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,9 @@ # Project-specific vars -PFX=ssvc -DOCKER=docker -DOCKER_BUILD=$(DOCKER) build -DOCKER_RUN=$(DOCKER) run --tty --rm -PROJECT_VOLUME=--volume $(shell pwd):/app MKDOCS_PORT=8765 - -# docker names -TEST_DOCKER_TARGET=test -TEST_IMAGE = $(PFX)_test -DOCS_DOCKER_TARGET=docs -DOCS_IMAGE = $(PFX)_docs +DOCKER_DIR=docker # Targets -.PHONY: all dockerbuild_test dockerrun_test dockerbuild_docs dockerrun_docs docs docker_test clean help +.PHONY: all test docs docker_test clean help all: help @@ -21,32 +11,29 @@ mdlint_fix: @echo "Running markdownlint..." markdownlint --config .markdownlint.yml --fix . -dockerbuild_test: - @echo "Building the test Docker image..." - $(DOCKER_BUILD) --target $(TEST_DOCKER_TARGET) --tag $(TEST_IMAGE) . - -dockerrun_test: - @echo "Running the test Docker image..." - $(DOCKER_RUN) $(PROJECT_VOLUME) $(TEST_IMAGE) +test: + @echo "Running tests locally..." + pytest -v src/test -dockerbuild_docs: - @echo "Building the docs Docker image..." - $(DOCKER_BUILD) --target $(DOCS_DOCKER_TARGET) --tag $(DOCS_IMAGE) . +docker_test: + @echo "Running tests in Docker..." + pushd $(DOCKER_DIR) && docker-compose run --rm test -dockerrun_docs: - @echo "Running the docs Docker image..." - $(DOCKER_RUN) --publish $(MKDOCS_PORT):8000 $(PROJECT_VOLUME) $(DOCS_IMAGE) +docs: + @echo "Building and running docs in Docker..." + pushd $(DOCKER_DIR) && docker-compose up docs -.PHONY: test -test: - pytest -v src/test +up: + @echo "Starting Docker services..." + pushd $(DOCKER_DIR) && docker-compose up -d -docs: dockerbuild_docs dockerrun_docs -docker_test: dockerbuild_test dockerrun_test +down: + @echo "Stopping Docker services..." + pushd $(DOCKER_DIR) && docker-compose down clean: - @echo "Cleaning up..." - $(DOCKER) rmi $(TEST_IMAGE) $(DOCS_IMAGE) || true + @echo "Cleaning up Docker resources..." + pushd $(DOCKER_DIR) && docker-compose down --rmi local || true help: @echo "Usage: make [target]" @@ -55,16 +42,7 @@ help: @echo " all - Display this help message" @echo " mdlint_fix - Run markdownlint with --fix" @echo " test - Run the tests in a local shell" - @echo " docs - Build and run the docs Docker image" - @echo " docker_test - Build and run the tests in a Docker image" - @echo "" - @echo " dockerbuild_test - Build the test Docker image" - @echo " dockerrun_test - Run the test Docker image" - @echo " dockerbuild_docs - Build the docs Docker image" - @echo " dockerrun_docs - Run the docs Docker image" - @echo "" - @echo " clean - Remove the Docker images" - @echo " help - Display this help message" - - - + @echo " docs - Build and run the docs Docker service" + @echo " docker_test - Run the tests in a Docker container" + @echo " clean - Remove Docker containers and images" + @echo " help - Display this help message" diff --git a/Dockerfile b/docker/Dockerfile similarity index 91% rename from Dockerfile rename to docker/Dockerfile index 79aa5836..964af2a6 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -5,10 +5,10 @@ WORKDIR /app FROM base AS dependencies # install requirements -COPY requirements.txt . +COPY ../requirements.txt . RUN pip install -r requirements.txt # Copy the files we need -COPY . /app +COPY .. /app # Set the environment variable ENV PYTHONPATH=/app/src diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..795eafb6 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,36 @@ +services: + base: + build: + context: .. + dockerfile: docker/Dockerfile + target: base + image: base:latest + + dependencies: + build: + context: .. + dockerfile: docker/Dockerfile + target: dependencies + image: dependencies:latest + depends_on: + - base + + test: + build: + context: .. + dockerfile: docker/Dockerfile + target: test + image: test:latest + depends_on: + - dependencies + + docs: + build: + context: .. + dockerfile: docker/Dockerfile + target: docs + image: docs:latest + depends_on: + - dependencies + ports: + - "8000:8000" From b99a76373ecc70d217318e15b3eaf9b531e9dca8 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 5 Jun 2025 16:03:27 -0400 Subject: [PATCH 86/99] simplify Makefile / Dockerfile interactions using docker-compose.yml # Conflicts: # Makefile --- Makefile | 66 ++++++++++++--------------------- README.md | 28 ++++++++------ Dockerfile => docker/Dockerfile | 4 +- docker/docker-compose.yml | 36 ++++++++++++++++++ 4 files changed, 79 insertions(+), 55 deletions(-) rename Dockerfile => docker/Dockerfile (91%) create mode 100644 docker/docker-compose.yml diff --git a/Makefile b/Makefile index 1345e143..af885c5c 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,9 @@ # Project-specific vars -PFX=ssvc -DOCKER=docker -DOCKER_BUILD=$(DOCKER) build -DOCKER_RUN=$(DOCKER) run --tty --rm -PROJECT_VOLUME=--volume $(shell pwd):/app MKDOCS_PORT=8765 - -# docker names -TEST_DOCKER_TARGET=test -TEST_IMAGE = $(PFX)_test -DOCS_DOCKER_TARGET=docs -DOCS_IMAGE = $(PFX)_docs +DOCKER_DIR=docker # Targets -.PHONY: all dockerbuild_test dockerrun_test dockerbuild_docs dockerrun_docs docs docker_test clean help +.PHONY: all test docs docker_test clean help all: help @@ -21,29 +11,29 @@ mdlint_fix: @echo "Running markdownlint..." markdownlint --config .markdownlint.yml --fix . -dockerbuild_test: - @echo "Building the test Docker image..." - $(DOCKER_BUILD) --target $(TEST_DOCKER_TARGET) --tag $(TEST_IMAGE) . - -dockerrun_test: - @echo "Running the test Docker image..." - $(DOCKER_RUN) $(PROJECT_VOLUME) $(TEST_IMAGE) +test: + @echo "Running tests locally..." + pytest -v src/test -dockerbuild_docs: - @echo "Building the docs Docker image..." - $(DOCKER_BUILD) --target $(DOCS_DOCKER_TARGET) --tag $(DOCS_IMAGE) . +docker_test: + @echo "Running tests in Docker..." + pushd $(DOCKER_DIR) && docker-compose run --rm test -dockerrun_docs: - @echo "Running the docs Docker image..." - $(DOCKER_RUN) --publish $(MKDOCS_PORT):8000 $(PROJECT_VOLUME) $(DOCS_IMAGE) +docs: + @echo "Building and running docs in Docker..." + pushd $(DOCKER_DIR) && docker-compose up docs +up: + @echo "Starting Docker services..." + pushd $(DOCKER_DIR) && docker-compose up -d -docs: dockerbuild_docs dockerrun_docs -docker_test: dockerbuild_test dockerrun_test +down: + @echo "Stopping Docker services..." + pushd $(DOCKER_DIR) && docker-compose down clean: - @echo "Cleaning up..." - $(DOCKER) rmi $(TEST_IMAGE) $(DOCS_IMAGE) || true + @echo "Cleaning up Docker resources..." + pushd $(DOCKER_DIR) && docker-compose down --rmi local || true help: @echo "Usage: make [target]" @@ -51,16 +41,8 @@ help: @echo "Targets:" @echo " all - Display this help message" @echo " mdlint_fix - Run markdownlint with --fix" - @echo " docs - Build and run the docs Docker image" - @echo " docker_test - Build and run the test Docker image" - @echo "" - @echo " dockerbuild_test - Build the test Docker image" - @echo " dockerrun_test - Run the test Docker image" - @echo " dockerbuild_docs - Build the docs Docker image" - @echo " dockerrun_docs - Run the docs Docker image" - @echo "" - @echo " clean - Remove the Docker images" - @echo " help - Display this help message" - - - + @echo " test - Run the tests in a local shell" + @echo " docs - Build and run the docs Docker service" + @echo " docker_test - Run the tests in a Docker container" + @echo " clean - Remove Docker containers and images" + @echo " help - Display this help message" diff --git a/README.md b/README.md index e1bc0c1e..adcb816e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,19 @@ These json files are generated examples from the python `ssvc` module. These files are used by the `ssvc-calc` module. +## `/docker/*` + +The `docker` directory contains Dockerfiles and related configurations for to +create images that can run the SSVC documentation site and unit tests. + +Example: + +```bash +cd docker +docker-compose up test +docker-compose up docs +``` + ## `/src/*` This directory holds helper scripts that can make managing or using SSVC easier. @@ -103,7 +116,7 @@ To preview any `make` command without actually executing it, run: make -n ``` -### Run Local Server With Docker +### Run Local Docs Server With Docker The easiest way to get started is using make to build a docker image and run the site: @@ -111,18 +124,12 @@ The easiest way to get started is using make to build a docker image and run the make docs ``` -Then navigate to to see the site. - -Note that the docker container will display a message with the URL to visit, for -example: `Serving on http://0.0.0.0:8000/SSVC/` in the output. However, that port -is only available inside the container. The host port 8765 is mapped to the container's -port 8000, so you should navigate to to see the site. +Then navigate to to see the site. Or, if make is not available: ```bash -docker build --target docs --tag ssvc_docs . -docker run --tty --rm -p 8765:8000 --volume .:/app ssvc_docs +cd docker && docker-compose up docs ``` ### Run Local Server Without Docker @@ -162,8 +169,7 @@ make docker_test Or, if make is not available: ```bash -docker build --target test --tag ssvc_test . -docker run --tty --rm --volume .:/app ssvc_test +cd docker && docker-compose up test ``` ### Run Tests Without Docker diff --git a/Dockerfile b/docker/Dockerfile similarity index 91% rename from Dockerfile rename to docker/Dockerfile index 79aa5836..964af2a6 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -5,10 +5,10 @@ WORKDIR /app FROM base AS dependencies # install requirements -COPY requirements.txt . +COPY ../requirements.txt . RUN pip install -r requirements.txt # Copy the files we need -COPY . /app +COPY .. /app # Set the environment variable ENV PYTHONPATH=/app/src diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..795eafb6 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,36 @@ +services: + base: + build: + context: .. + dockerfile: docker/Dockerfile + target: base + image: base:latest + + dependencies: + build: + context: .. + dockerfile: docker/Dockerfile + target: dependencies + image: dependencies:latest + depends_on: + - base + + test: + build: + context: .. + dockerfile: docker/Dockerfile + target: test + image: test:latest + depends_on: + - dependencies + + docs: + build: + context: .. + dockerfile: docker/Dockerfile + target: docs + image: docs:latest + depends_on: + - dependencies + ports: + - "8000:8000" From 6c6f2b0ed84d8c6b42c0a73bd286283c48718f97 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 13 Jun 2025 16:04:39 -0400 Subject: [PATCH 87/99] make unit tests pass --- src/ssvc/decision_tables/base.py | 497 ++++++------------ src/ssvc/decision_tables/experimental_base.py | 399 ++++++++++++++ src/ssvc/dp_groups/base.py | 10 +- src/test/decision_tables/test_base.py | 325 +++++------- .../decision_tables/test_experimental_base.py | 280 ++++++++++ 5 files changed, 966 insertions(+), 545 deletions(-) create mode 100644 src/ssvc/decision_tables/experimental_base.py create mode 100644 src/test/decision_tables/test_experimental_base.py diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py index 7d3034df..b85e1b72 100644 --- a/src/ssvc/decision_tables/base.py +++ b/src/ssvc/decision_tables/base.py @@ -1,5 +1,7 @@ #!/usr/bin/env python - +""" +DecisionTableBase: A flexible, serializable SSVC decision table model. +""" # Copyright (c) 2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE # ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. @@ -18,381 +20,198 @@ # This Software includes and/or makes use of Third-Party Software each # subject to its own license. # DM24-0278 -""" -Provides a DecisionTable class that can be used to model decisions in SSVC -""" -import itertools + import logging -from typing import Optional +from itertools import product +from typing import List, Optional -import networkx as nx import pandas as pd -from pydantic import BaseModel, Field +from pydantic import BaseModel, model_validator from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned +from ssvc.decision_points.base import ValueSummary from ssvc.dp_groups.base import DecisionPointGroup from ssvc.outcomes.base import OutcomeGroup -from ssvc.policy_generator import PolicyGenerator logger = logging.getLogger(__name__) -class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): - """ - The DecisionTable class is a model for decisions in SSVC. +class ValueCombo(BaseModel): + values: tuple[ValueSummary, ...] + + +class MappingRow(BaseModel): + decision_point_values: ValueCombo + outcome: Optional[ValueSummary] - It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. - The mapping is generated by the PolicyGenerator class, and stored as a dictionary. - The mapping dict keys are tuples of decision points and decision point values. - The mapping dict values are outcomes. +class DecisionTableBase(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): + """ + DecisionTableBase: A flexible, serializable SSVC decision table model. """ decision_point_group: DecisionPointGroup outcome_group: OutcomeGroup - mapping: list[tuple[tuple[str, ...], tuple[str, ...]]] = Field(default_factory=list) - - def get_mapping_df(self, weights: Optional[list[float]] = None) -> pd.DataFrame: - # create a policy generator object and extract a mapping from it - with PolicyGenerator( - dp_group=self.decision_point_group, - outcomes=self.outcome_group, - outcome_weights=weights, - ) as pg: - df = pg.clean_policy() - - return df - - def dataframe_to_tuple_list( - self, df: pd.DataFrame, n_outcols: int = 1 - ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: - """ - Converts a DataFrame into a list of tuples where each tuple contains: - - A tuple of input values (all columns except the last n_outcols) - - A tuple containing the outcome value (last n_outcols columns) - - Note: - In every decision we've modeled to date, there is only one outcome column. - We have not yet encountered a decision with multiple outcome columns, - however, it has come up in some discussions, so we're allowing for it here as - a future-proofing measure. - - Attributes: - df: pandas DataFrame - n_outcols: int, default=1 - - Returns: - list[tuple[tuple[str,...],tuple[str,...]]]: A list of tuples - - """ - input_columns = df.columns[:-n_outcols] # All columns except the last one - output_column = df.columns[-n_outcols] # The last column - - return [ - (tuple(row[input_columns]), (row[output_column],)) - for _, row in df.iterrows() - ] - - def set_mapping( - self, df: pd.DataFrame - ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: - """ - Sets the mapping attribute to the output of dataframe_to_tuple_list - - :param df: pandas DataFrame - """ - self.mapping = self.dataframe_to_tuple_list(df) - return self.mapping - - def generate_mapping(self) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + mapping: Optional[List[MappingRow]] = None + + @model_validator(mode="after") + def populate_mapping_if_none(self): + + if self.mapping is not None: + # short-circuit if mapping is already set + return self + + dpg = self.decision_point_group + og = self.outcome_group + if dpg is not None and og is not None: + mapping = self.generate_full_mapping() + outcome_values = getattr(og, "value_summaries", None) + if outcome_values: + mapping = distribute_outcomes_evenly(mapping, outcome_values) + else: + raise ValueError( + "Outcome group must have value_summaries to distribute outcomes." + ) + self.mapping = mapping + return self + + def to_csv(self) -> str: """ - Generates a mapping from the decision point group to the outcome group using the PolicyGenerator class - and sets the mapping attribute to the output of dataframe_to_tuple_list - - Returns: - list[tuple[tuple[str,...],tuple[str,...]]]: The generated mapping + Export the mapping to a CSV string. Columns: one per decision point (by name), one for outcome. + Indiviual decision point and outcome values are represented as a colon-separated tuple + consisting of the namespace, decision point key, decision point version, and value key. """ - df = self.get_mapping_df() - return self.set_mapping(df) - - def consistency_check_mapping(self) -> bool: - """Checks the mapping attribute by ensuring that the contents are consistent with the partial order over the decision points and outcomes""" - - # convert the decision point values to a list of numerical tuples - # and create a lookup table for the values - # The end result will be that vector contains a list of tuples - # where each tuple is a list of integers that represent the values - # in a decision point. The dp_lookup list will contain a dictionary - # for each decision point that maps the integer values to the string values - vector = [] - dp_lookup = [] - - # create an inverted index for the outcomes - outcome_lookup = {v: k for k, v in self.outcome_group.enumerated_values.items()} - logger.debug(f"Outcome lookup: {outcome_lookup}") - - for dp in self.decision_point_group: - valuesdict = dp.enumerated_values - dp_lookup.append(valuesdict) - - _vlist = tuple(valuesdict.keys()) - vector.append(_vlist) - - # vector now looks like - # [(0, 1, 2), (0, 1), (0, 1, 2, 3)] - logger.debug(f"Vector: {vector}") - # dp_lookup looks like - # [{0: 'a', 1: 'b', 2: 'c'}, {0: 'x', 1: 'y'}, {0: 'i', 1: 'j', 2: 'k', 3: 'l'}] - logger.debug(f"DP lookup: {dp_lookup}") - - bottom = tuple([min(t) for t in vector]) - top = tuple([max(t) for t in vector]) - - logger.debug(f"Bottom node: {bottom}") - logger.debug(f"Top node: {top}") - - # construct a directed graph - G = nx.DiGraph() - G = self._add_nodes(G, vector) - G = self._add_edges(G) - - problems = self._check_graph(G, bottom, top) - - # if we already have problems, we should stop here - # because the graph is not valid - if problems: - for problem in problems: - logger.error(f"Problem detected: {problem}") - return False - - # the graph has a lot of edges where the outcome does not change between the nodes - # we need to find the decision boundaries where the outcome does change - decision_boundaries = self._find_decision_boundaries(G, dp_lookup) - - problems = self._check_decision_boundaries(decision_boundaries, outcome_lookup) - for problem in problems: - logger.error(f"Problem detected: {problem}") - - # return True if there are no problems - return len(problems) == 0 - - def _check_decision_boundaries( - self, decision_boundaries: list[dict[str, str]], outcome_lookup: dict[str, int] - ) -> list[str]: - - problems = [] - for db in decision_boundaries: - from_outcome = outcome_lookup[db["from_outcome"]] - to_outcome = outcome_lookup[db["to_outcome"]] - - if from_outcome < to_outcome: - # this is what we wanted, no problem found - continue - - problem = ( - f"{db['from']} < {db['to']} but {from_outcome} is not < {to_outcome}" - ) - problems.append(problem) - return problems - - def _find_decision_boundaries( - self, G: nx.DiGraph, dp_lookup: list[dict[int, str]] - ) -> list[dict[str, str]]: - decision_boundaries = [] - for edge in G.edges: - u, v = edge - - # we need to translate from a node int tuple to the strings so we can look it up in the mapping - u_str = self.int_node_to_str(u, dp_lookup) - v_str = self.int_node_to_str(v, dp_lookup) - - mapping_dict = dict(self.mapping) - try: - u_outcome_str = mapping_dict[u_str][0] - except KeyError: - print(f"Node {u_str} has no mapping") - - try: - v_outcome_str = mapping_dict[v_str][0] - except KeyError: - logger.error(f"Node {v_str} has no mapping") - raise ValueError - - if u_outcome_str == v_outcome_str: - # no decision boundary here - continue - - # if we got here, there is a decision boundary - # so we need to record it - row = { - "from": u_str, - "to": v_str, - "from_outcome": u_outcome_str, - "to_outcome": v_outcome_str, + if not self.mapping: + raise ValueError("No mapping to export.") + dp_names = [dp.name for dp in self.decision_point_group.decision_points] + outcome_name = self.outcome_group.name + rows = [] + for row in self.mapping: + row_dict = { + name: str(val) + for name, val in zip(dp_names, row.decision_point_values.values) } - decision_boundaries.append(row) - - return decision_boundaries - - def int_node_to_str( - self, node: tuple[int, ...], dp_lookup: list[dict[int, str]] - ) -> tuple[str, ...]: - return tuple([dp_lookup[i][node[i]] for i in range(len(node))]) - - def _check_graph( - self, G: nx.DiGraph, bottom: tuple[int, ...], top: tuple[int, ...] - ) -> list[str]: - problems = [] - # check nodes for edges - for node in G.nodes: - if node != bottom and not G.in_degree(node): - # all nodes except bottom should have at least one incoming edge - problems.append(f"Node {node} has no incoming edges") - if node != top and not G.out_degree(node): - # all nodes except top should have at least one outgoing edge - problems.append(f"Node {node} has no outgoing edges") - return problems - - def _add_nodes(self, G: nx.DiGraph, vector: list[tuple[int, ...]]) -> nx.DiGraph: + row_dict[outcome_name] = str(row.outcome) if row.outcome else "" + rows.append(row_dict) + df = pd.DataFrame(rows, columns=dp_names + [outcome_name]) + return df.to_csv(index=False) - for node in itertools.product(*vector): - node = tuple(node) - # node is a tuple of integers - G.add_node(node) - - return G - - def _add_edges(self, G: nx.DiGraph) -> nx.DiGraph: + def generate_full_mapping(self) -> List[MappingRow]: """ - Add edges to the graph G based on the nodes in G. - Node identities are tuples of integers. - Edges are added between nodes where one and only one element of the tuples differ by 1. - - Examples: - - | Node 1 | Node 2 | Edge? | - |--------|--------|-------| - | (0,0) | (0,1) | Yes | - | (0,0) | (1,0) | Yes | - | (0,0) | (1,1) | No | - | (0,0) | (0,0) | No | - | (0,0) | (0,2) | No | - | (0,1) | (0,2) | Yes | - - Args: - G: a networkx DiGraph object - - Returns: - a networkx DiGraph object with edges added - + Generate a full mapping for the decision table, with every possible combination of decision point values. + Each MappingRow will have a ValueCombo of ValueSummary objects, and outcome=None. """ - # add edges - for u, v in itertools.product(G.nodes, G.nodes): - # enforce that u and v are tuples of integers - if not isinstance(u, tuple) or any([not isinstance(i, int) for i in u]): - raise ValueError(f"Node {u} is not an integer tuple") - if not isinstance(v, tuple) or any([not isinstance(i, int) for i in v]): - raise ValueError(f"Node {v} is not an integer tuple") - - # add an edge if the difference between u and v is 1 - if u == v: - # do not create self-reflexive edges - continue - - if any(u[i] > v[i] for i in range(len(u))): - # skip the upper triangle of the connectivity matrix - continue - - # if you get here, we know that u < v, but it could be - # a gap larger than 1 from u to v. - # We only want to add edges where the gap is exactly 1. - - # compute the individual differences for each vector element - delta = [v[i] - u[i] for i in range(len(u))] + if self.mapping is not None: + # short-circuit if mapping is already set + logger.warning("Mapping is already set, skipping full generation.") + return self.mapping - if sum(delta) != 1: - # gap is too large - continue + logger.debug("Generating full mapping.") + self.mapping = generate_full_mapping(self) - if not all([d in [0, 1] for d in delta]): - # more than one element is different - # this would be odd if it happened, but check for it anyway - continue - - # if you get here, then there is exactly one element that is different - # by 1, and the rest are the same - # add the edge - G.add_edge(u, v) - - # clean up the graph before we return it - # the transitive reduction of a graph is a graph with the same - # reachability properties, but with as few edges as possible - # https://en.wikipedia.org/wiki/Transitive_reduction - # in principle, our algorithm above shouldn't create any redundant edges - # so this is more of a belt-and-suspenders approach - before = len(G.edges) - G = nx.transitive_reduction(G) - after = len(G.edges) - if before != after: - logger.warning(f"Transitive reduction removed {before - after} edges") - logger.debug(f"Edge count: {after}") - return G - - def mapping_to_csv_str(self): - """ - Returns the mapping as a CSV string - """ - columns = [dp.str for dp in self.decision_point_group] - columns.append(self.outcome_group.str) - - rows = [] - for dpstrs, ostrs in self.mapping: - row = list(dpstrs) - row.append(ostrs[0]) - rows.append(row) - - return pd.DataFrame(rows, columns=columns).to_csv(index=False) + return self.mapping - def load_from_csv_str(self, csv_str: str): + def distribute_outcomes_evenly(self, overwrite: bool = False) -> List[MappingRow]: """ - Loads the mapping from a CSV string + Distribute the given outcome_values across the mapping rows in sorted order. + The earliest mappings get the lowest outcome, the latest get the highest. + If the mapping count is not divisible by the number of outcomes, the last outcome(s) get the remainder. + Returns a new list of MappingRow with outcomes assigned. """ - # TODO add a mechanism to read a mapping from a CSV file and create a DecisionTable object from it - raise NotImplementedError + if self.mapping is not None: + if not overwrite: + # short-circuit if mapping is already set + logger.warning("Mapping is already set, skipping distribution.") + return self.mapping + else: + logger.info("Overwriting existing mapping with new distribution.") + + self.generate_full_mapping() + + outcome_values = getattr(self.outcome_group, "value_summaries", None) + if outcome_values is None: + raise ValueError( + "Outcome group must have value_summaries to distribute outcomes." + ) + if mapping is None: + self.mapping = distribute_outcomes_evenly(outcome_values) -# convenience alias -Policy = DecisionTable +def generate_full_mapping(decision_table: "DecisionTableBase") -> list[MappingRow]: + """ + Generate a full mapping for the decision table, with every possible combination of decision point values. + Each MappingRow will have a ValueCombo of ValueSummary objects, and outcome=None. + """ + dp_group = decision_table.decision_point_group + # For each decision point, get its value summaries + value_lists = [ + [ + ValueSummary( + key=dp.key, + version=dp.version, + namespace=dp.namespace, + value=val.key, + ) + for val in dp.values + ] + for dp in dp_group.decision_points + ] + all_combos = product(*value_lists) + mapping = [ + MappingRow(decision_point_values=ValueCombo(values=combo), outcome=None) + for combo in all_combos + ] + return mapping + + +def distribute_outcomes_evenly( + mapping: list[MappingRow], outcome_values: list[ValueSummary] +) -> list[MappingRow]: + """ + Distribute the given outcome_values across the mapping rows in sorted order. + The earliest mappings get the lowest outcome, the latest get the highest. + If the mapping count is not divisible by the number of outcomes, the last outcome(s) get the remainder. + Returns a new list of MappingRow with outcomes assigned. + """ + if not outcome_values: + raise ValueError("No outcome values provided for distribution.") + n = len(mapping) + k = len(outcome_values) + base = n // k + rem = n % k + new_mapping = [] + idx = 0 + for i, outcome in enumerate(outcome_values): + count = base + (1 if i < rem else 0) + for _ in range(count): + if idx >= n: + break + row = mapping[idx] + new_mapping.append( + MappingRow( + decision_point_values=row.decision_point_values, outcome=outcome + ) + ) + idx += 1 + return new_mapping -def main(): - from ssvc.dp_groups.ssvc.coordinator_publication import LATEST as dpg - from ssvc.outcomes.ssvc_.publish import PUBLISH as og - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) +def main() -> None: + from ssvc.dp_groups.ssvc.coordinator_triage import LATEST as dpg + from ssvc.outcomes.x_basic.mscw import LATEST as outcomes - dt = DecisionTable( - name="Example Decision Table", - description="The description for an Example Decision Table", + table = DecisionTableBase( + name="Test Table", + description="A test decision table", namespace="x_test", - version="1.0.0", decision_point_group=dpg, - outcome_group=og, + outcome_group=outcomes, ) - - df = dt.get_mapping_df() - dt.set_mapping(df) - - print(dt.model_dump_json(indent=2)) - - with open("foo.json", "w") as f: - f.write(dt.model_dump_json()) - - dt.consistency_check_mapping() - - print(dt.mapping_to_csv_str()) + table.mapping = generate_full_mapping(table) + table.mapping = distribute_outcomes_evenly(table.mapping, outcomes.value_summaries) + csv_str = table.to_csv() + print(csv_str) if __name__ == "__main__": diff --git a/src/ssvc/decision_tables/experimental_base.py b/src/ssvc/decision_tables/experimental_base.py new file mode 100644 index 00000000..7d3034df --- /dev/null +++ b/src/ssvc/decision_tables/experimental_base.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python + +# 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 a DecisionTable class that can be used to model decisions in SSVC +""" +import itertools +import logging +from typing import Optional + +import networkx as nx +import pandas as pd +from pydantic import BaseModel, Field + +from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned +from ssvc.dp_groups.base import DecisionPointGroup +from ssvc.outcomes.base import OutcomeGroup +from ssvc.policy_generator import PolicyGenerator + +logger = logging.getLogger(__name__) + + +class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): + """ + The DecisionTable class is a model for decisions in SSVC. + + It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. + + The mapping is generated by the PolicyGenerator class, and stored as a dictionary. + The mapping dict keys are tuples of decision points and decision point values. + The mapping dict values are outcomes. + """ + + decision_point_group: DecisionPointGroup + outcome_group: OutcomeGroup + mapping: list[tuple[tuple[str, ...], tuple[str, ...]]] = Field(default_factory=list) + + def get_mapping_df(self, weights: Optional[list[float]] = None) -> pd.DataFrame: + # create a policy generator object and extract a mapping from it + with PolicyGenerator( + dp_group=self.decision_point_group, + outcomes=self.outcome_group, + outcome_weights=weights, + ) as pg: + df = pg.clean_policy() + + return df + + def dataframe_to_tuple_list( + self, df: pd.DataFrame, n_outcols: int = 1 + ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Converts a DataFrame into a list of tuples where each tuple contains: + - A tuple of input values (all columns except the last n_outcols) + - A tuple containing the outcome value (last n_outcols columns) + + Note: + In every decision we've modeled to date, there is only one outcome column. + We have not yet encountered a decision with multiple outcome columns, + however, it has come up in some discussions, so we're allowing for it here as + a future-proofing measure. + + Attributes: + df: pandas DataFrame + n_outcols: int, default=1 + + Returns: + list[tuple[tuple[str,...],tuple[str,...]]]: A list of tuples + + """ + input_columns = df.columns[:-n_outcols] # All columns except the last one + output_column = df.columns[-n_outcols] # The last column + + return [ + (tuple(row[input_columns]), (row[output_column],)) + for _, row in df.iterrows() + ] + + def set_mapping( + self, df: pd.DataFrame + ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Sets the mapping attribute to the output of dataframe_to_tuple_list + + :param df: pandas DataFrame + """ + self.mapping = self.dataframe_to_tuple_list(df) + return self.mapping + + def generate_mapping(self) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: + """ + Generates a mapping from the decision point group to the outcome group using the PolicyGenerator class + and sets the mapping attribute to the output of dataframe_to_tuple_list + + Returns: + list[tuple[tuple[str,...],tuple[str,...]]]: The generated mapping + """ + df = self.get_mapping_df() + return self.set_mapping(df) + + def consistency_check_mapping(self) -> bool: + """Checks the mapping attribute by ensuring that the contents are consistent with the partial order over the decision points and outcomes""" + + # convert the decision point values to a list of numerical tuples + # and create a lookup table for the values + # The end result will be that vector contains a list of tuples + # where each tuple is a list of integers that represent the values + # in a decision point. The dp_lookup list will contain a dictionary + # for each decision point that maps the integer values to the string values + vector = [] + dp_lookup = [] + + # create an inverted index for the outcomes + outcome_lookup = {v: k for k, v in self.outcome_group.enumerated_values.items()} + logger.debug(f"Outcome lookup: {outcome_lookup}") + + for dp in self.decision_point_group: + valuesdict = dp.enumerated_values + dp_lookup.append(valuesdict) + + _vlist = tuple(valuesdict.keys()) + vector.append(_vlist) + + # vector now looks like + # [(0, 1, 2), (0, 1), (0, 1, 2, 3)] + logger.debug(f"Vector: {vector}") + # dp_lookup looks like + # [{0: 'a', 1: 'b', 2: 'c'}, {0: 'x', 1: 'y'}, {0: 'i', 1: 'j', 2: 'k', 3: 'l'}] + logger.debug(f"DP lookup: {dp_lookup}") + + bottom = tuple([min(t) for t in vector]) + top = tuple([max(t) for t in vector]) + + logger.debug(f"Bottom node: {bottom}") + logger.debug(f"Top node: {top}") + + # construct a directed graph + G = nx.DiGraph() + G = self._add_nodes(G, vector) + G = self._add_edges(G) + + problems = self._check_graph(G, bottom, top) + + # if we already have problems, we should stop here + # because the graph is not valid + if problems: + for problem in problems: + logger.error(f"Problem detected: {problem}") + return False + + # the graph has a lot of edges where the outcome does not change between the nodes + # we need to find the decision boundaries where the outcome does change + decision_boundaries = self._find_decision_boundaries(G, dp_lookup) + + problems = self._check_decision_boundaries(decision_boundaries, outcome_lookup) + for problem in problems: + logger.error(f"Problem detected: {problem}") + + # return True if there are no problems + return len(problems) == 0 + + def _check_decision_boundaries( + self, decision_boundaries: list[dict[str, str]], outcome_lookup: dict[str, int] + ) -> list[str]: + + problems = [] + for db in decision_boundaries: + from_outcome = outcome_lookup[db["from_outcome"]] + to_outcome = outcome_lookup[db["to_outcome"]] + + if from_outcome < to_outcome: + # this is what we wanted, no problem found + continue + + problem = ( + f"{db['from']} < {db['to']} but {from_outcome} is not < {to_outcome}" + ) + problems.append(problem) + return problems + + def _find_decision_boundaries( + self, G: nx.DiGraph, dp_lookup: list[dict[int, str]] + ) -> list[dict[str, str]]: + decision_boundaries = [] + for edge in G.edges: + u, v = edge + + # we need to translate from a node int tuple to the strings so we can look it up in the mapping + u_str = self.int_node_to_str(u, dp_lookup) + v_str = self.int_node_to_str(v, dp_lookup) + + mapping_dict = dict(self.mapping) + try: + u_outcome_str = mapping_dict[u_str][0] + except KeyError: + print(f"Node {u_str} has no mapping") + + try: + v_outcome_str = mapping_dict[v_str][0] + except KeyError: + logger.error(f"Node {v_str} has no mapping") + raise ValueError + + if u_outcome_str == v_outcome_str: + # no decision boundary here + continue + + # if we got here, there is a decision boundary + # so we need to record it + row = { + "from": u_str, + "to": v_str, + "from_outcome": u_outcome_str, + "to_outcome": v_outcome_str, + } + decision_boundaries.append(row) + + return decision_boundaries + + def int_node_to_str( + self, node: tuple[int, ...], dp_lookup: list[dict[int, str]] + ) -> tuple[str, ...]: + return tuple([dp_lookup[i][node[i]] for i in range(len(node))]) + + def _check_graph( + self, G: nx.DiGraph, bottom: tuple[int, ...], top: tuple[int, ...] + ) -> list[str]: + problems = [] + # check nodes for edges + for node in G.nodes: + if node != bottom and not G.in_degree(node): + # all nodes except bottom should have at least one incoming edge + problems.append(f"Node {node} has no incoming edges") + if node != top and not G.out_degree(node): + # all nodes except top should have at least one outgoing edge + problems.append(f"Node {node} has no outgoing edges") + return problems + + def _add_nodes(self, G: nx.DiGraph, vector: list[tuple[int, ...]]) -> nx.DiGraph: + + for node in itertools.product(*vector): + node = tuple(node) + # node is a tuple of integers + G.add_node(node) + + return G + + def _add_edges(self, G: nx.DiGraph) -> nx.DiGraph: + """ + Add edges to the graph G based on the nodes in G. + Node identities are tuples of integers. + Edges are added between nodes where one and only one element of the tuples differ by 1. + + Examples: + + | Node 1 | Node 2 | Edge? | + |--------|--------|-------| + | (0,0) | (0,1) | Yes | + | (0,0) | (1,0) | Yes | + | (0,0) | (1,1) | No | + | (0,0) | (0,0) | No | + | (0,0) | (0,2) | No | + | (0,1) | (0,2) | Yes | + + Args: + G: a networkx DiGraph object + + Returns: + a networkx DiGraph object with edges added + + """ + # add edges + for u, v in itertools.product(G.nodes, G.nodes): + # enforce that u and v are tuples of integers + if not isinstance(u, tuple) or any([not isinstance(i, int) for i in u]): + raise ValueError(f"Node {u} is not an integer tuple") + if not isinstance(v, tuple) or any([not isinstance(i, int) for i in v]): + raise ValueError(f"Node {v} is not an integer tuple") + + # add an edge if the difference between u and v is 1 + if u == v: + # do not create self-reflexive edges + continue + + if any(u[i] > v[i] for i in range(len(u))): + # skip the upper triangle of the connectivity matrix + continue + + # if you get here, we know that u < v, but it could be + # a gap larger than 1 from u to v. + # We only want to add edges where the gap is exactly 1. + + # compute the individual differences for each vector element + delta = [v[i] - u[i] for i in range(len(u))] + + if sum(delta) != 1: + # gap is too large + continue + + if not all([d in [0, 1] for d in delta]): + # more than one element is different + # this would be odd if it happened, but check for it anyway + continue + + # if you get here, then there is exactly one element that is different + # by 1, and the rest are the same + # add the edge + G.add_edge(u, v) + + # clean up the graph before we return it + # the transitive reduction of a graph is a graph with the same + # reachability properties, but with as few edges as possible + # https://en.wikipedia.org/wiki/Transitive_reduction + # in principle, our algorithm above shouldn't create any redundant edges + # so this is more of a belt-and-suspenders approach + before = len(G.edges) + G = nx.transitive_reduction(G) + after = len(G.edges) + if before != after: + logger.warning(f"Transitive reduction removed {before - after} edges") + logger.debug(f"Edge count: {after}") + return G + + def mapping_to_csv_str(self): + """ + Returns the mapping as a CSV string + """ + columns = [dp.str for dp in self.decision_point_group] + columns.append(self.outcome_group.str) + + rows = [] + for dpstrs, ostrs in self.mapping: + row = list(dpstrs) + row.append(ostrs[0]) + rows.append(row) + + return pd.DataFrame(rows, columns=columns).to_csv(index=False) + + def load_from_csv_str(self, csv_str: str): + """ + Loads the mapping from a CSV string + """ + # TODO add a mechanism to read a mapping from a CSV file and create a DecisionTable object from it + raise NotImplementedError + + +# convenience alias +Policy = DecisionTable + + +def main(): + from ssvc.dp_groups.ssvc.coordinator_publication import LATEST as dpg + from ssvc.outcomes.ssvc_.publish import PUBLISH as og + + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + + dt = DecisionTable( + name="Example Decision Table", + description="The description for an Example Decision Table", + namespace="x_test", + version="1.0.0", + decision_point_group=dpg, + outcome_group=og, + ) + + df = dt.get_mapping_df() + dt.set_mapping(df) + + print(dt.model_dump_json(indent=2)) + + with open("foo.json", "w") as f: + f.write(dt.model_dump_json()) + + dt.consistency_check_mapping() + + print(dt.mapping_to_csv_str()) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index daf282a7..4f496a83 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -31,6 +31,7 @@ from ssvc._mixins import _Base, _SchemaVersioned from ssvc.decision_points.base import ( DecisionPoint, + ValueSummary, ) @@ -71,7 +72,14 @@ def combination_strings(self) -> Generator[tuple[str, ...], None, None]: """ Return a list of tuples of the value short strings for all combinations of the decision points. """ - value_tuples = [dp.value_summaries_str for dp in self.decision_points] + for combo in self.combinations(): + yield tuple(str(x) for x in combo) + + def combinations(self) -> Generator[tuple[ValueSummary, ...], None, None]: + """ + Return a list of tuples of the value summaries for all combinations of the decision points. + """ + value_tuples = [dp.value_summaries for dp in self.decision_points] for combo in itertools.product(*value_tuples): yield combo diff --git a/src/test/decision_tables/test_base.py b/src/test/decision_tables/test_base.py index 760a3d29..81cbd82b 100644 --- a/src/test/decision_tables/test_base.py +++ b/src/test/decision_tables/test_base.py @@ -16,230 +16,145 @@ # This Software includes and/or makes use of Third-Party Software each # subject to its own license. # DM24-0278 +import os import tempfile import unittest import pandas as pd -from pandas import DataFrame -from ssvc.decision_points.base import ( - DP_REGISTRY, - DecisionPoint, - DecisionPointValue, -) -from ssvc.decision_tables import base -from ssvc.dp_groups.base import DecisionPointGroup -from ssvc.outcomes.base import OutcomeValue +from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_tables.base import DecisionTableBase, MappingRow +from ssvc.dp_groups.base import DecisionPoint, DecisionPointGroup +from ssvc.outcomes.base import OutcomeGroup -class TestDecisionTable(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.TemporaryDirectory() - self.tempdir_path = self.tempdir.name - - DP_REGISTRY.clear() - - dps = [] - for i in range(3): - dpvs = [] - for j in range(3): - dpv = DecisionPointValue( - name=f"Value {i}{j}", - key=f"DP{i}V{j}", - description=f"Decision Point {i} Value {j} Description", - ) - dpvs.append(dpv) - - dp = DecisionPoint( - name=f"Decision Point {i}", - key=f"DP{i}", - description=f"Decision Point {i} Description", - version="1.0.0", - namespace="x_test", - values=tuple(dpvs), - ) - dps.append(dp) +class DummyDecisionPoint(DecisionPoint): + pass + + +class DummyOutcomeGroup(OutcomeGroup): + pass + +class TestDecisionTableBase(unittest.TestCase): + def setUp(self): + # create a temporary directory for testing + self.tmpdir = tempfile.TemporaryDirectory() + + # Create dummy decision point values + self.dp1v1 = DecisionPointValue(name="a", key="a", description="A value") + self.dp1v2 = DecisionPointValue(name="b", key="b", description="B value") + self.dp2v1 = DecisionPointValue(name="x", key="x", description="X value") + self.dp2v2 = DecisionPointValue(name="y", key="y", description="Y value") + # Create dummy decision points and group + self.dp1 = DummyDecisionPoint( + name="dp1", + description="", + version="1.0", + namespace="x_test", + key="dp1", + values=(self.dp1v1, self.dp1v2), + ) + self.dp2 = DummyDecisionPoint( + name="dp2", + description="", + version="1.0", + namespace="x_test", + key="dp2", + values=(self.dp2v1, self.dp2v2), + ) self.dpg = DecisionPointGroup( - name="Decision Point Group", - description="Decision Point Group description", - decision_points=tuple(dps), + name="dpg", + description="", + version="1.0", + namespace="x_test", + decision_points=(self.dp1, self.dp2), ) - - ogvs = [] - for i in range(3): - ogv = OutcomeValue( - name=f"Outcome Value {i}", - key=f"OV{i}", - description=f"Outcome Value {i} description", - ) - ogvs.append(ogv) - - self.og = DecisionPoint( - name="Outcome Group", - key="OG", - description="Outcome Group description", + # Create dummy outcome group + self.ogv1 = DecisionPointValue(name="o1", key="o1", description="Outcome 1") + self.ogv2 = DecisionPointValue(name="o2", key="o2", description="Outcome 2") + self.og = DummyOutcomeGroup( + name="outcome", + description="", + version="1.0", namespace="x_test", - values=tuple(ogvs), + key="outcome", + values=(self.ogv1, self.ogv2), ) - self.dt = base.DecisionTable( - name="foo", - description="foo description", + def tearDown(self): + # clean up the temporary directory + self.tmpdir.cleanup() + + def test_init(self): + dt = DecisionTableBase( + name="Test Table", namespace="x_test", + description="", decision_point_group=self.dpg, outcome_group=self.og, + mapping=None, ) - - def tearDown(self): - self.tempdir.cleanup() - - def test_create(self): - # self.dt exists in setUp - self.assertEqual(self.dt.name, "foo") - self.assertEqual(self.dt.description, "foo description") - self.assertEqual(self.dt.namespace, "x_test") - self.assertEqual(self.dt.decision_point_group, self.dpg) - self.assertEqual(self.dt.outcome_group, self.og) - - def test_get_mapping_df(self): - df = self.dt.get_mapping_df() - self.assertIsInstance(df, DataFrame) - - # df is not empty - self.assertFalse(df.empty) - # df has some rows - self.assertGreater(len(df), 0) - # df has the same number of rows as the product of the number of decision points and their values - combos = list(self.dpg.combination_strings()) - self.assertGreater(len(combos), 0) - self.assertEqual(len(df), len(combos)) - - # column names are the decision point strings and the outcome group string - for i, dp in enumerate(self.dpg.decision_points): - self.assertEqual(dp.str, df.columns[i]) - self.assertEqual(self.og.str, df.columns[-1]) - - for col in df.columns: - # col is in the registry - self.assertIn(col, DP_REGISTRY) - - dp = DP_REGISTRY[col] - - uniq = df[col].unique() - - # all values in the decision point should be in the column at least once - for vsum in dp.value_summaries_str: - self.assertIn(vsum, uniq) - - def test_dataframe_to_tuple_list(self): - df = pd.DataFrame( - [ - {"a": 1, "b": 2, "c": 3}, - {"a": 4, "b": 5, "c": 6}, - {"a": 7, "b": 8, "c": 9}, - ] - ) - - tuple_list = self.dt.dataframe_to_tuple_list(df) - - self.assertEqual(len(tuple_list), len(df)) - - for row in tuple_list: - self.assertIsInstance(row, tuple) - self.assertEqual(len(row), 2) - self.assertIsInstance(row[0], tuple) - self.assertIsInstance(row[1], tuple) - - # manually check the structure of the tuple list - self.assertEqual((1, 2), tuple_list[0][0]) - self.assertEqual((3,), tuple_list[0][1]) - self.assertEqual((4, 5), tuple_list[1][0]) - self.assertEqual((6,), tuple_list[1][1]) - self.assertEqual((7, 8), tuple_list[2][0]) - self.assertEqual((9,), tuple_list[2][1]) - - def test_set_mapping(self): - df = pd.DataFrame( - { - "a": ["x", "y", "z"], - "b": ["one", "two", "three"], - "c": ["apple", "orange", "pear"], - } + self.assertEqual(dt.decision_point_group, self.dpg) + self.assertEqual(dt.outcome_group, self.og) + + # default should be to populate mapping if not provided + self.assertIsNotNone(dt.mapping) + # mapping length should match product of decision point values + combos = list(self.dpg.combinations()) + expected_length = len(combos) + self.assertEqual(len(dt.mapping), expected_length) + # Check if mapping is a list of MappingRow objects + for row in dt.mapping: + self.assertIsInstance(row, MappingRow) + # We aren't testing the actual values here, just that they are created + # correctly. The mappings will be tested in more detail in other tests. + + def test_to_csv(self): + dt = DecisionTableBase( + name="Test Table", + namespace="x_test", + description="", + decision_point_group=self.dpg, + outcome_group=self.og, + mapping=None, ) - - result = self.dt.set_mapping(df) - - self.assertIn((("x", "one"), ("apple",)), result) - self.assertIn((("y", "two"), ("orange",)), result) - self.assertIn((("z", "three"), ("pear",)), result) - - self.assertEqual(result, self.dt.mapping) - - @unittest.skip("Test not implemented") - def test_consistency_check_mapping(self): - pass - - @unittest.skip("Test not implemented") - def test_check_decision_boundaries(self): - pass - - @unittest.skip("Test not implemented") - def test_find_decision_boundaries(self): - pass - - @unittest.skip("Test not implemented") - def test_int_node_to_str(self): - pass - - @unittest.skip("Test not implemented") - def test_check_graph(self): - pass - - @unittest.skip("Test not implemented") - def test_add_nodes(self): - pass - - @unittest.skip("Test not implemented") - def test_add_edges(self): - pass - - def test_mapping_to_csv_str(self): - df = self.dt.get_mapping_df() - self.dt.set_mapping(df) - - csv_str = self.dt.mapping_to_csv_str() - self.assertIsInstance(csv_str, str) - - lines = csv_str.strip().split("\n") - - # first line is the header - self.assertEqual( - lines[0], - ",".join([dp.str for dp in self.dpg.decision_points] + [self.og.str]), + csv_str = dt.to_csv() + + # write csv to a temporary file + csvfile = os.path.join(self.tmpdir.name, "test_table.csv") + with open(csvfile, "w") as f: + f.write(csv_str) + + # read the csv file into a DataFrame + # using pandas + df = pd.read_csv(csvfile) + + # does line count match expected? + expected_lines = len(dt.mapping) # for header + self.assertEqual(len(df), expected_lines) + # Check if the DataFrame has the expected columns + + expected_columns = [ + "dp1", + "dp2", + "outcome", + ] + self.assertTrue(all(col in df.columns for col in expected_columns)) + + def test_model_dump_json(self): + dt = DecisionTableBase( + name="Test Table", + namespace="x_test", + description="", + decision_point_group=self.dpg, + outcome_group=self.og, + mapping=None, ) - - combinations = list(self.dpg.combination_strings()) - - # there should be one line for each combination after the header - self.assertEqual(len(lines[1:]), len(combinations)) - - # each line after the header starts with a combination of decision point values - for combo, line in zip(combinations, lines[1:]): - # there are as many commas in the line as there are combos - comma_count = line.count(",") - self.assertEqual(comma_count, len(combo)) - - for dpv_str in combo: - self.assertIn(dpv_str, line) - - # the last thing in the line is the outcome group value - og_value = line.split(",")[-1] - self.assertIn(og_value, self.og.value_summaries_str) - - @unittest.skip("Test not implemented") - def test_load_from_csv_str(self): - pass + json_str = dt.model_dump_json() + self.assertIn("decision_point_group", json_str) + self.assertIn("outcome_group", json_str) + self.assertIn("mapping", json_str) if __name__ == "__main__": diff --git a/src/test/decision_tables/test_experimental_base.py b/src/test/decision_tables/test_experimental_base.py new file mode 100644 index 00000000..288bbcbf --- /dev/null +++ b/src/test/decision_tables/test_experimental_base.py @@ -0,0 +1,280 @@ +# 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 +import tempfile +import unittest + +import pandas as pd +from pandas import DataFrame + +from ssvc.decision_points.base import ( + DP_REGISTRY, + DecisionPoint, + DecisionPointValue, +) +from ssvc.decision_tables import experimental_base as base +from ssvc.dp_groups.base import DecisionPointGroup +from ssvc.outcomes.base import OutcomeValue + + +class TestDecisionTable(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.tempdir_path = self.tempdir.name + + DP_REGISTRY.clear() + + dps = [] + for i in range(3): + dpvs = [] + for j in range(3): + dpv = DecisionPointValue( + name=f"Value {i}{j}", + key=f"DP{i}V{j}", + description=f"Decision Point {i} Value {j} Description", + ) + dpvs.append(dpv) + + dp = DecisionPoint( + name=f"Decision Point {i}", + key=f"DP{i}", + description=f"Decision Point {i} Description", + version="1.0.0", + namespace="x_test", + values=tuple(dpvs), + ) + dps.append(dp) + + self.dpg = DecisionPointGroup( + name="Decision Point Group", + description="Decision Point Group description", + decision_points=tuple(dps), + ) + + ogvs = [] + for i in range(3): + ogv = OutcomeValue( + name=f"Outcome Value {i}", + key=f"OV{i}", + description=f"Outcome Value {i} description", + ) + ogvs.append(ogv) + + self.og = DecisionPoint( + name="Outcome Group", + key="OG", + description="Outcome Group description", + namespace="x_test", + values=tuple(ogvs), + ) + + self.dt = base.DecisionTable( + name="foo", + description="foo description", + namespace="x_test", + decision_point_group=self.dpg, + outcome_group=self.og, + ) + + def tearDown(self): + self.tempdir.cleanup() + + def test_create(self): + # self.dt exists in setUp + self.assertEqual(self.dt.name, "foo") + self.assertEqual(self.dt.description, "foo description") + self.assertEqual(self.dt.namespace, "x_test") + self.assertEqual(self.dt.decision_point_group, self.dpg) + self.assertEqual(self.dt.outcome_group, self.og) + + def test_get_mapping_df(self): + df = self.dt.get_mapping_df() + self.assertIsInstance(df, DataFrame) + + # df is not empty + self.assertFalse(df.empty) + # df has some rows + self.assertGreater(len(df), 0) + # df has the same number of rows as the product of the number of decision points and their values + combos = list(self.dpg.combination_strings()) + self.assertGreater(len(combos), 0) + self.assertEqual(len(df), len(combos)) + + # column names are the decision point strings and the outcome group string + for i, dp in enumerate(self.dpg.decision_points): + self.assertEqual(dp.str, df.columns[i]) + self.assertEqual(self.og.str, df.columns[-1]) + + for col in df.columns: + # col is in the registry + self.assertIn(col, DP_REGISTRY) + + dp = DP_REGISTRY[col] + + uniq = df[col].unique() + + # all values in the decision point should be in the column at least once + for vsum in dp.value_summaries_str: + self.assertIn(vsum, uniq) + + def test_dataframe_to_tuple_list(self): + df = pd.DataFrame( + [ + {"a": 1, "b": 2, "c": 3}, + {"a": 4, "b": 5, "c": 6}, + {"a": 7, "b": 8, "c": 9}, + ] + ) + + tuple_list = self.dt.dataframe_to_tuple_list(df) + + self.assertEqual(len(tuple_list), len(df)) + + for row in tuple_list: + self.assertIsInstance(row, tuple) + self.assertEqual(len(row), 2) + self.assertIsInstance(row[0], tuple) + self.assertIsInstance(row[1], tuple) + + # manually check the structure of the tuple list + self.assertEqual((1, 2), tuple_list[0][0]) + self.assertEqual((3,), tuple_list[0][1]) + self.assertEqual((4, 5), tuple_list[1][0]) + self.assertEqual((6,), tuple_list[1][1]) + self.assertEqual((7, 8), tuple_list[2][0]) + self.assertEqual((9,), tuple_list[2][1]) + + def test_set_mapping(self): + df = pd.DataFrame( + { + "a": ["x", "y", "z"], + "b": ["one", "two", "three"], + "c": ["apple", "orange", "pear"], + } + ) + + result = self.dt.set_mapping(df) + + self.assertIn((("x", "one"), ("apple",)), result) + self.assertIn((("y", "two"), ("orange",)), result) + self.assertIn((("z", "three"), ("pear",)), result) + + self.assertEqual(result, self.dt.mapping) + + @unittest.skip("Test not implemented") + def test_consistency_check_mapping(self): + pass + + @unittest.skip("Test not implemented") + def test_check_decision_boundaries(self): + pass + + @unittest.skip("Test not implemented") + def test_find_decision_boundaries(self): + pass + + @unittest.skip("Test not implemented") + def test_int_node_to_str(self): + pass + + @unittest.skip("Test not implemented") + def test_check_graph(self): + pass + + @unittest.skip("Test not implemented") + def test_add_nodes(self): + pass + + @unittest.skip("Test not implemented") + def test_add_edges(self): + pass + + def test_mapping_to_csv_str(self): + df = self.dt.get_mapping_df() + self.dt.set_mapping(df) + + csv_str = self.dt.mapping_to_csv_str() + self.assertIsInstance(csv_str, str) + + lines = csv_str.strip().split("\n") + + # first line is the header + self.assertEqual( + lines[0], + ",".join([dp.str for dp in self.dpg.decision_points] + [self.og.str]), + ) + + combinations = list(self.dpg.combination_strings()) + + # there should be one line for each combination after the header + self.assertEqual(len(lines[1:]), len(combinations)) + + # each line after the header starts with a combination of decision point values + for combo, line in zip(combinations, lines[1:]): + # there are as many commas in the line as there are combos + comma_count = line.count(",") + self.assertEqual(comma_count, len(combo)) + + for dpv_str in combo: + self.assertIn(dpv_str, line) + + # the last thing in the line is the outcome group value + og_value = line.split(",")[-1] + self.assertIn(og_value, self.og.value_summaries_str) + + @unittest.skip("Method not implemented") + def test_load_from_csv_str(self): + df = self.dt.get_mapping_df() + self.dt.set_mapping(df) + + csv_str = self.dt.mapping_to_csv_str() + + print("Original CSV String:") + print(csv_str) + # modify the csv_str to simulate a new mapping + # replace "OV2" with "OV1" + modified_csv_str = csv_str.replace("OV2", "OV1") + # confirm that "OV2" is no longer in the modified string + self.assertNotIn("OV2", modified_csv_str) + print("Modified CSV String:") + print(modified_csv_str) + # load the modified mapping from the CSV string + new_dt = base.DecisionTable.load_from_csv_str(csv_str=modified_csv_str) + self.assertIsInstance(new_dt, base.DecisionTable) + self.assertEqual(new_dt.name, self.dt.name) + self.assertEqual(new_dt.description, self.dt.description) + self.assertEqual(new_dt.namespace, self.dt.namespace) + self.assertEqual(new_dt.decision_point_group, self.dpg) + self.assertEqual(new_dt.outcome_group, self.og) + # check that the mapping is the same as the modified CSV string + new_mapping = new_dt.get_mapping_df() + self.assertIsInstance(new_mapping, DataFrame) + self.assertFalse(new_mapping.empty) + # check that the modified value is in the mapping + # for each row in the old mapping, the new mapping should have the same values + # original: OV0 = new: OV1 + # original: OV1 = new: OV1 + # original: OV2 = new: OV1 + for index, row in df.iterrows(): + print(row) + + self.assertIn("OV1", new_mapping[self.og.str].unique()) + + +if __name__ == "__main__": + unittest.main() From 7676a663180e864f29f208cfa6f186b1e7af869d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 13 Jun 2025 16:09:22 -0400 Subject: [PATCH 88/99] use namespace enum rather than hard-coded string for namespace in cisa objects --- src/ssvc/decision_points/cisa/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ssvc/decision_points/cisa/base.py b/src/ssvc/decision_points/cisa/base.py index 76e10368..342c37a9 100644 --- a/src/ssvc/decision_points/cisa/base.py +++ b/src/ssvc/decision_points/cisa/base.py @@ -23,7 +23,8 @@ from pydantic import BaseModel from ssvc.decision_points.base import DecisionPoint +from ssvc.namespaces import NameSpace class CisaDecisionPoint(DecisionPoint, BaseModel): - namespace: str = "cisa" + namespace: str = NameSpace.CISA From 6be9d7b4baec8cf39c17773a913b47ede9cbfc00 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Mon, 16 Jun 2025 15:22:41 -0400 Subject: [PATCH 89/99] refactoring branches --- src/ssvc/decision_tables/__init__.py | 12 - src/ssvc/decision_tables/base.py | 218 ---------- src/ssvc/decision_tables/experimental_base.py | 399 ------------------ src/test/decision_tables/__init__.py | 15 - src/test/decision_tables/test_base.py | 161 ------- .../decision_tables/test_experimental_base.py | 280 ------------ 6 files changed, 1085 deletions(-) delete mode 100644 src/ssvc/decision_tables/__init__.py delete mode 100644 src/ssvc/decision_tables/base.py delete mode 100644 src/ssvc/decision_tables/experimental_base.py delete mode 100644 src/test/decision_tables/__init__.py delete mode 100644 src/test/decision_tables/test_base.py delete mode 100644 src/test/decision_tables/test_experimental_base.py diff --git a/src/ssvc/decision_tables/__init__.py b/src/ssvc/decision_tables/__init__.py deleted file mode 100644 index b9c0e1cc..00000000 --- a/src/ssvc/decision_tables/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University diff --git a/src/ssvc/decision_tables/base.py b/src/ssvc/decision_tables/base.py deleted file mode 100644 index b85e1b72..00000000 --- a/src/ssvc/decision_tables/base.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python -""" -DecisionTableBase: A flexible, serializable SSVC decision table model. -""" -# 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 - -import logging -from itertools import product -from typing import List, Optional - -import pandas as pd -from pydantic import BaseModel, model_validator - -from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned -from ssvc.decision_points.base import ValueSummary -from ssvc.dp_groups.base import DecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup - -logger = logging.getLogger(__name__) - - -class ValueCombo(BaseModel): - values: tuple[ValueSummary, ...] - - -class MappingRow(BaseModel): - decision_point_values: ValueCombo - outcome: Optional[ValueSummary] - - -class DecisionTableBase(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): - """ - DecisionTableBase: A flexible, serializable SSVC decision table model. - """ - - decision_point_group: DecisionPointGroup - outcome_group: OutcomeGroup - mapping: Optional[List[MappingRow]] = None - - @model_validator(mode="after") - def populate_mapping_if_none(self): - - if self.mapping is not None: - # short-circuit if mapping is already set - return self - - dpg = self.decision_point_group - og = self.outcome_group - if dpg is not None and og is not None: - mapping = self.generate_full_mapping() - outcome_values = getattr(og, "value_summaries", None) - if outcome_values: - mapping = distribute_outcomes_evenly(mapping, outcome_values) - else: - raise ValueError( - "Outcome group must have value_summaries to distribute outcomes." - ) - self.mapping = mapping - return self - - def to_csv(self) -> str: - """ - Export the mapping to a CSV string. Columns: one per decision point (by name), one for outcome. - Indiviual decision point and outcome values are represented as a colon-separated tuple - consisting of the namespace, decision point key, decision point version, and value key. - """ - if not self.mapping: - raise ValueError("No mapping to export.") - dp_names = [dp.name for dp in self.decision_point_group.decision_points] - outcome_name = self.outcome_group.name - rows = [] - for row in self.mapping: - row_dict = { - name: str(val) - for name, val in zip(dp_names, row.decision_point_values.values) - } - row_dict[outcome_name] = str(row.outcome) if row.outcome else "" - rows.append(row_dict) - df = pd.DataFrame(rows, columns=dp_names + [outcome_name]) - return df.to_csv(index=False) - - def generate_full_mapping(self) -> List[MappingRow]: - """ - Generate a full mapping for the decision table, with every possible combination of decision point values. - Each MappingRow will have a ValueCombo of ValueSummary objects, and outcome=None. - """ - if self.mapping is not None: - # short-circuit if mapping is already set - logger.warning("Mapping is already set, skipping full generation.") - return self.mapping - - logger.debug("Generating full mapping.") - self.mapping = generate_full_mapping(self) - - return self.mapping - - def distribute_outcomes_evenly(self, overwrite: bool = False) -> List[MappingRow]: - """ - Distribute the given outcome_values across the mapping rows in sorted order. - The earliest mappings get the lowest outcome, the latest get the highest. - If the mapping count is not divisible by the number of outcomes, the last outcome(s) get the remainder. - Returns a new list of MappingRow with outcomes assigned. - """ - if self.mapping is not None: - if not overwrite: - # short-circuit if mapping is already set - logger.warning("Mapping is already set, skipping distribution.") - return self.mapping - else: - logger.info("Overwriting existing mapping with new distribution.") - - self.generate_full_mapping() - - outcome_values = getattr(self.outcome_group, "value_summaries", None) - if outcome_values is None: - raise ValueError( - "Outcome group must have value_summaries to distribute outcomes." - ) - - if mapping is None: - self.mapping = distribute_outcomes_evenly(outcome_values) - - -def generate_full_mapping(decision_table: "DecisionTableBase") -> list[MappingRow]: - """ - Generate a full mapping for the decision table, with every possible combination of decision point values. - Each MappingRow will have a ValueCombo of ValueSummary objects, and outcome=None. - """ - dp_group = decision_table.decision_point_group - # For each decision point, get its value summaries - value_lists = [ - [ - ValueSummary( - key=dp.key, - version=dp.version, - namespace=dp.namespace, - value=val.key, - ) - for val in dp.values - ] - for dp in dp_group.decision_points - ] - all_combos = product(*value_lists) - mapping = [ - MappingRow(decision_point_values=ValueCombo(values=combo), outcome=None) - for combo in all_combos - ] - return mapping - - -def distribute_outcomes_evenly( - mapping: list[MappingRow], outcome_values: list[ValueSummary] -) -> list[MappingRow]: - """ - Distribute the given outcome_values across the mapping rows in sorted order. - The earliest mappings get the lowest outcome, the latest get the highest. - If the mapping count is not divisible by the number of outcomes, the last outcome(s) get the remainder. - Returns a new list of MappingRow with outcomes assigned. - """ - if not outcome_values: - raise ValueError("No outcome values provided for distribution.") - n = len(mapping) - k = len(outcome_values) - base = n // k - rem = n % k - new_mapping = [] - idx = 0 - for i, outcome in enumerate(outcome_values): - count = base + (1 if i < rem else 0) - for _ in range(count): - if idx >= n: - break - row = mapping[idx] - new_mapping.append( - MappingRow( - decision_point_values=row.decision_point_values, outcome=outcome - ) - ) - idx += 1 - return new_mapping - - -def main() -> None: - from ssvc.dp_groups.ssvc.coordinator_triage import LATEST as dpg - from ssvc.outcomes.x_basic.mscw import LATEST as outcomes - - table = DecisionTableBase( - name="Test Table", - description="A test decision table", - namespace="x_test", - decision_point_group=dpg, - outcome_group=outcomes, - ) - table.mapping = generate_full_mapping(table) - table.mapping = distribute_outcomes_evenly(table.mapping, outcomes.value_summaries) - csv_str = table.to_csv() - print(csv_str) - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/decision_tables/experimental_base.py b/src/ssvc/decision_tables/experimental_base.py deleted file mode 100644 index 7d3034df..00000000 --- a/src/ssvc/decision_tables/experimental_base.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python - -# 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 a DecisionTable class that can be used to model decisions in SSVC -""" -import itertools -import logging -from typing import Optional - -import networkx as nx -import pandas as pd -from pydantic import BaseModel, Field - -from ssvc._mixins import _Base, _Commented, _Namespaced, _SchemaVersioned -from ssvc.dp_groups.base import DecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup -from ssvc.policy_generator import PolicyGenerator - -logger = logging.getLogger(__name__) - - -class DecisionTable(_SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel): - """ - The DecisionTable class is a model for decisions in SSVC. - - It is a collection of decision points and outcomes, and a mapping of decision points to outcomes. - - The mapping is generated by the PolicyGenerator class, and stored as a dictionary. - The mapping dict keys are tuples of decision points and decision point values. - The mapping dict values are outcomes. - """ - - decision_point_group: DecisionPointGroup - outcome_group: OutcomeGroup - mapping: list[tuple[tuple[str, ...], tuple[str, ...]]] = Field(default_factory=list) - - def get_mapping_df(self, weights: Optional[list[float]] = None) -> pd.DataFrame: - # create a policy generator object and extract a mapping from it - with PolicyGenerator( - dp_group=self.decision_point_group, - outcomes=self.outcome_group, - outcome_weights=weights, - ) as pg: - df = pg.clean_policy() - - return df - - def dataframe_to_tuple_list( - self, df: pd.DataFrame, n_outcols: int = 1 - ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: - """ - Converts a DataFrame into a list of tuples where each tuple contains: - - A tuple of input values (all columns except the last n_outcols) - - A tuple containing the outcome value (last n_outcols columns) - - Note: - In every decision we've modeled to date, there is only one outcome column. - We have not yet encountered a decision with multiple outcome columns, - however, it has come up in some discussions, so we're allowing for it here as - a future-proofing measure. - - Attributes: - df: pandas DataFrame - n_outcols: int, default=1 - - Returns: - list[tuple[tuple[str,...],tuple[str,...]]]: A list of tuples - - """ - input_columns = df.columns[:-n_outcols] # All columns except the last one - output_column = df.columns[-n_outcols] # The last column - - return [ - (tuple(row[input_columns]), (row[output_column],)) - for _, row in df.iterrows() - ] - - def set_mapping( - self, df: pd.DataFrame - ) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: - """ - Sets the mapping attribute to the output of dataframe_to_tuple_list - - :param df: pandas DataFrame - """ - self.mapping = self.dataframe_to_tuple_list(df) - return self.mapping - - def generate_mapping(self) -> list[tuple[tuple[str, ...], tuple[str, ...]]]: - """ - Generates a mapping from the decision point group to the outcome group using the PolicyGenerator class - and sets the mapping attribute to the output of dataframe_to_tuple_list - - Returns: - list[tuple[tuple[str,...],tuple[str,...]]]: The generated mapping - """ - df = self.get_mapping_df() - return self.set_mapping(df) - - def consistency_check_mapping(self) -> bool: - """Checks the mapping attribute by ensuring that the contents are consistent with the partial order over the decision points and outcomes""" - - # convert the decision point values to a list of numerical tuples - # and create a lookup table for the values - # The end result will be that vector contains a list of tuples - # where each tuple is a list of integers that represent the values - # in a decision point. The dp_lookup list will contain a dictionary - # for each decision point that maps the integer values to the string values - vector = [] - dp_lookup = [] - - # create an inverted index for the outcomes - outcome_lookup = {v: k for k, v in self.outcome_group.enumerated_values.items()} - logger.debug(f"Outcome lookup: {outcome_lookup}") - - for dp in self.decision_point_group: - valuesdict = dp.enumerated_values - dp_lookup.append(valuesdict) - - _vlist = tuple(valuesdict.keys()) - vector.append(_vlist) - - # vector now looks like - # [(0, 1, 2), (0, 1), (0, 1, 2, 3)] - logger.debug(f"Vector: {vector}") - # dp_lookup looks like - # [{0: 'a', 1: 'b', 2: 'c'}, {0: 'x', 1: 'y'}, {0: 'i', 1: 'j', 2: 'k', 3: 'l'}] - logger.debug(f"DP lookup: {dp_lookup}") - - bottom = tuple([min(t) for t in vector]) - top = tuple([max(t) for t in vector]) - - logger.debug(f"Bottom node: {bottom}") - logger.debug(f"Top node: {top}") - - # construct a directed graph - G = nx.DiGraph() - G = self._add_nodes(G, vector) - G = self._add_edges(G) - - problems = self._check_graph(G, bottom, top) - - # if we already have problems, we should stop here - # because the graph is not valid - if problems: - for problem in problems: - logger.error(f"Problem detected: {problem}") - return False - - # the graph has a lot of edges where the outcome does not change between the nodes - # we need to find the decision boundaries where the outcome does change - decision_boundaries = self._find_decision_boundaries(G, dp_lookup) - - problems = self._check_decision_boundaries(decision_boundaries, outcome_lookup) - for problem in problems: - logger.error(f"Problem detected: {problem}") - - # return True if there are no problems - return len(problems) == 0 - - def _check_decision_boundaries( - self, decision_boundaries: list[dict[str, str]], outcome_lookup: dict[str, int] - ) -> list[str]: - - problems = [] - for db in decision_boundaries: - from_outcome = outcome_lookup[db["from_outcome"]] - to_outcome = outcome_lookup[db["to_outcome"]] - - if from_outcome < to_outcome: - # this is what we wanted, no problem found - continue - - problem = ( - f"{db['from']} < {db['to']} but {from_outcome} is not < {to_outcome}" - ) - problems.append(problem) - return problems - - def _find_decision_boundaries( - self, G: nx.DiGraph, dp_lookup: list[dict[int, str]] - ) -> list[dict[str, str]]: - decision_boundaries = [] - for edge in G.edges: - u, v = edge - - # we need to translate from a node int tuple to the strings so we can look it up in the mapping - u_str = self.int_node_to_str(u, dp_lookup) - v_str = self.int_node_to_str(v, dp_lookup) - - mapping_dict = dict(self.mapping) - try: - u_outcome_str = mapping_dict[u_str][0] - except KeyError: - print(f"Node {u_str} has no mapping") - - try: - v_outcome_str = mapping_dict[v_str][0] - except KeyError: - logger.error(f"Node {v_str} has no mapping") - raise ValueError - - if u_outcome_str == v_outcome_str: - # no decision boundary here - continue - - # if we got here, there is a decision boundary - # so we need to record it - row = { - "from": u_str, - "to": v_str, - "from_outcome": u_outcome_str, - "to_outcome": v_outcome_str, - } - decision_boundaries.append(row) - - return decision_boundaries - - def int_node_to_str( - self, node: tuple[int, ...], dp_lookup: list[dict[int, str]] - ) -> tuple[str, ...]: - return tuple([dp_lookup[i][node[i]] for i in range(len(node))]) - - def _check_graph( - self, G: nx.DiGraph, bottom: tuple[int, ...], top: tuple[int, ...] - ) -> list[str]: - problems = [] - # check nodes for edges - for node in G.nodes: - if node != bottom and not G.in_degree(node): - # all nodes except bottom should have at least one incoming edge - problems.append(f"Node {node} has no incoming edges") - if node != top and not G.out_degree(node): - # all nodes except top should have at least one outgoing edge - problems.append(f"Node {node} has no outgoing edges") - return problems - - def _add_nodes(self, G: nx.DiGraph, vector: list[tuple[int, ...]]) -> nx.DiGraph: - - for node in itertools.product(*vector): - node = tuple(node) - # node is a tuple of integers - G.add_node(node) - - return G - - def _add_edges(self, G: nx.DiGraph) -> nx.DiGraph: - """ - Add edges to the graph G based on the nodes in G. - Node identities are tuples of integers. - Edges are added between nodes where one and only one element of the tuples differ by 1. - - Examples: - - | Node 1 | Node 2 | Edge? | - |--------|--------|-------| - | (0,0) | (0,1) | Yes | - | (0,0) | (1,0) | Yes | - | (0,0) | (1,1) | No | - | (0,0) | (0,0) | No | - | (0,0) | (0,2) | No | - | (0,1) | (0,2) | Yes | - - Args: - G: a networkx DiGraph object - - Returns: - a networkx DiGraph object with edges added - - """ - # add edges - for u, v in itertools.product(G.nodes, G.nodes): - # enforce that u and v are tuples of integers - if not isinstance(u, tuple) or any([not isinstance(i, int) for i in u]): - raise ValueError(f"Node {u} is not an integer tuple") - if not isinstance(v, tuple) or any([not isinstance(i, int) for i in v]): - raise ValueError(f"Node {v} is not an integer tuple") - - # add an edge if the difference between u and v is 1 - if u == v: - # do not create self-reflexive edges - continue - - if any(u[i] > v[i] for i in range(len(u))): - # skip the upper triangle of the connectivity matrix - continue - - # if you get here, we know that u < v, but it could be - # a gap larger than 1 from u to v. - # We only want to add edges where the gap is exactly 1. - - # compute the individual differences for each vector element - delta = [v[i] - u[i] for i in range(len(u))] - - if sum(delta) != 1: - # gap is too large - continue - - if not all([d in [0, 1] for d in delta]): - # more than one element is different - # this would be odd if it happened, but check for it anyway - continue - - # if you get here, then there is exactly one element that is different - # by 1, and the rest are the same - # add the edge - G.add_edge(u, v) - - # clean up the graph before we return it - # the transitive reduction of a graph is a graph with the same - # reachability properties, but with as few edges as possible - # https://en.wikipedia.org/wiki/Transitive_reduction - # in principle, our algorithm above shouldn't create any redundant edges - # so this is more of a belt-and-suspenders approach - before = len(G.edges) - G = nx.transitive_reduction(G) - after = len(G.edges) - if before != after: - logger.warning(f"Transitive reduction removed {before - after} edges") - logger.debug(f"Edge count: {after}") - return G - - def mapping_to_csv_str(self): - """ - Returns the mapping as a CSV string - """ - columns = [dp.str for dp in self.decision_point_group] - columns.append(self.outcome_group.str) - - rows = [] - for dpstrs, ostrs in self.mapping: - row = list(dpstrs) - row.append(ostrs[0]) - rows.append(row) - - return pd.DataFrame(rows, columns=columns).to_csv(index=False) - - def load_from_csv_str(self, csv_str: str): - """ - Loads the mapping from a CSV string - """ - # TODO add a mechanism to read a mapping from a CSV file and create a DecisionTable object from it - raise NotImplementedError - - -# convenience alias -Policy = DecisionTable - - -def main(): - from ssvc.dp_groups.ssvc.coordinator_publication import LATEST as dpg - from ssvc.outcomes.ssvc_.publish import PUBLISH as og - - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) - - dt = DecisionTable( - name="Example Decision Table", - description="The description for an Example Decision Table", - namespace="x_test", - version="1.0.0", - decision_point_group=dpg, - outcome_group=og, - ) - - df = dt.get_mapping_df() - dt.set_mapping(df) - - print(dt.model_dump_json(indent=2)) - - with open("foo.json", "w") as f: - f.write(dt.model_dump_json()) - - dt.consistency_check_mapping() - - print(dt.mapping_to_csv_str()) - - -if __name__ == "__main__": - main() diff --git a/src/test/decision_tables/__init__.py b/src/test/decision_tables/__init__.py deleted file mode 100644 index 3a081023..00000000 --- a/src/test/decision_tables/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -""" -Provides test classes for ssvc.decision_tables. -""" diff --git a/src/test/decision_tables/test_base.py b/src/test/decision_tables/test_base.py deleted file mode 100644 index 81cbd82b..00000000 --- a/src/test/decision_tables/test_base.py +++ /dev/null @@ -1,161 +0,0 @@ -# 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 -import os -import tempfile -import unittest - -import pandas as pd - -from ssvc.decision_points.base import DecisionPointValue -from ssvc.decision_tables.base import DecisionTableBase, MappingRow -from ssvc.dp_groups.base import DecisionPoint, DecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup - - -class DummyDecisionPoint(DecisionPoint): - pass - - -class DummyOutcomeGroup(OutcomeGroup): - pass - - -class TestDecisionTableBase(unittest.TestCase): - def setUp(self): - # create a temporary directory for testing - self.tmpdir = tempfile.TemporaryDirectory() - - # Create dummy decision point values - self.dp1v1 = DecisionPointValue(name="a", key="a", description="A value") - self.dp1v2 = DecisionPointValue(name="b", key="b", description="B value") - self.dp2v1 = DecisionPointValue(name="x", key="x", description="X value") - self.dp2v2 = DecisionPointValue(name="y", key="y", description="Y value") - # Create dummy decision points and group - self.dp1 = DummyDecisionPoint( - name="dp1", - description="", - version="1.0", - namespace="x_test", - key="dp1", - values=(self.dp1v1, self.dp1v2), - ) - self.dp2 = DummyDecisionPoint( - name="dp2", - description="", - version="1.0", - namespace="x_test", - key="dp2", - values=(self.dp2v1, self.dp2v2), - ) - self.dpg = DecisionPointGroup( - name="dpg", - description="", - version="1.0", - namespace="x_test", - decision_points=(self.dp1, self.dp2), - ) - # Create dummy outcome group - self.ogv1 = DecisionPointValue(name="o1", key="o1", description="Outcome 1") - self.ogv2 = DecisionPointValue(name="o2", key="o2", description="Outcome 2") - self.og = DummyOutcomeGroup( - name="outcome", - description="", - version="1.0", - namespace="x_test", - key="outcome", - values=(self.ogv1, self.ogv2), - ) - - def tearDown(self): - # clean up the temporary directory - self.tmpdir.cleanup() - - def test_init(self): - dt = DecisionTableBase( - name="Test Table", - namespace="x_test", - description="", - decision_point_group=self.dpg, - outcome_group=self.og, - mapping=None, - ) - self.assertEqual(dt.decision_point_group, self.dpg) - self.assertEqual(dt.outcome_group, self.og) - - # default should be to populate mapping if not provided - self.assertIsNotNone(dt.mapping) - # mapping length should match product of decision point values - combos = list(self.dpg.combinations()) - expected_length = len(combos) - self.assertEqual(len(dt.mapping), expected_length) - # Check if mapping is a list of MappingRow objects - for row in dt.mapping: - self.assertIsInstance(row, MappingRow) - # We aren't testing the actual values here, just that they are created - # correctly. The mappings will be tested in more detail in other tests. - - def test_to_csv(self): - dt = DecisionTableBase( - name="Test Table", - namespace="x_test", - description="", - decision_point_group=self.dpg, - outcome_group=self.og, - mapping=None, - ) - csv_str = dt.to_csv() - - # write csv to a temporary file - csvfile = os.path.join(self.tmpdir.name, "test_table.csv") - with open(csvfile, "w") as f: - f.write(csv_str) - - # read the csv file into a DataFrame - # using pandas - df = pd.read_csv(csvfile) - - # does line count match expected? - expected_lines = len(dt.mapping) # for header - self.assertEqual(len(df), expected_lines) - # Check if the DataFrame has the expected columns - - expected_columns = [ - "dp1", - "dp2", - "outcome", - ] - self.assertTrue(all(col in df.columns for col in expected_columns)) - - def test_model_dump_json(self): - dt = DecisionTableBase( - name="Test Table", - namespace="x_test", - description="", - decision_point_group=self.dpg, - outcome_group=self.og, - mapping=None, - ) - json_str = dt.model_dump_json() - self.assertIn("decision_point_group", json_str) - self.assertIn("outcome_group", json_str) - self.assertIn("mapping", json_str) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/decision_tables/test_experimental_base.py b/src/test/decision_tables/test_experimental_base.py deleted file mode 100644 index 288bbcbf..00000000 --- a/src/test/decision_tables/test_experimental_base.py +++ /dev/null @@ -1,280 +0,0 @@ -# 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 -import tempfile -import unittest - -import pandas as pd -from pandas import DataFrame - -from ssvc.decision_points.base import ( - DP_REGISTRY, - DecisionPoint, - DecisionPointValue, -) -from ssvc.decision_tables import experimental_base as base -from ssvc.dp_groups.base import DecisionPointGroup -from ssvc.outcomes.base import OutcomeValue - - -class TestDecisionTable(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.TemporaryDirectory() - self.tempdir_path = self.tempdir.name - - DP_REGISTRY.clear() - - dps = [] - for i in range(3): - dpvs = [] - for j in range(3): - dpv = DecisionPointValue( - name=f"Value {i}{j}", - key=f"DP{i}V{j}", - description=f"Decision Point {i} Value {j} Description", - ) - dpvs.append(dpv) - - dp = DecisionPoint( - name=f"Decision Point {i}", - key=f"DP{i}", - description=f"Decision Point {i} Description", - version="1.0.0", - namespace="x_test", - values=tuple(dpvs), - ) - dps.append(dp) - - self.dpg = DecisionPointGroup( - name="Decision Point Group", - description="Decision Point Group description", - decision_points=tuple(dps), - ) - - ogvs = [] - for i in range(3): - ogv = OutcomeValue( - name=f"Outcome Value {i}", - key=f"OV{i}", - description=f"Outcome Value {i} description", - ) - ogvs.append(ogv) - - self.og = DecisionPoint( - name="Outcome Group", - key="OG", - description="Outcome Group description", - namespace="x_test", - values=tuple(ogvs), - ) - - self.dt = base.DecisionTable( - name="foo", - description="foo description", - namespace="x_test", - decision_point_group=self.dpg, - outcome_group=self.og, - ) - - def tearDown(self): - self.tempdir.cleanup() - - def test_create(self): - # self.dt exists in setUp - self.assertEqual(self.dt.name, "foo") - self.assertEqual(self.dt.description, "foo description") - self.assertEqual(self.dt.namespace, "x_test") - self.assertEqual(self.dt.decision_point_group, self.dpg) - self.assertEqual(self.dt.outcome_group, self.og) - - def test_get_mapping_df(self): - df = self.dt.get_mapping_df() - self.assertIsInstance(df, DataFrame) - - # df is not empty - self.assertFalse(df.empty) - # df has some rows - self.assertGreater(len(df), 0) - # df has the same number of rows as the product of the number of decision points and their values - combos = list(self.dpg.combination_strings()) - self.assertGreater(len(combos), 0) - self.assertEqual(len(df), len(combos)) - - # column names are the decision point strings and the outcome group string - for i, dp in enumerate(self.dpg.decision_points): - self.assertEqual(dp.str, df.columns[i]) - self.assertEqual(self.og.str, df.columns[-1]) - - for col in df.columns: - # col is in the registry - self.assertIn(col, DP_REGISTRY) - - dp = DP_REGISTRY[col] - - uniq = df[col].unique() - - # all values in the decision point should be in the column at least once - for vsum in dp.value_summaries_str: - self.assertIn(vsum, uniq) - - def test_dataframe_to_tuple_list(self): - df = pd.DataFrame( - [ - {"a": 1, "b": 2, "c": 3}, - {"a": 4, "b": 5, "c": 6}, - {"a": 7, "b": 8, "c": 9}, - ] - ) - - tuple_list = self.dt.dataframe_to_tuple_list(df) - - self.assertEqual(len(tuple_list), len(df)) - - for row in tuple_list: - self.assertIsInstance(row, tuple) - self.assertEqual(len(row), 2) - self.assertIsInstance(row[0], tuple) - self.assertIsInstance(row[1], tuple) - - # manually check the structure of the tuple list - self.assertEqual((1, 2), tuple_list[0][0]) - self.assertEqual((3,), tuple_list[0][1]) - self.assertEqual((4, 5), tuple_list[1][0]) - self.assertEqual((6,), tuple_list[1][1]) - self.assertEqual((7, 8), tuple_list[2][0]) - self.assertEqual((9,), tuple_list[2][1]) - - def test_set_mapping(self): - df = pd.DataFrame( - { - "a": ["x", "y", "z"], - "b": ["one", "two", "three"], - "c": ["apple", "orange", "pear"], - } - ) - - result = self.dt.set_mapping(df) - - self.assertIn((("x", "one"), ("apple",)), result) - self.assertIn((("y", "two"), ("orange",)), result) - self.assertIn((("z", "three"), ("pear",)), result) - - self.assertEqual(result, self.dt.mapping) - - @unittest.skip("Test not implemented") - def test_consistency_check_mapping(self): - pass - - @unittest.skip("Test not implemented") - def test_check_decision_boundaries(self): - pass - - @unittest.skip("Test not implemented") - def test_find_decision_boundaries(self): - pass - - @unittest.skip("Test not implemented") - def test_int_node_to_str(self): - pass - - @unittest.skip("Test not implemented") - def test_check_graph(self): - pass - - @unittest.skip("Test not implemented") - def test_add_nodes(self): - pass - - @unittest.skip("Test not implemented") - def test_add_edges(self): - pass - - def test_mapping_to_csv_str(self): - df = self.dt.get_mapping_df() - self.dt.set_mapping(df) - - csv_str = self.dt.mapping_to_csv_str() - self.assertIsInstance(csv_str, str) - - lines = csv_str.strip().split("\n") - - # first line is the header - self.assertEqual( - lines[0], - ",".join([dp.str for dp in self.dpg.decision_points] + [self.og.str]), - ) - - combinations = list(self.dpg.combination_strings()) - - # there should be one line for each combination after the header - self.assertEqual(len(lines[1:]), len(combinations)) - - # each line after the header starts with a combination of decision point values - for combo, line in zip(combinations, lines[1:]): - # there are as many commas in the line as there are combos - comma_count = line.count(",") - self.assertEqual(comma_count, len(combo)) - - for dpv_str in combo: - self.assertIn(dpv_str, line) - - # the last thing in the line is the outcome group value - og_value = line.split(",")[-1] - self.assertIn(og_value, self.og.value_summaries_str) - - @unittest.skip("Method not implemented") - def test_load_from_csv_str(self): - df = self.dt.get_mapping_df() - self.dt.set_mapping(df) - - csv_str = self.dt.mapping_to_csv_str() - - print("Original CSV String:") - print(csv_str) - # modify the csv_str to simulate a new mapping - # replace "OV2" with "OV1" - modified_csv_str = csv_str.replace("OV2", "OV1") - # confirm that "OV2" is no longer in the modified string - self.assertNotIn("OV2", modified_csv_str) - print("Modified CSV String:") - print(modified_csv_str) - # load the modified mapping from the CSV string - new_dt = base.DecisionTable.load_from_csv_str(csv_str=modified_csv_str) - self.assertIsInstance(new_dt, base.DecisionTable) - self.assertEqual(new_dt.name, self.dt.name) - self.assertEqual(new_dt.description, self.dt.description) - self.assertEqual(new_dt.namespace, self.dt.namespace) - self.assertEqual(new_dt.decision_point_group, self.dpg) - self.assertEqual(new_dt.outcome_group, self.og) - # check that the mapping is the same as the modified CSV string - new_mapping = new_dt.get_mapping_df() - self.assertIsInstance(new_mapping, DataFrame) - self.assertFalse(new_mapping.empty) - # check that the modified value is in the mapping - # for each row in the old mapping, the new mapping should have the same values - # original: OV0 = new: OV1 - # original: OV1 = new: OV1 - # original: OV2 = new: OV1 - for index, row in df.iterrows(): - print(row) - - self.assertIn("OV1", new_mapping[self.og.str].unique()) - - -if __name__ == "__main__": - unittest.main() From c37c0363401cf951b8938fcfc9335510030a3b96 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 17 Jun 2025 15:39:07 -0400 Subject: [PATCH 90/99] update Makefile --- Makefile | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index af885c5c..f5e33ada 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MKDOCS_PORT=8765 DOCKER_DIR=docker # Targets -.PHONY: all test docs docker_test clean help +.PHONY: all test docs docker_test clean help mdlint_fix up down all: help @@ -40,9 +40,13 @@ help: @echo "" @echo "Targets:" @echo " all - Display this help message" - @echo " mdlint_fix - Run markdownlint with --fix" - @echo " test - Run the tests in a local shell" - @echo " docs - Build and run the docs Docker service" - @echo " docker_test - Run the tests in a Docker container" - @echo " clean - Remove Docker containers and images" - @echo " help - Display this help message" + @echo " mdlint_fix - Run markdownlint with fix" + @echo " test - Run tests locally" + @echo " docker_test - Run tests in Docker" + @echo " docs - Build and run documentation in Docker" + @echo " up - Start Docker services" + @echo " down - Stop Docker services" + @echo " clean - Clean up Docker resources" + @echo " help - Display this help message" + + From 5343403e7a187659c0bb6de45b213cf1a16f31ed Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 17 Jun 2025 15:47:05 -0400 Subject: [PATCH 91/99] Update mission_prevalence.py fixing mission prevalence imports. --- src/ssvc/decision_points/ssvc_/mission_prevalence.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ssvc/decision_points/ssvc_/mission_prevalence.py b/src/ssvc/decision_points/ssvc_/mission_prevalence.py index ba9c1a3b..9b3b0640 100644 --- a/src/ssvc/decision_points/ssvc_/mission_prevalence.py +++ b/src/ssvc/decision_points/ssvc_/mission_prevalence.py @@ -23,7 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import DecisionPointValue, SsvcDecisionPoint + +from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs MINIMAL = DecisionPointValue( From bddc0f84543d075cd6dfe5e91332af4cc3d15c32 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Tue, 17 Jun 2025 16:19:48 -0400 Subject: [PATCH 92/99] rename packages (*.ssvc_ -> *.ssvc) --- docs/howto/coordination_triage_decision.md | 14 ++-- docs/howto/deployer_tree.md | 10 +-- docs/howto/publication_decision.md | 6 +- docs/howto/supplier_tree.md | 8 +-- docs/reference/decision_points/automatable.md | 4 +- .../reference/decision_points/exploitation.md | 4 +- .../reference/decision_points/human_impact.md | 4 +- .../decision_points/mission_impact.md | 4 +- .../decision_points/public_safety_impact.md | 4 +- .../decision_points/public_value_added.md | 2 +- .../decision_points/report_credibility.md | 2 +- .../decision_points/report_public.md | 2 +- .../decision_points/safety_impact.md | 4 +- .../decision_points/supplier_cardinality.md | 2 +- .../decision_points/supplier_contacted.md | 2 +- .../decision_points/supplier_engagement.md | 2 +- .../decision_points/supplier_involvement.md | 2 +- .../decision_points/system_exposure.md | 4 +- .../decision_points/technical_impact.md | 2 +- docs/reference/decision_points/utility.md | 4 +- .../decision_points/value_density.md | 2 +- src/ssvc/decision_points/ssvc/__init__.py | 21 ++++++ .../{ssvc_ => ssvc}/automatable.py | 2 +- .../decision_points/{ssvc_ => ssvc}/base.py | 0 .../{ssvc_ => ssvc}/critical_software.py | 2 +- .../{ssvc_ => ssvc}/exploitation.py | 2 +- .../{ssvc_ => ssvc}/high_value_asset.py | 2 +- .../{ssvc_ => ssvc}/human_impact.py | 2 +- .../decision_points/{ssvc_ => ssvc}/in_kev.py | 2 +- .../{ssvc_ => ssvc}/mission_impact.py | 2 +- .../{ssvc_ => ssvc}/mission_prevalence.py | 0 .../{ssvc_ => ssvc}/public_safety_impact.py | 2 +- .../{ssvc_ => ssvc}/public_value_added.py | 2 +- .../{ssvc_ => ssvc}/report_credibility.py | 2 +- .../{ssvc_ => ssvc}/report_public.py | 2 +- .../{ssvc_ => ssvc}/safety_impact.py | 2 +- .../{ssvc_ => ssvc}/supplier_cardinality.py | 2 +- .../{ssvc_ => ssvc}/supplier_contacted.py | 2 +- .../{ssvc_ => ssvc}/supplier_engagement.py | 2 +- .../{ssvc_ => ssvc}/supplier_involvement.py | 2 +- .../{ssvc_ => ssvc}/system_exposure.py | 2 +- .../{ssvc_ => ssvc}/technical_impact.py | 2 +- .../{ssvc_ => ssvc}/utility.py | 2 +- .../{ssvc_ => ssvc}/value_density.py | 2 +- src/ssvc/decision_points/ssvc_/__init__.py | 15 ----- src/ssvc/doc_helpers.py | 2 +- src/ssvc/doctools.py | 46 +++++++++++-- .../dp_groups/ssvc/coordinator_publication.py | 6 +- src/ssvc/dp_groups/ssvc/coordinator_triage.py | 20 +++--- src/ssvc/dp_groups/ssvc/deployer.py | 16 ++--- src/ssvc/dp_groups/ssvc/supplier.py | 12 ++-- src/ssvc/outcomes/base.py | 2 +- src/ssvc/outcomes/ssvc/__init__.py | 25 ++++++++ src/ssvc/outcomes/ssvc/coordinate.py | 53 +++++++++++++++ src/ssvc/outcomes/ssvc/dsoi.py | 64 +++++++++++++++++++ src/ssvc/outcomes/ssvc/publish.py | 55 ++++++++++++++++ src/ssvc/outcomes/ssvc_/__init__.py | 19 ------ src/ssvc/outcomes/ssvc_/coordinate.py | 47 -------------- src/ssvc/outcomes/ssvc_/dsoi.py | 58 ----------------- src/ssvc/outcomes/ssvc_/publish.py | 49 -------------- src/ssvc/policy_generator.py | 10 +-- src/test/decision_points/test_dp_base.py | 10 +-- src/test/dp_groups/test_dp_groups.py | 34 ++++++---- src/test/test_schema.py | 28 ++++---- 64 files changed, 393 insertions(+), 327 deletions(-) create mode 100644 src/ssvc/decision_points/ssvc/__init__.py rename src/ssvc/decision_points/{ssvc_ => ssvc}/automatable.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/base.py (100%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/critical_software.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/exploitation.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/high_value_asset.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/human_impact.py (98%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/in_kev.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/mission_impact.py (98%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/mission_prevalence.py (100%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/public_safety_impact.py (98%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/public_value_added.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/report_credibility.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/report_public.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/safety_impact.py (99%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/supplier_cardinality.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/supplier_contacted.py (96%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/supplier_engagement.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/supplier_involvement.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/system_exposure.py (98%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/technical_impact.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/utility.py (97%) rename src/ssvc/decision_points/{ssvc_ => ssvc}/value_density.py (97%) delete mode 100644 src/ssvc/decision_points/ssvc_/__init__.py create mode 100644 src/ssvc/outcomes/ssvc/__init__.py create mode 100644 src/ssvc/outcomes/ssvc/coordinate.py create mode 100644 src/ssvc/outcomes/ssvc/dsoi.py create mode 100644 src/ssvc/outcomes/ssvc/publish.py delete mode 100644 src/ssvc/outcomes/ssvc_/__init__.py delete mode 100644 src/ssvc/outcomes/ssvc_/coordinate.py delete mode 100644 src/ssvc/outcomes/ssvc_/dsoi.py delete mode 100644 src/ssvc/outcomes/ssvc_/publish.py diff --git a/docs/howto/coordination_triage_decision.md b/docs/howto/coordination_triage_decision.md index 240572d3..53949660 100644 --- a/docs/howto/coordination_triage_decision.md +++ b/docs/howto/coordination_triage_decision.md @@ -82,13 +82,13 @@ The remaining five decision points are: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.report_public import LATEST as RP -from ssvc.decision_points.ssvc_.supplier_contacted import LATEST as SC -from ssvc.decision_points.ssvc_.report_credibility import LATEST as RC -from ssvc.decision_points.ssvc_.supplier_cardinality import LATEST as SI -from ssvc.decision_points.ssvc_.supplier_engagement import LATEST as SE -from ssvc.decision_points.ssvc_.utility import LATEST as U -from ssvc.decision_points.ssvc_.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc.report_public import LATEST as RP +from ssvc.decision_points.ssvc.supplier_contacted import LATEST as SC +from ssvc.decision_points.ssvc.report_credibility import LATEST as RC +from ssvc.decision_points.ssvc.supplier_cardinality import LATEST as SI +from ssvc.decision_points.ssvc.supplier_engagement import LATEST as SE +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block for dp in [RP, SC, RC, SI, SE, U, PSI]: diff --git a/docs/howto/deployer_tree.md b/docs/howto/deployer_tree.md index 637b3888..ffbde896 100644 --- a/docs/howto/deployer_tree.md +++ b/docs/howto/deployer_tree.md @@ -113,14 +113,14 @@ The Deployer Patch Deployment Priority decision model uses the following decisio More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP -from ssvc.decision_points.ssvc_.system_exposure import LATEST as SE -from ssvc.decision_points.ssvc_.utility import LATEST as U -from ssvc.decision_points.ssvc_.human_impact import LATEST as HI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.system_exposure import LATEST as SE +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.human_impact import LATEST as HI from ssvc.doc_helpers import example_block for dp in [EXP, SE, U, HI]: - print(example_block(dp)) + print(example_block(dp)) ``` In the *Human Impact* table above, *MEF* stands for Mission Essential Function. diff --git a/docs/howto/publication_decision.md b/docs/howto/publication_decision.md index e8090709..739b3b88 100644 --- a/docs/howto/publication_decision.md +++ b/docs/howto/publication_decision.md @@ -133,9 +133,9 @@ and adds two new ones ([*Supplier Involvement*](../reference/decision_points/sup More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.supplier_involvement import LATEST as SI -from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP -from ssvc.decision_points.ssvc_.public_value_added import LATEST as PVA +from ssvc.decision_points.ssvc.supplier_involvement import LATEST as SI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.public_value_added import LATEST as PVA from ssvc.doc_helpers import example_block diff --git a/docs/howto/supplier_tree.md b/docs/howto/supplier_tree.md index aa12d0e6..3d1c2384 100644 --- a/docs/howto/supplier_tree.md +++ b/docs/howto/supplier_tree.md @@ -72,10 +72,10 @@ The decision to create a patch is based on the following decision points: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.exploitation import LATEST as EXP -from ssvc.decision_points.ssvc_.utility import LATEST as U -from ssvc.decision_points.ssvc_.technical_impact import LATEST as TI -from ssvc.decision_points.ssvc_.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.technical_impact import LATEST as TI +from ssvc.decision_points.ssvc.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block diff --git a/docs/reference/decision_points/automatable.md b/docs/reference/decision_points/automatable.md index 44ef71b1..33592885 100644 --- a/docs/reference/decision_points/automatable.md +++ b/docs/reference/decision_points/automatable.md @@ -1,7 +1,7 @@ # Automatable (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.automatable import LATEST +from ssvc.decision_points.ssvc.automatable import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -62,7 +62,7 @@ Due to vulnerability chaining, there is some nuance as to whether reconnaissance ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.automatable import VERSIONS +from ssvc.decision_points.ssvc.automatable import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/exploitation.md b/docs/reference/decision_points/exploitation.md index 44fa49cb..b0c92a43 100644 --- a/docs/reference/decision_points/exploitation.md +++ b/docs/reference/decision_points/exploitation.md @@ -1,7 +1,7 @@ # Exploitation ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.exploitation import LATEST +from ssvc.decision_points.ssvc.exploitation import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -49,7 +49,7 @@ The table below lists CWE-IDs that could be used to mark a vulnerability as *PoC ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.exploitation import VERSIONS +from ssvc.decision_points.ssvc.exploitation import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/human_impact.md b/docs/reference/decision_points/human_impact.md index dc802e9d..339cdceb 100644 --- a/docs/reference/decision_points/human_impact.md +++ b/docs/reference/decision_points/human_impact.md @@ -1,7 +1,7 @@ # Human Impact ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.human_impact import LATEST +from ssvc.decision_points.ssvc.human_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -47,7 +47,7 @@ see [Guidance on Communicating Results](../../howto/bootstrap/use.md). ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.human_impact import VERSIONS +from ssvc.decision_points.ssvc.human_impact import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/mission_impact.md b/docs/reference/decision_points/mission_impact.md index 617a0327..801aea6b 100644 --- a/docs/reference/decision_points/mission_impact.md +++ b/docs/reference/decision_points/mission_impact.md @@ -1,7 +1,7 @@ # Mission Impact ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.mission_impact import LATEST +from ssvc.decision_points.ssvc.mission_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -42,7 +42,7 @@ It should require the vulnerability management team to interact with more senior ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.mission_impact import VERSIONS +from ssvc.decision_points.ssvc.mission_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_safety_impact.md b/docs/reference/decision_points/public_safety_impact.md index 4fc7df48..15960fa5 100644 --- a/docs/reference/decision_points/public_safety_impact.md +++ b/docs/reference/decision_points/public_safety_impact.md @@ -1,7 +1,7 @@ # Public Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.public_safety_impact import LATEST +from ssvc.decision_points.ssvc.public_safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -21,7 +21,7 @@ Therefore we simplify the above into a binary categorization: ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.public_safety_impact import VERSIONS +from ssvc.decision_points.ssvc.public_safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_value_added.md b/docs/reference/decision_points/public_value_added.md index 4a085f25..8a07a42e 100644 --- a/docs/reference/decision_points/public_value_added.md +++ b/docs/reference/decision_points/public_value_added.md @@ -1,7 +1,7 @@ # Public Value Added ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.public_value_added import LATEST +from ssvc.decision_points.ssvc.public_value_added import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_credibility.md b/docs/reference/decision_points/report_credibility.md index fc648de8..0bd2047c 100644 --- a/docs/reference/decision_points/report_credibility.md +++ b/docs/reference/decision_points/report_credibility.md @@ -1,7 +1,7 @@ # Report Credibility ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.report_credibility import LATEST +from ssvc.decision_points.ssvc.report_credibility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_public.md b/docs/reference/decision_points/report_public.md index ea60fda9..fd5df8e2 100644 --- a/docs/reference/decision_points/report_public.md +++ b/docs/reference/decision_points/report_public.md @@ -1,7 +1,7 @@ # Report Public ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.report_public import LATEST +from ssvc.decision_points.ssvc.report_public import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/safety_impact.md b/docs/reference/decision_points/safety_impact.md index 18f25e7d..128275ba 100644 --- a/docs/reference/decision_points/safety_impact.md +++ b/docs/reference/decision_points/safety_impact.md @@ -1,7 +1,7 @@ # Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.safety_impact import LATEST +from ssvc.decision_points.ssvc.safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -217,7 +217,7 @@ We defer this topic for now because we combine it with [*Mission Impact*](missio ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.safety_impact import VERSIONS +from ssvc.decision_points.ssvc.safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/supplier_cardinality.md b/docs/reference/decision_points/supplier_cardinality.md index 332633ab..6c27a0f3 100644 --- a/docs/reference/decision_points/supplier_cardinality.md +++ b/docs/reference/decision_points/supplier_cardinality.md @@ -1,7 +1,7 @@ # Supplier Cardinality ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.supplier_cardinality import LATEST +from ssvc.decision_points.ssvc.supplier_cardinality import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_contacted.md b/docs/reference/decision_points/supplier_contacted.md index 5a863768..d8bcbdcf 100644 --- a/docs/reference/decision_points/supplier_contacted.md +++ b/docs/reference/decision_points/supplier_contacted.md @@ -1,7 +1,7 @@ # Supplier Contacted ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.supplier_contacted import LATEST +from ssvc.decision_points.ssvc.supplier_contacted import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_engagement.md b/docs/reference/decision_points/supplier_engagement.md index f97a917f..35b395d7 100644 --- a/docs/reference/decision_points/supplier_engagement.md +++ b/docs/reference/decision_points/supplier_engagement.md @@ -1,7 +1,7 @@ # Supplier Engagement ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.supplier_engagement import LATEST +from ssvc.decision_points.ssvc.supplier_engagement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_involvement.md b/docs/reference/decision_points/supplier_involvement.md index bfc45eab..519db57b 100644 --- a/docs/reference/decision_points/supplier_involvement.md +++ b/docs/reference/decision_points/supplier_involvement.md @@ -1,7 +1,7 @@ # Supplier Involvement ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.supplier_involvement import LATEST +from ssvc.decision_points.ssvc.supplier_involvement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/system_exposure.md b/docs/reference/decision_points/system_exposure.md index a50c131c..5bbaa2f1 100644 --- a/docs/reference/decision_points/system_exposure.md +++ b/docs/reference/decision_points/system_exposure.md @@ -1,7 +1,7 @@ # System Exposure ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.system_exposure import LATEST +from ssvc.decision_points.ssvc.system_exposure import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -44,7 +44,7 @@ If you have suggestions for further heuristics, or potential counterexamples to ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.system_exposure import VERSIONS +from ssvc.decision_points.ssvc.system_exposure import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/technical_impact.md b/docs/reference/decision_points/technical_impact.md index 6bc02a33..0c3925ff 100644 --- a/docs/reference/decision_points/technical_impact.md +++ b/docs/reference/decision_points/technical_impact.md @@ -1,7 +1,7 @@ # Technical Impact ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.technical_impact import LATEST +from ssvc.decision_points.ssvc.technical_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/utility.md b/docs/reference/decision_points/utility.md index 90ac80ca..13fca8b1 100644 --- a/docs/reference/decision_points/utility.md +++ b/docs/reference/decision_points/utility.md @@ -1,7 +1,7 @@ # Utility ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.utility import LATEST +from ssvc.decision_points.ssvc.utility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -46,7 +46,7 @@ However, future work should look for and prevent large mismatches between the ou ## Previous Versions ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.utility import VERSIONS +from ssvc.decision_points.ssvc.utility import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/value_density.md b/docs/reference/decision_points/value_density.md index 2cc7bb8c..265c680c 100644 --- a/docs/reference/decision_points/value_density.md +++ b/docs/reference/decision_points/value_density.md @@ -1,7 +1,7 @@ # Value Density (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.ssvc_.value_density import LATEST +from ssvc.decision_points.ssvc.value_density import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/src/ssvc/decision_points/ssvc/__init__.py b/src/ssvc/decision_points/ssvc/__init__.py new file mode 100644 index 00000000..03abf5f2 --- /dev/null +++ b/src/ssvc/decision_points/ssvc/__init__.py @@ -0,0 +1,21 @@ +# 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 +""" +This package contains SSVC decision points belonging to the `ssvc` namespace. +""" diff --git a/src/ssvc/decision_points/ssvc_/automatable.py b/src/ssvc/decision_points/ssvc/automatable.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/automatable.py rename to src/ssvc/decision_points/ssvc/automatable.py index 3c8fc3ac..3ebe61ff 100644 --- a/src/ssvc/decision_points/ssvc_/automatable.py +++ b/src/ssvc/decision_points/ssvc/automatable.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint RAPID = DecisionPointValue( name="Rapid", diff --git a/src/ssvc/decision_points/ssvc_/base.py b/src/ssvc/decision_points/ssvc/base.py similarity index 100% rename from src/ssvc/decision_points/ssvc_/base.py rename to src/ssvc/decision_points/ssvc/base.py diff --git a/src/ssvc/decision_points/ssvc_/critical_software.py b/src/ssvc/decision_points/ssvc/critical_software.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/critical_software.py rename to src/ssvc/decision_points/ssvc/critical_software.py index 334bc29f..6fad730c 100644 --- a/src/ssvc/decision_points/ssvc_/critical_software.py +++ b/src/ssvc/decision_points/ssvc/critical_software.py @@ -24,7 +24,7 @@ from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint YES = DecisionPointValue( name="Yes", diff --git a/src/ssvc/decision_points/ssvc_/exploitation.py b/src/ssvc/decision_points/ssvc/exploitation.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/exploitation.py rename to src/ssvc/decision_points/ssvc/exploitation.py index 2082a767..c4a4ccc8 100644 --- a/src/ssvc/decision_points/ssvc_/exploitation.py +++ b/src/ssvc/decision_points/ssvc/exploitation.py @@ -21,9 +21,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint ACTIVE = DecisionPointValue( name="Active", diff --git a/src/ssvc/decision_points/ssvc_/high_value_asset.py b/src/ssvc/decision_points/ssvc/high_value_asset.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/high_value_asset.py rename to src/ssvc/decision_points/ssvc/high_value_asset.py index 7309d31a..3612b094 100644 --- a/src/ssvc/decision_points/ssvc_/high_value_asset.py +++ b/src/ssvc/decision_points/ssvc/high_value_asset.py @@ -24,7 +24,7 @@ from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint YES = DecisionPointValue( name="Yes", diff --git a/src/ssvc/decision_points/ssvc_/human_impact.py b/src/ssvc/decision_points/ssvc/human_impact.py similarity index 98% rename from src/ssvc/decision_points/ssvc_/human_impact.py rename to src/ssvc/decision_points/ssvc/human_impact.py index f1cac59d..a5a583bf 100644 --- a/src/ssvc/decision_points/ssvc_/human_impact.py +++ b/src/ssvc/decision_points/ssvc/human_impact.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint LOW_1 = DecisionPointValue( name="Low", diff --git a/src/ssvc/decision_points/ssvc_/in_kev.py b/src/ssvc/decision_points/ssvc/in_kev.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/in_kev.py rename to src/ssvc/decision_points/ssvc/in_kev.py index 90c185a9..0e8c83bd 100644 --- a/src/ssvc/decision_points/ssvc_/in_kev.py +++ b/src/ssvc/decision_points/ssvc/in_kev.py @@ -23,7 +23,7 @@ from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint YES = DecisionPointValue( name="Yes", diff --git a/src/ssvc/decision_points/ssvc_/mission_impact.py b/src/ssvc/decision_points/ssvc/mission_impact.py similarity index 98% rename from src/ssvc/decision_points/ssvc_/mission_impact.py rename to src/ssvc/decision_points/ssvc/mission_impact.py index 98fa52dd..c9666fb2 100644 --- a/src/ssvc/decision_points/ssvc_/mission_impact.py +++ b/src/ssvc/decision_points/ssvc/mission_impact.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint MISSION_FAILURE = DecisionPointValue( name="Mission Failure", diff --git a/src/ssvc/decision_points/ssvc_/mission_prevalence.py b/src/ssvc/decision_points/ssvc/mission_prevalence.py similarity index 100% rename from src/ssvc/decision_points/ssvc_/mission_prevalence.py rename to src/ssvc/decision_points/ssvc/mission_prevalence.py diff --git a/src/ssvc/decision_points/ssvc_/public_safety_impact.py b/src/ssvc/decision_points/ssvc/public_safety_impact.py similarity index 98% rename from src/ssvc/decision_points/ssvc_/public_safety_impact.py rename to src/ssvc/decision_points/ssvc/public_safety_impact.py index 0665732c..449c8938 100644 --- a/src/ssvc/decision_points/ssvc_/public_safety_impact.py +++ b/src/ssvc/decision_points/ssvc/public_safety_impact.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint MINIMAL_1 = DecisionPointValue( name="Minimal", diff --git a/src/ssvc/decision_points/ssvc_/public_value_added.py b/src/ssvc/decision_points/ssvc/public_value_added.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/public_value_added.py rename to src/ssvc/decision_points/ssvc/public_value_added.py index 38cd8090..eac5543b 100644 --- a/src/ssvc/decision_points/ssvc_/public_value_added.py +++ b/src/ssvc/decision_points/ssvc/public_value_added.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint LIMITED = DecisionPointValue( name="Limited", diff --git a/src/ssvc/decision_points/ssvc_/report_credibility.py b/src/ssvc/decision_points/ssvc/report_credibility.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/report_credibility.py rename to src/ssvc/decision_points/ssvc/report_credibility.py index 6a7c99a9..e74218ce 100644 --- a/src/ssvc/decision_points/ssvc_/report_credibility.py +++ b/src/ssvc/decision_points/ssvc/report_credibility.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint NOT_CREDIBLE = DecisionPointValue( name="Not Credible", diff --git a/src/ssvc/decision_points/ssvc_/report_public.py b/src/ssvc/decision_points/ssvc/report_public.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/report_public.py rename to src/ssvc/decision_points/ssvc/report_public.py index 0e5b0ffe..73cccfee 100644 --- a/src/ssvc/decision_points/ssvc_/report_public.py +++ b/src/ssvc/decision_points/ssvc/report_public.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint YES = DecisionPointValue( name="Yes", diff --git a/src/ssvc/decision_points/ssvc_/safety_impact.py b/src/ssvc/decision_points/ssvc/safety_impact.py similarity index 99% rename from src/ssvc/decision_points/ssvc_/safety_impact.py rename to src/ssvc/decision_points/ssvc/safety_impact.py index 4e05a51f..5db63999 100644 --- a/src/ssvc/decision_points/ssvc_/safety_impact.py +++ b/src/ssvc/decision_points/ssvc/safety_impact.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint CATASTROPHIC = DecisionPointValue( name="Catastrophic", diff --git a/src/ssvc/decision_points/ssvc_/supplier_cardinality.py b/src/ssvc/decision_points/ssvc/supplier_cardinality.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/supplier_cardinality.py rename to src/ssvc/decision_points/ssvc/supplier_cardinality.py index ac09c252..c78167f6 100644 --- a/src/ssvc/decision_points/ssvc_/supplier_cardinality.py +++ b/src/ssvc/decision_points/ssvc/supplier_cardinality.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint MULTIPLE = DecisionPointValue( name="Multiple", diff --git a/src/ssvc/decision_points/ssvc_/supplier_contacted.py b/src/ssvc/decision_points/ssvc/supplier_contacted.py similarity index 96% rename from src/ssvc/decision_points/ssvc_/supplier_contacted.py rename to src/ssvc/decision_points/ssvc/supplier_contacted.py index a08b931b..9d440415 100644 --- a/src/ssvc/decision_points/ssvc_/supplier_contacted.py +++ b/src/ssvc/decision_points/ssvc/supplier_contacted.py @@ -21,9 +21,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint YES = DecisionPointValue( name="Yes", diff --git a/src/ssvc/decision_points/ssvc_/supplier_engagement.py b/src/ssvc/decision_points/ssvc/supplier_engagement.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/supplier_engagement.py rename to src/ssvc/decision_points/ssvc/supplier_engagement.py index 354d7b2f..ed9660fb 100644 --- a/src/ssvc/decision_points/ssvc_/supplier_engagement.py +++ b/src/ssvc/decision_points/ssvc/supplier_engagement.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint UNRESPONSIVE = DecisionPointValue( name="Unresponsive", diff --git a/src/ssvc/decision_points/ssvc_/supplier_involvement.py b/src/ssvc/decision_points/ssvc/supplier_involvement.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/supplier_involvement.py rename to src/ssvc/decision_points/ssvc/supplier_involvement.py index 1a9bb3dd..07dcfc2d 100644 --- a/src/ssvc/decision_points/ssvc_/supplier_involvement.py +++ b/src/ssvc/decision_points/ssvc/supplier_involvement.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint UNCOOPERATIVE = DecisionPointValue( name="Uncooperative/Unresponsive", diff --git a/src/ssvc/decision_points/ssvc_/system_exposure.py b/src/ssvc/decision_points/ssvc/system_exposure.py similarity index 98% rename from src/ssvc/decision_points/ssvc_/system_exposure.py rename to src/ssvc/decision_points/ssvc/system_exposure.py index c5926cfc..06b87b82 100644 --- a/src/ssvc/decision_points/ssvc_/system_exposure.py +++ b/src/ssvc/decision_points/ssvc/system_exposure.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint EXP_UNAVOIDABLE = DecisionPointValue( name="Unavoidable", diff --git a/src/ssvc/decision_points/ssvc_/technical_impact.py b/src/ssvc/decision_points/ssvc/technical_impact.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/technical_impact.py rename to src/ssvc/decision_points/ssvc/technical_impact.py index bd205a54..e2bd46dd 100644 --- a/src/ssvc/decision_points/ssvc_/technical_impact.py +++ b/src/ssvc/decision_points/ssvc/technical_impact.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint TOTAL = DecisionPointValue( name="Total", diff --git a/src/ssvc/decision_points/ssvc_/utility.py b/src/ssvc/decision_points/ssvc/utility.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/utility.py rename to src/ssvc/decision_points/ssvc/utility.py index e75b431c..f83518c3 100644 --- a/src/ssvc/decision_points/ssvc_/utility.py +++ b/src/ssvc/decision_points/ssvc/utility.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint SUPER_EFFECTIVE_2 = DecisionPointValue( name="Super Effective", diff --git a/src/ssvc/decision_points/ssvc_/value_density.py b/src/ssvc/decision_points/ssvc/value_density.py similarity index 97% rename from src/ssvc/decision_points/ssvc_/value_density.py rename to src/ssvc/decision_points/ssvc/value_density.py index 9f3d76c4..610291a8 100644 --- a/src/ssvc/decision_points/ssvc_/value_density.py +++ b/src/ssvc/decision_points/ssvc/value_density.py @@ -22,9 +22,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint CONCENTRATED = DecisionPointValue( name="Concentrated", diff --git a/src/ssvc/decision_points/ssvc_/__init__.py b/src/ssvc/decision_points/ssvc_/__init__.py deleted file mode 100644 index 3fa844ad..00000000 --- a/src/ssvc/decision_points/ssvc_/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -""" -This package contains SSVC decision points belonging to the `ssvc` namespace. -""" diff --git a/src/ssvc/doc_helpers.py b/src/ssvc/doc_helpers.py index 9748df47..9ab8a8dc 100644 --- a/src/ssvc/doc_helpers.py +++ b/src/ssvc/doc_helpers.py @@ -23,7 +23,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint MD_TABLE_ROW_TEMPLATE = "| {value.name} | {value.description} |" diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index 2d5ae11d..09bfcce9 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -34,19 +34,48 @@ python -m ssvc.doctools --overwrite --jsondir data/json/decision_points """ +import importlib import logging import os -import ssvc.dp_groups.cvss.collections # noqa -import ssvc.dp_groups.ssvc.collections # noqa from ssvc.decision_points.base import ( - DecisionPoint, REGISTERED_DECISION_POINTS, + DecisionPoint, + REGISTERED_DECISION_POINTS, ) -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint logger = logging.getLogger(__name__) +def find_modules_to_import( + directory: str = "../decision_points", package: str = "ssvc.decision_points" +) -> bool: + """ + Find all modules that contain decision points and import them. + + This is necessary to ensure that all decision points are registered. + """ + imported_modules = [] + for root, _, files in os.walk(os.path.abspath(directory)): + for file in files: + if file.endswith(".py") and not file.startswith("__"): + # build the module name relative to the package + relative_path = os.path.relpath(root, directory) + module_name = os.path.join(relative_path, file[:-3]).replace( + os.sep, "." + ) + + full_module_name = f"{package}.{module_name}" + # import the module + try: + logger.info(f"Importing module {full_module_name}") + module = importlib.import_module(full_module_name) + imported_modules.append(module) + except ImportError as e: + logger.error(f"Failed to import {full_module_name}: {e}") + return imported_modules + + def _filename_friendly(name: str) -> str: """ Given a string, return a version that is friendly for use in a filename. @@ -120,9 +149,7 @@ def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool) -> dump_json(basename, dp, jsondir, overwrite) -def dump_json( - basename: str, dp: DecisionPoint, jsondir: str, overwrite: bool -) -> str: +def dump_json(basename: str, dp: DecisionPoint, jsondir: str, overwrite: bool) -> str: """ Generate the json example for a decision point. @@ -189,6 +216,11 @@ def main(): overwrite = args.overwrite jsondir = args.jsondir + find_modules_to_import("./decision_points", "ssvc.decision_points") + find_modules_to_import("./outcomes", "ssvc.outcomes") + from ssvc.dp_groups.ssvc import collections # noqa: E402 + from ssvc.dp_groups.cvss import collections # noqa: E402 + # for each decision point: for dp in REGISTERED_DECISION_POINTS: dump_decision_point(jsondir, dp, overwrite) diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index 08003b1c..77f773d0 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -23,9 +23,9 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 -from ssvc.decision_points.ssvc_.public_value_added import PUBLIC_VALUE_ADDED_1 -from ssvc.decision_points.ssvc_.supplier_involvement import SUPPLIER_INVOLVEMENT_1 +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.public_value_added import PUBLIC_VALUE_ADDED_1 +from ssvc.decision_points.ssvc.supplier_involvement import SUPPLIER_INVOLVEMENT_1 from ssvc.dp_groups.base import DecisionPointGroup diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index ac6e2153..cd625ef4 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -23,16 +23,16 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 -from ssvc.decision_points.ssvc_.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 -from ssvc.decision_points.ssvc_.report_credibility import REPORT_CREDIBILITY_1 -from ssvc.decision_points.ssvc_.report_public import REPORT_PUBLIC_1 -from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.ssvc_.supplier_cardinality import SUPPLIER_CARDINALITY_1 -from ssvc.decision_points.ssvc_.supplier_contacted import SUPPLIER_CONTACTED_1 -from ssvc.decision_points.ssvc_.supplier_engagement import SUPPLIER_ENGAGEMENT_1 -from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 -from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 +from ssvc.decision_points.ssvc.report_credibility import REPORT_CREDIBILITY_1 +from ssvc.decision_points.ssvc.report_public import REPORT_PUBLIC_1 +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.supplier_cardinality import SUPPLIER_CARDINALITY_1 +from ssvc.decision_points.ssvc.supplier_contacted import SUPPLIER_CONTACTED_1 +from ssvc.decision_points.ssvc.supplier_engagement import SUPPLIER_ENGAGEMENT_1 +from ssvc.decision_points.ssvc.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import DecisionPointGroup COORDINATOR_TRIAGE_1 = DecisionPointGroup( diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index dd2fc6d9..4473746c 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -24,20 +24,20 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 -from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 -from ssvc.decision_points.ssvc_.human_impact import HUMAN_IMPACT_2 -from ssvc.decision_points.ssvc_.mission_impact import ( +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.human_impact import HUMAN_IMPACT_2 +from ssvc.decision_points.ssvc.mission_impact import ( MISSION_IMPACT_1, MISSION_IMPACT_2, ) -from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.ssvc_.system_exposure import ( +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.system_exposure import ( SYSTEM_EXPOSURE_1, SYSTEM_EXPOSURE_1_0_1, ) -from ssvc.decision_points.ssvc_.utility import UTILITY_1_0_1 -from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import DecisionPointGroup PATCH_APPLIER_1 = DecisionPointGroup( diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index fd863152..f7a73c13 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -24,12 +24,12 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2, VIRULENCE_1 -from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 -from ssvc.decision_points.ssvc_.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.ssvc_.technical_impact import TECHNICAL_IMPACT_1 -from ssvc.decision_points.ssvc_.utility import UTILITY_1, UTILITY_1_0_1 -from ssvc.decision_points.ssvc_.value_density import VALUE_DENSITY_1 +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2, VIRULENCE_1 +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.technical_impact import TECHNICAL_IMPACT_1 +from ssvc.decision_points.ssvc.utility import UTILITY_1, UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 from ssvc.dp_groups.base import DecisionPointGroup PATCH_DEVELOPER_1 = DecisionPointGroup( diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index f2074f4f..82c9da08 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -23,7 +23,7 @@ from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_points.cisa.base import CisaDecisionPoint from ssvc.decision_points.cvss.base import CvssDecisionPoint -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint OutcomeValue = DecisionPointValue OutcomeGroup = DecisionPoint diff --git a/src/ssvc/outcomes/ssvc/__init__.py b/src/ssvc/outcomes/ssvc/__init__.py new file mode 100644 index 00000000..55937544 --- /dev/null +++ b/src/ssvc/outcomes/ssvc/__init__.py @@ -0,0 +1,25 @@ +# 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 outcome group objects in the ssvc namespace +""" + +from .coordinate import LATEST as COORDINATE +from .dsoi import LATEST as DSOI +from .publish import LATEST as PUBLISH diff --git a/src/ssvc/outcomes/ssvc/coordinate.py b/src/ssvc/outcomes/ssvc/coordinate.py new file mode 100644 index 00000000..136d987e --- /dev/null +++ b/src/ssvc/outcomes/ssvc/coordinate.py @@ -0,0 +1,53 @@ +# 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 ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DECLINE = DecisionPointValue(name="Decline", key="D", description="Decline") + +_TRACK = DecisionPointValue(name="Track", key="T", description="Track") + +_COORDINATE = DecisionPointValue(name="Coordinate", key="C", description="Coordinate") + +COORDINATE = SsvcDecisionPoint( + name="Decline, Track, Coordinate", + key="COORDINATE", + description="The coordinate outcome group.", + version="1.0.0", + values=( + _DECLINE, + _TRACK, + _COORDINATE, + ), +) +""" +The coordinate outcome group. +""" + +VERSIONS = (COORDINATE,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc/dsoi.py b/src/ssvc/outcomes/ssvc/dsoi.py new file mode 100644 index 00000000..cf477abb --- /dev/null +++ b/src/ssvc/outcomes/ssvc/dsoi.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# 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 the defer, scheduled, out-of-cycle, immediate outcome group for use in SSVC. +""" +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DEFER = DecisionPointValue(name="Defer", key="D", description="Defer") + +_SCHEDULED = DecisionPointValue(name="Scheduled", key="S", description="Scheduled") + +_OUT_OF_CYCLE = DecisionPointValue( + name="Out-of-Cycle", key="O", description="Out-of-Cycle" +) + +_IMMEDIATE = DecisionPointValue(name="Immediate", key="I", description="Immediate") + +DSOI = SsvcDecisionPoint( + name="Defer, Scheduled, Out-of-Cycle, Immediate", + key="DSOI", + description="The original SSVC outcome group.", + version="1.0.0", + values=( + _DEFER, + _SCHEDULED, + _OUT_OF_CYCLE, + _IMMEDIATE, + ), +) +""" +The original SSVC outcome group. +""" + + +VERSIONS = (DSOI,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc/publish.py b/src/ssvc/outcomes/ssvc/publish.py new file mode 100644 index 00000000..bcc2eb13 --- /dev/null +++ b/src/ssvc/outcomes/ssvc/publish.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# 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 ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DO_NOT_PUBLISH = DecisionPointValue( + name="Do Not Publish", key="N", description="Do Not Publish" +) + +_PUBLISH = DecisionPointValue(name="Publish", key="P", description="Publish") + +PUBLISH = SsvcDecisionPoint( + name="Publish, Do Not Publish", + key="PUBLISH", + description="The publish outcome group.", + version="1.0.0", + values=( + _DO_NOT_PUBLISH, + _PUBLISH, + ), +) +""" +The publish outcome group. +""" + +VERSIONS = (PUBLISH,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc_/__init__.py b/src/ssvc/outcomes/ssvc_/__init__.py deleted file mode 100644 index 60041bf3..00000000 --- a/src/ssvc/outcomes/ssvc_/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -""" -Provides outcome group objects in the ssvc namespace -""" - -from .coordinate import LATEST as COORDINATE -from .dsoi import LATEST as DSOI -from .publish import LATEST as PUBLISH diff --git a/src/ssvc/outcomes/ssvc_/coordinate.py b/src/ssvc/outcomes/ssvc_/coordinate.py deleted file mode 100644 index f1c8259c..00000000 --- a/src/ssvc/outcomes/ssvc_/coordinate.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue -from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint - -_DECLINE = DecisionPointValue(name="Decline", key="D", description="Decline") - -_TRACK = DecisionPointValue(name="Track", key="T", description="Track") - -_COORDINATE = DecisionPointValue(name="Coordinate", key="C", description="Coordinate") - -COORDINATE = SsvcDecisionPoint( - name="Decline, Track, Coordinate", - key="COORDINATE", - description="The coordinate outcome group.", - version="1.0.0", - values=( - _DECLINE, - _TRACK, - _COORDINATE, - ), -) -""" -The coordinate outcome group. -""" - -VERSIONS = (COORDINATE,) -LATEST = VERSIONS[-1] - - -def main(): - print_versions_and_diffs(VERSIONS) - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/outcomes/ssvc_/dsoi.py b/src/ssvc/outcomes/ssvc_/dsoi.py deleted file mode 100644 index 297c3f56..00000000 --- a/src/ssvc/outcomes/ssvc_/dsoi.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University -""" -Provides the defer, scheduled, out-of-cycle, immediate outcome group for use in SSVC. -""" -from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue -from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint - -_DEFER = DecisionPointValue(name="Defer", key="D", description="Defer") - -_SCHEDULED = DecisionPointValue(name="Scheduled", key="S", description="Scheduled") - -_OUT_OF_CYCLE = DecisionPointValue( - name="Out-of-Cycle", key="O", description="Out-of-Cycle" -) - -_IMMEDIATE = DecisionPointValue(name="Immediate", key="I", description="Immediate") - -DSOI = SsvcDecisionPoint( - name="Defer, Scheduled, Out-of-Cycle, Immediate", - key="DSOI", - description="The original SSVC outcome group.", - version="1.0.0", - values=( - _DEFER, - _SCHEDULED, - _OUT_OF_CYCLE, - _IMMEDIATE, - ), -) -""" -The original SSVC outcome group. -""" - - -VERSIONS = (DSOI,) -LATEST = VERSIONS[-1] - - -def main(): - print_versions_and_diffs(VERSIONS) - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/outcomes/ssvc_/publish.py b/src/ssvc/outcomes/ssvc_/publish.py deleted file mode 100644 index 58311b6e..00000000 --- a/src/ssvc/outcomes/ssvc_/publish.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University - -from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue -from ssvc.decision_points.helpers import print_versions_and_diffs -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint - -_DO_NOT_PUBLISH = DecisionPointValue( - name="Do Not Publish", key="N", description="Do Not Publish" -) - -_PUBLISH = DecisionPointValue(name="Publish", key="P", description="Publish") - -PUBLISH = SsvcDecisionPoint( - name="Publish, Do Not Publish", - key="PUBLISH", - description="The publish outcome group.", - version="1.0.0", - values=( - _DO_NOT_PUBLISH, - _PUBLISH, - ), -) -""" -The publish outcome group. -""" - -VERSIONS = (PUBLISH,) -LATEST = VERSIONS[-1] - - -def main(): - print_versions_and_diffs(VERSIONS) - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index c77e649d..b99b4b8e 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -336,11 +336,11 @@ def _is_topological_order(self, node_order: list) -> bool: def main(): - from ssvc.decision_points.ssvc_.automatable import AUTOMATABLE_2 - from ssvc.decision_points.ssvc_.exploitation import EXPLOITATION_1 - from ssvc.decision_points.ssvc_.human_impact import HUMAN_IMPACT_2 - from ssvc.decision_points.ssvc_.system_exposure import SYSTEM_EXPOSURE_1_0_1 - from ssvc.outcomes.ssvc_.dsoi import DSOI + from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 + from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 + from ssvc.decision_points.ssvc.human_impact import HUMAN_IMPACT_2 + from ssvc.decision_points.ssvc.system_exposure import SYSTEM_EXPOSURE_1_0_1 + from ssvc.outcomes.ssvc.dsoi import DSOI # set up logging logger = logging.getLogger() diff --git a/src/test/decision_points/test_dp_base.py b/src/test/decision_points/test_dp_base.py index faa68b4b..9ff78118 100644 --- a/src/test/decision_points/test_dp_base.py +++ b/src/test/decision_points/test_dp_base.py @@ -20,7 +20,7 @@ import unittest import ssvc.decision_points.base as base -import ssvc.decision_points.ssvc_.base +import ssvc.decision_points.ssvc.base class MyTestCase(unittest.TestCase): @@ -36,7 +36,7 @@ def setUp(self) -> None: ) ) - self.dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( + self.dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="foo", key="bar", description="baz", @@ -61,7 +61,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -74,7 +74,7 @@ def test_registry(self): # just by creating the objects, they should be registered self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - dp2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name="asdfad", key="asdfasdf", description="asdfasdf", @@ -124,7 +124,7 @@ def test_ssvc_decision_point_json_roundtrip(self): self.assertIsInstance(json, str) self.assertGreater(len(json), 0) - obj2 = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint.model_validate_json( + obj2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint.model_validate_json( json ) diff --git a/src/test/dp_groups/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py index fed3dc76..b6342b06 100644 --- a/src/test/dp_groups/test_dp_groups.py +++ b/src/test/dp_groups/test_dp_groups.py @@ -1,19 +1,25 @@ -# Copyright (c) 2023-2025 Carnegie Mellon University and Contributors. -# - see Contributors.md for a full list of Contributors -# - see ContributionInstructions.md for information on how you can Contribute to this project -# Stakeholder Specific Vulnerability Categorization (SSVC) is -# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed -# with this Software or contact permission@sei.cmu.edu for full terms. -# Created, in part, with funding and support from the United States Government -# (see Acknowledgments file). This program may include and/or can make use of -# certain third party source code, object code, documentation and other files -# (“Third Party Software”). See LICENSE.md for more details. -# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the -# U.S. Patent and Trademark Office by Carnegie Mellon University +# Copyright (c) 2023-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 import unittest -import ssvc.decision_points.ssvc_.base +import ssvc.decision_points.ssvc.base import ssvc.dp_groups.base as dpg from ssvc.decision_points.base import DecisionPointValue @@ -22,7 +28,7 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: self.dps = [] for i in range(10): - dp = ssvc.decision_points.ssvc_.base.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( name=f"Decision Point {i}", key=f"DP_{i}", description=f"Description of Decision Point {i}", diff --git a/src/test/test_schema.py b/src/test/test_schema.py index a88fcfad..383f60d4 100644 --- a/src/test/test_schema.py +++ b/src/test/test_schema.py @@ -28,16 +28,18 @@ import ssvc.decision_points # noqa F401 from ssvc.decision_points.base import REGISTERED_DECISION_POINTS + # importing these causes the decision points to register themselves -from ssvc.decision_points.ssvc_.critical_software import CRITICAL_SOFTWARE_1 # noqa -from ssvc.decision_points.ssvc_.high_value_asset import HIGH_VALUE_ASSET_1 # noqa -from ssvc.decision_points.ssvc_.in_kev import IN_KEV_1 +from ssvc.decision_points.ssvc.critical_software import CRITICAL_SOFTWARE_1 # noqa +from ssvc.decision_points.ssvc.high_value_asset import HIGH_VALUE_ASSET_1 # noqa +from ssvc.decision_points.ssvc.in_kev import IN_KEV_1 from ssvc.dp_groups.cvss.collections import ( CVSSv1, CVSSv2, CVSSv3, CVSSv4, ) # noqa + # importing these causes the decision points to register themselves from ssvc.dp_groups.ssvc.collections import SSVCv1, SSVCv2, SSVCv2_1 # noqa @@ -98,15 +100,13 @@ def test_decision_point_validation(self): loaded = json.loads(as_json) try: - Draft202012Validator( - {"$ref": schema_url}, registry=registry - ).validate(loaded) + Draft202012Validator({"$ref": schema_url}, registry=registry).validate( + loaded + ) except jsonschema.exceptions.ValidationError as e: exp = e - self.assertIsNone( - exp, f"Validation failed for {dp.name} {dp.version}" - ) + self.assertIsNone(exp, f"Validation failed for {dp.name} {dp.version}") self.logger.debug( f"Validation passed for Decision Point ({dp.namespace}) {dp.name} v{dp.version}" ) @@ -119,15 +119,13 @@ def test_decision_point_group_validation(self): loaded = json.loads(as_json) try: - Draft202012Validator( - {"$ref": schema_url}, registry=registry - ).validate(loaded) + Draft202012Validator({"$ref": schema_url}, registry=registry).validate( + loaded + ) except jsonschema.exceptions.ValidationError as e: exp = e - self.assertIsNone( - exp, f"Validation failed for {dpg.name} {dpg.version}" - ) + self.assertIsNone(exp, f"Validation failed for {dpg.name} {dpg.version}") self.logger.debug( f"Validation passed for Decision Point Group {dpg.name} v{dpg.version}" ) From df69a40fc9f3de1054c64651d5660be60a88656e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 13:57:24 -0400 Subject: [PATCH 93/99] make registry fail on duplicate item keys --- src/ssvc/decision_points/base.py | 17 +++++++++++++++-- src/ssvc/decision_points/cvss/helpers.py | 10 ++++++++-- src/ssvc/doctools.py | 15 ++++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 9f38fd2b..69e42d96 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -24,7 +24,7 @@ import logging -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator from ssvc._mixins import ( _Base, @@ -53,8 +53,19 @@ def __getitem__(self, key: str) -> object: return self.registry[key] def __setitem__(self, key: str, value: object) -> None: + if key in self.registry: - logger.warning(f"Duplicate key {key}") + # are the values the same? + registered = self.registry[key].model_dump_json() + value_dumped = value.model_dump_json() + if registered == value_dumped: + logger.warning(f"Duplicate key {key} with the same value, ignoring.") + return + + logger.warning(f"Duplicate key {key}:") + logger.warning(f"\t{registered}") + logger.warning(f"\t{value_dumped}") + raise KeyError(f"Duplicate key {key}") self.registry[key] = value @@ -171,6 +182,8 @@ class DecisionPoint( values: tuple[DecisionPointValue, ...] + model_config = ConfigDict(revalidate_instances="always") + def __str__(self): return FIELD_DELIMITER.join([self.namespace, self.key, self.version]) diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index 4fe610a2..c2677735 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -23,6 +23,8 @@ from copy import deepcopy +import semver + from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint as DecisionPoint @@ -30,8 +32,8 @@ def _modify_3(dp: DecisionPoint): _dp_dict = deepcopy(dp.model_dump()) - _dp_dict["name"] = "Modified " + _dp_dict["name"] - _dp_dict["key"] = "M" + _dp_dict["key"] + _dp_dict["name"] = f"Modified {_dp_dict["name"]}" + _dp_dict["key"] = f"M{_dp_dict["key"]}" # if there is no value named "Not Defined" value, add it nd = NOT_DEFINED_X @@ -59,6 +61,7 @@ def modify_3(dp: DecisionPoint): """ _dp = _modify_3(dp) + DecisionPoint.model_validate(_dp) # validate the modified object return _dp @@ -75,6 +78,7 @@ def modify_4(dp: DecisionPoint): _dp = _modify_3(dp) _dp = _modify_4(_dp) + DecisionPoint.model_validate(_dp) # validate the modified object return _dp @@ -91,6 +95,8 @@ def _modify_4(dp: DecisionPoint): if v["key"] == "N": v["name"] = "Negligible" v["description"] = v["description"].replace(" no ", " negligible ") + # we need to bump the version for this change + _dp_dict["version"] = semver.bump_patch(_dp_dict["version"]) break # Note: For MSI, There is also a highest severity level, Safety (S), in addition to the same values as the diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py index 09bfcce9..b5bf0fa2 100644 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -37,6 +37,7 @@ import importlib import logging import os +import re from ssvc.decision_points.base import ( DecisionPoint, @@ -86,7 +87,13 @@ def _filename_friendly(name: str) -> str: Returns: str: A version of the string that is friendly for use in a filename. """ - return name.lower().replace(" ", "_").replace(".", "_") + # replace all non-alphanumeric characters with underscores and convert to lowercase + name = re.sub(r"[^a-zA-Z0-9]", "_", name) + name = name.lower() + # replace any sequence of underscores with a single underscore + name = re.sub(r"_+", "_", name) + + return name # create a runtime context that ensures that dir exists @@ -218,8 +225,10 @@ def main(): find_modules_to_import("./decision_points", "ssvc.decision_points") find_modules_to_import("./outcomes", "ssvc.outcomes") - from ssvc.dp_groups.ssvc import collections # noqa: E402 - from ssvc.dp_groups.cvss import collections # noqa: E402 + + # import collections to ensure they are registered too + import ssvc.dp_groups.ssvc.collections # noqa: F401 + import ssvc.dp_groups.cvss.collections # noqa: F401 # for each decision point: for dp in REGISTERED_DECISION_POINTS: From f7e6a2a471238efee6cd6e9e72550ac11781072b Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 13:57:39 -0400 Subject: [PATCH 94/99] fix duplicate keys --- src/ssvc/decision_points/cvss/supplemental/safety.py | 2 +- src/ssvc/decision_points/ssvc/supplier_contacted.py | 2 +- src/ssvc/decision_points/ssvc/supplier_involvement.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ssvc/decision_points/cvss/supplemental/safety.py b/src/ssvc/decision_points/cvss/supplemental/safety.py index 5666cb27..c05d62cf 100644 --- a/src/ssvc/decision_points/cvss/supplemental/safety.py +++ b/src/ssvc/decision_points/cvss/supplemental/safety.py @@ -42,7 +42,7 @@ SAFETY_1 = CvssDecisionPoint( name="Safety", description="The Safety decision point is a measure of the potential for harm to humans or the environment.", - key="S", + key="SF", version="1.0.0", values=( NOT_DEFINED_X, diff --git a/src/ssvc/decision_points/ssvc/supplier_contacted.py b/src/ssvc/decision_points/ssvc/supplier_contacted.py index 9d440415..2b6d16d9 100644 --- a/src/ssvc/decision_points/ssvc/supplier_contacted.py +++ b/src/ssvc/decision_points/ssvc/supplier_contacted.py @@ -40,7 +40,7 @@ SUPPLIER_CONTACTED_1 = SsvcDecisionPoint( name="Supplier Contacted", description="Has the reporter made a good-faith effort to contact the supplier of the vulnerable component using a quality contact method?", - key="SC", + key="SCON", version="1.0.0", values=( NO, diff --git a/src/ssvc/decision_points/ssvc/supplier_involvement.py b/src/ssvc/decision_points/ssvc/supplier_involvement.py index 07dcfc2d..253620d9 100644 --- a/src/ssvc/decision_points/ssvc/supplier_involvement.py +++ b/src/ssvc/decision_points/ssvc/supplier_involvement.py @@ -47,7 +47,7 @@ SUPPLIER_INVOLVEMENT_1 = SsvcDecisionPoint( name="Supplier Involvement", description="What is the state of the supplier’s work on addressing the vulnerability?", - key="SI", + key="SINV", version="1.0.0", values=( FIX_READY, From 43f551b7e84c95a0836ca1dc2015cb450a2ee8a0 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 13:57:58 -0400 Subject: [PATCH 95/99] regenerate json examples --- .../cisa/cisa_levels_1_0_0.json} | 10 +++--- .../cvss/availability_impact_2_0_1.json | 25 ------------- .../cvss/confidentiality_impact_2_0_1.json | 25 ------------- ...alitative_severity_rating_scale_1_0_0.json | 35 +++++++++++++++++++ .../cvss/integrity_impact_2_0_1.json | 25 ------------- .../cvss/integrity_requirement_1_0_1.json | 30 ---------------- .../modified_availability_impact_2_0_1.json | 30 ---------------- ...impact_to_the_subsequent_system_1_0_0.json | 4 +-- ...mpact_to_the_subsequent_system_1_0_1.json} | 8 ++--- ...impact_to_the_subsequent_system_1_0_0.json | 2 +- ...mpact_to_the_subsequent_system_1_0_1.json} | 16 ++++----- .../cvss/modified_integrity_impact_2_0_1.json | 30 ---------------- ...impact_to_the_subsequent_system_1_0_0.json | 9 ++--- ...impact_to_the_subsequent_system_1_0_1.json | 35 +++++++++++++++++++ .../decision_points/cvss/safety_1_0_0.json | 2 +- .../subsequent_availability_impact_1_0_0.json | 25 ------------- .../ssvc/critical_software_1_0_0.json | 20 +++++++++++ .../ssvc/decline_track_coordinate_1_0_0.json} | 10 +++--- ...heduled_out_of_cycle_immediate_1_0_0.json} | 10 +++--- .../ssvc/high_value_asset_1_0_0.json | 20 +++++++++++ .../ssvc/human_impact_1_0_0.json | 30 ---------------- .../decision_points/ssvc/in_kev_1_0_0.json | 20 +++++++++++ ... mission_and_well_being_impact_1_0_0.json} | 0 .../ssvc/public_safety_impact_1_0_0.json | 20 ----------- ...on => public_well_being_impact_1_0_0.json} | 0 .../ssvc/publish_do_not_publish_1_0_0.json} | 10 +++--- .../ssvc/supplier_contacted_1_0_0.json | 2 +- .../ssvc/supplier_involvement_1_0_0.json | 2 +- .../do_schedule_delegate_delete_1_0_0.json} | 10 +++--- .../x_basic/moscow_1_0_0.json} | 10 +++--- .../x_basic/value_complexity_1_0_0.json} | 10 +++--- .../x_basic/yes_no_1_0_0.json} | 10 +++--- .../x_community/theparanoids_1_0_0.json} | 10 +++--- data/json/outcomes/CVSS.json | 28 --------------- 34 files changed, 204 insertions(+), 329 deletions(-) rename data/json/{outcomes/CISA.json => decision_points/cisa/cisa_levels_1_0_0.json} (97%) delete mode 100644 data/json/decision_points/cvss/availability_impact_2_0_1.json delete mode 100644 data/json/decision_points/cvss/confidentiality_impact_2_0_1.json create mode 100644 data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json delete mode 100644 data/json/decision_points/cvss/integrity_impact_2_0_1.json delete mode 100644 data/json/decision_points/cvss/integrity_requirement_1_0_1.json delete mode 100644 data/json/decision_points/cvss/modified_availability_impact_2_0_1.json rename data/json/decision_points/cvss/{modified_subsequent_availability_impact_1_0_0.json => modified_availability_impact_to_the_subsequent_system_1_0_1.json} (81%) rename data/json/decision_points/cvss/{modified_confidentiality_impact_2_0_1.json => modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json} (52%) delete mode 100644 data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json create mode 100644 data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json delete mode 100644 data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json create mode 100644 data/json/decision_points/ssvc/critical_software_1_0_0.json rename data/json/{outcomes/COORDINATE.json => decision_points/ssvc/decline_track_coordinate_1_0_0.json} (86%) rename data/json/{outcomes/DSOI.json => decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json} (90%) create mode 100644 data/json/decision_points/ssvc/high_value_asset_1_0_0.json delete mode 100644 data/json/decision_points/ssvc/human_impact_1_0_0.json create mode 100644 data/json/decision_points/ssvc/in_kev_1_0_0.json rename data/json/decision_points/ssvc/{mission_and_well-being_impact_1_0_0.json => mission_and_well_being_impact_1_0_0.json} (100%) delete mode 100644 data/json/decision_points/ssvc/public_safety_impact_1_0_0.json rename data/json/decision_points/ssvc/{public_well-being_impact_1_0_0.json => public_well_being_impact_1_0_0.json} (100%) rename data/json/{outcomes/PUBLISH.json => decision_points/ssvc/publish_do_not_publish_1_0_0.json} (84%) rename data/json/{outcomes/EISENHOWER.json => decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json} (89%) rename data/json/{outcomes/MOSCOW.json => decision_points/x_basic/moscow_1_0_0.json} (71%) rename data/json/{outcomes/VALUE_COMPLEXITY.json => decision_points/x_basic/value_complexity_1_0_0.json} (87%) rename data/json/{outcomes/YES_NO.json => decision_points/x_basic/yes_no_1_0_0.json} (82%) rename data/json/{outcomes/THE_PARANOIDS.json => decision_points/x_community/theparanoids_1_0_0.json} (91%) delete mode 100644 data/json/outcomes/CVSS.json diff --git a/data/json/outcomes/CISA.json b/data/json/decision_points/cisa/cisa_levels_1_0_0.json similarity index 97% rename from data/json/outcomes/CISA.json rename to data/json/decision_points/cisa/cisa_levels_1_0_0.json index c4ebbd2a..e836c69c 100644 --- a/data/json/outcomes/CISA.json +++ b/data/json/decision_points/cisa/cisa_levels_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "CISA Levels", "description": "The CISA outcome group. CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", - "outcomes": [ + "namespace": "cisa", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CISA", + "values": [ { "key": "T", "name": "Track", @@ -25,4 +27,4 @@ "description": "The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. CISA recommends remediating Act vulnerabilities as soon as possible." } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/cvss/availability_impact_2_0_1.json b/data/json/decision_points/cvss/availability_impact_2_0_1.json deleted file mode 100644 index e815d46a..00000000 --- a/data/json/decision_points/cvss/availability_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "A", - "name": "Availability Impact", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "There is reduced performance or interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users. The resources in the Vulnerable System are either partially available all of the time, or fully available only some of the time, but overall there is no direct, serious consequence to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - } - ] -} diff --git a/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json b/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json deleted file mode 100644 index 4c72a5d5..00000000 --- a/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "C", - "name": "Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of confidentiality within the impacted component." - }, - { - "key": "L", - "name": "Low", - "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is constrained. The information disclosure does not cause a direct, serious loss to the impacted component." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of confidentiality, resulting in all resources within the impacted component being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact. For example, an attacker steals the administrator's password, or private encryption keys of a web server." - } - ] -} diff --git a/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json b/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json new file mode 100644 index 00000000..82135a43 --- /dev/null +++ b/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json @@ -0,0 +1,35 @@ +{ + "name": "CVSS Qualitative Severity Rating Scale", + "description": "The CVSS Qualitative Severity Rating Scale group.", + "namespace": "cvss", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CVSS", + "values": [ + { + "key": "N", + "name": "None", + "description": "None (0.0)" + }, + { + "key": "L", + "name": "Low", + "description": "Low (0.1-3.9)" + }, + { + "key": "M", + "name": "Medium", + "description": "Medium (4.0-6.9)" + }, + { + "key": "H", + "name": "High", + "description": "High (7.0-8.9)" + }, + { + "key": "C", + "name": "Critical", + "description": "Critical (9.0-10.0)" + } + ] +} diff --git a/data/json/decision_points/cvss/integrity_impact_2_0_1.json b/data/json/decision_points/cvss/integrity_impact_2_0_1.json deleted file mode 100644 index 59579fbd..00000000 --- a/data/json/decision_points/cvss/integrity_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "I", - "name": "Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of integrity within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of integrity, or a complete loss of protection." - } - ] -} diff --git a/data/json/decision_points/cvss/integrity_requirement_1_0_1.json b/data/json/decision_points/cvss/integrity_requirement_1_0_1.json deleted file mode 100644 index 4c8e1762..00000000 --- a/data/json/decision_points/cvss/integrity_requirement_1_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "1.0.1", - "schemaVersion": "1-0-1", - "key": "IR", - "name": "Integrity Requirement", - "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", - "values": [ - { - "key": "L", - "name": "Low", - "description": "Loss of integrity is likely to have only a limited adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "M", - "name": "Medium", - "description": "Loss of integrity is likely to have a serious adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "H", - "name": "High", - "description": "Loss of integrity is likely to have a catastrophic adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json b/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json deleted file mode 100644 index 793c5579..00000000 --- a/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "MA", - "name": "Modified Availability Impact", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "There is reduced performance or interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users. The resources in the Vulnerable System are either partially available all of the time, or fully available only some of the time, but overall there is no direct, serious consequence to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json index b36e78ae..1bd3acd8 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json @@ -8,8 +8,8 @@ "values": [ { "key": "N", - "name": "Negligible", - "description": "There is negligible impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." + "name": "None", + "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json similarity index 81% rename from data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json rename to data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json index d8f83c65..bed5818f 100644 --- a/data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json @@ -1,15 +1,15 @@ { + "name": "Modified Availability Impact to the Subsequent System", + "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "namespace": "cvss", - "version": "1.0.0", + "version": "1.0.1", "schemaVersion": "1-0-1", "key": "MSA", - "name": "Modified Subsequent Availability Impact", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "values": [ { "key": "N", "name": "Negligible", - "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." + "description": "There is negligible impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json index 03e23cf7..ea677a2a 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_0.json @@ -9,7 +9,7 @@ { "key": "N", "name": "Negligible", - "description": "There is negligible loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." + "description": "There is no loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json similarity index 52% rename from data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json rename to data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json index 027f96a0..3e4adaa2 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json @@ -1,25 +1,25 @@ { + "name": "Modified Confidentiality Impact to the Subsequent System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "namespace": "cvss", - "version": "2.0.1", + "version": "1.0.1", "schemaVersion": "1-0-1", - "key": "MC", - "name": "Modified Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", + "key": "MSC", "values": [ { "key": "N", - "name": "None", - "description": "There is no loss of confidentiality within the impacted component." + "name": "Negligible", + "description": "There is negligible loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." }, { "key": "L", "name": "Low", - "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is constrained. The information disclosure does not cause a direct, serious loss to the impacted component." + "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is limited. The information disclosure does not cause a direct, serious loss to the Subsequent System." }, { "key": "H", "name": "High", - "description": "There is total loss of confidentiality, resulting in all resources within the impacted component being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact. For example, an attacker steals the administrator's password, or private encryption keys of a web server." + "description": "There is a total loss of confidentiality, resulting in all resources within the Subsequent System being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact." }, { "key": "X", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json b/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json deleted file mode 100644 index a02b0fe3..00000000 --- a/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "MI", - "name": "Modified Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of integrity within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of integrity, or a complete loss of protection." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json index ab2207f7..6af70ddc 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json @@ -8,8 +8,8 @@ "values": [ { "key": "N", - "name": "Negligible", - "description": "There is negligible loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." + "name": "None", + "description": "There is no loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." }, { "key": "L", @@ -25,11 +25,6 @@ "key": "X", "name": "Not Defined", "description": "This metric value is not defined. See CVSS documentation for details." - }, - { - "key": "S", - "name": "Safety", - "description": "The Safety metric value measures the impact regarding the Safety of a human actor or participant that can be predictably injured as a result of the vulnerability being exploited." } ] } diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json new file mode 100644 index 00000000..7e0c391a --- /dev/null +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json @@ -0,0 +1,35 @@ +{ + "name": "Modified Integrity Impact to the Subsequent System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", + "namespace": "cvss", + "version": "1.0.1", + "schemaVersion": "1-0-1", + "key": "MSI", + "values": [ + { + "key": "N", + "name": "Negligible", + "description": "There is negligible loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." + }, + { + "key": "L", + "name": "Low", + "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Subsequent System." + }, + { + "key": "H", + "name": "High", + "description": "There is a total loss of integrity, or a complete loss of protection. For example, the attacker is able to modify any/all files protected by the Subsequent System. Alternatively, only some files can be modified, but malicious modification would present a direct, serious consequence to the Subsequent System." + }, + { + "key": "X", + "name": "Not Defined", + "description": "This metric value is not defined. See CVSS documentation for details." + }, + { + "key": "S", + "name": "Safety", + "description": "The Safety metric value measures the impact regarding the Safety of a human actor or participant that can be predictably injured as a result of the vulnerability being exploited." + } + ] +} diff --git a/data/json/decision_points/cvss/safety_1_0_0.json b/data/json/decision_points/cvss/safety_1_0_0.json index 987de4d0..b1505feb 100644 --- a/data/json/decision_points/cvss/safety_1_0_0.json +++ b/data/json/decision_points/cvss/safety_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "S", + "key": "SF", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json b/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json deleted file mode 100644 index a7ed8c04..00000000 --- a/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "1.0.0", - "schemaVersion": "1-0-1", - "key": "SA", - "name": "Subsequent Availability Impact", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Performance is reduced or there are interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of availability, resulting in the attacker being able to fully deny access to resources in the Subsequent System; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - } - ] -} diff --git a/data/json/decision_points/ssvc/critical_software_1_0_0.json b/data/json/decision_points/ssvc/critical_software_1_0_0.json new file mode 100644 index 00000000..232c633d --- /dev/null +++ b/data/json/decision_points/ssvc/critical_software_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "Critical Software", + "description": "Denotes whether a system meets a critical software definition.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CS", + "values": [ + { + "key": "N", + "name": "No", + "description": "System does not meet a critical software definition." + }, + { + "key": "Y", + "name": "Yes", + "description": "System meets a critical software definition." + } + ] +} diff --git a/data/json/outcomes/COORDINATE.json b/data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json similarity index 86% rename from data/json/outcomes/COORDINATE.json rename to data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json index 67a4d9fa..fc7aae55 100644 --- a/data/json/outcomes/COORDINATE.json +++ b/data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Decline, Track, Coordinate", "description": "The coordinate outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "COORDINATE", + "values": [ { "key": "D", "name": "Decline", @@ -20,4 +22,4 @@ "description": "Coordinate" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/DSOI.json b/data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json similarity index 90% rename from data/json/outcomes/DSOI.json rename to data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json index 8e16b6f6..2474d981 100644 --- a/data/json/outcomes/DSOI.json +++ b/data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Defer, Scheduled, Out-of-Cycle, Immediate", "description": "The original SSVC outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "DSOI", + "values": [ { "key": "D", "name": "Defer", @@ -25,4 +27,4 @@ "description": "Immediate" } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/ssvc/high_value_asset_1_0_0.json b/data/json/decision_points/ssvc/high_value_asset_1_0_0.json new file mode 100644 index 00000000..a0e94922 --- /dev/null +++ b/data/json/decision_points/ssvc/high_value_asset_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "High Value Asset", + "description": "Denotes whether a system meets a high value asset definition.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "HVA", + "values": [ + { + "key": "N", + "name": "No", + "description": "System does not meet a high value asset definition." + }, + { + "key": "Y", + "name": "Yes", + "description": "System meets a high value asset definition." + } + ] +} diff --git a/data/json/decision_points/ssvc/human_impact_1_0_0.json b/data/json/decision_points/ssvc/human_impact_1_0_0.json deleted file mode 100644 index 051c3789..00000000 --- a/data/json/decision_points/ssvc/human_impact_1_0_0.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schemaVersion": "1-0-1", - "namespace": "ssvc", - "version": "1.0.0", - "key": "HI", - "name": "Human Impact", - "description": "Human Impact is a combination of Safety and Mission impacts.", - "values": [ - { - "key": "L", - "name": "Low", - "description": "Safety Impact:(None OR Minor) AND Mission Impact:(None OR Degraded OR Crippled)" - }, - { - "key": "M", - "name": "Medium", - "description": "(Safety Impact:(None OR Minor) AND Mission Impact:MEF Failure) OR (Safety Impact:Major AND Mission Impact:(None OR Degraded OR Crippled))" - }, - { - "key": "H", - "name": "High", - "description": "(Safety Impact:Hazardous AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Major AND Mission Impact:MEF Failure)" - }, - { - "key": "VH", - "name": "Very High", - "description": "Safety Impact:Catastrophic OR Mission Impact:Mission Failure" - } - ] -} \ No newline at end of file diff --git a/data/json/decision_points/ssvc/in_kev_1_0_0.json b/data/json/decision_points/ssvc/in_kev_1_0_0.json new file mode 100644 index 00000000..cadc960b --- /dev/null +++ b/data/json/decision_points/ssvc/in_kev_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "In KEV", + "description": "Denotes whether a vulnerability is in the CISA Known Exploited Vulnerabilities (KEV) list.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "KEV", + "values": [ + { + "key": "N", + "name": "No", + "description": "Vulnerability is not listed in KEV." + }, + { + "key": "Y", + "name": "Yes", + "description": "Vulnerability is listed in KEV." + } + ] +} diff --git a/data/json/decision_points/ssvc/mission_and_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/mission_and_well_being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/ssvc/mission_and_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/mission_and_well_being_impact_1_0_0.json diff --git a/data/json/decision_points/ssvc/public_safety_impact_1_0_0.json b/data/json/decision_points/ssvc/public_safety_impact_1_0_0.json deleted file mode 100644 index 0426c72b..00000000 --- a/data/json/decision_points/ssvc/public_safety_impact_1_0_0.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schemaVersion": "1-0-1", - "namespace": "ssvc", - "version": "1.0.0", - "key": "PSI", - "name": "Public Safety Impact", - "description": "A coarse-grained representation of impact to public safety.", - "values": [ - { - "key": "M", - "name": "Minimal", - "description": "Safety Impact:(None OR Minor)" - }, - { - "key": "S", - "name": "Significant", - "description": "Safety Impact:(Major OR Hazardous OR Catastrophic)" - } - ] -} \ No newline at end of file diff --git a/data/json/decision_points/ssvc/public_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/public_well_being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/ssvc/public_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/public_well_being_impact_1_0_0.json diff --git a/data/json/outcomes/PUBLISH.json b/data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json similarity index 84% rename from data/json/outcomes/PUBLISH.json rename to data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json index fd656624..aa5d01fc 100644 --- a/data/json/outcomes/PUBLISH.json +++ b/data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Publish, Do Not Publish", "description": "The publish outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "PUBLISH", + "values": [ { "key": "N", "name": "Do Not Publish", @@ -15,4 +17,4 @@ "description": "Publish" } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json b/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json index c32d5755..ee1cfb50 100644 --- a/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json +++ b/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "SC", + "key": "SCON", "values": [ { "key": "N", diff --git a/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json b/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json index 15d014e5..b1e48b02 100644 --- a/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json +++ b/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "SI", + "key": "SINV", "values": [ { "key": "FR", diff --git a/data/json/outcomes/EISENHOWER.json b/data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json similarity index 89% rename from data/json/outcomes/EISENHOWER.json rename to data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json index 40d98902..f83890fe 100644 --- a/data/json/outcomes/EISENHOWER.json +++ b/data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Do, Schedule, Delegate, Delete", "description": "The Eisenhower outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "IKE", + "values": [ { "key": "D", "name": "Delete", @@ -25,4 +27,4 @@ "description": "Do" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/MOSCOW.json b/data/json/decision_points/x_basic/moscow_1_0_0.json similarity index 71% rename from data/json/outcomes/MOSCOW.json rename to data/json/decision_points/x_basic/moscow_1_0_0.json index 3156c47d..77955737 100644 --- a/data/json/outcomes/MOSCOW.json +++ b/data/json/decision_points/x_basic/moscow_1_0_0.json @@ -1,9 +1,11 @@ { + "name": "MoSCoW", + "description": "The MoSCoW (Must, Should, Could, Won't) outcome group.", + "namespace": "x_basic", "version": "1.0.0", "schemaVersion": "1-0-1", - "name": "Must, Should, Could, Won't", - "description": "The Moscow outcome group.", - "outcomes": [ + "key": "MSCW", + "values": [ { "key": "W", "name": "Won't", @@ -25,4 +27,4 @@ "description": "Must" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/VALUE_COMPLEXITY.json b/data/json/decision_points/x_basic/value_complexity_1_0_0.json similarity index 87% rename from data/json/outcomes/VALUE_COMPLEXITY.json rename to data/json/decision_points/x_basic/value_complexity_1_0_0.json index b60d42f8..83579078 100644 --- a/data/json/outcomes/VALUE_COMPLEXITY.json +++ b/data/json/decision_points/x_basic/value_complexity_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Value, Complexity", "description": "The Value/Complexity outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "VALUE_COMPLEXITY", + "values": [ { "key": "D", "name": "Drop", @@ -25,4 +27,4 @@ "description": "Do First" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/YES_NO.json b/data/json/decision_points/x_basic/yes_no_1_0_0.json similarity index 82% rename from data/json/outcomes/YES_NO.json rename to data/json/decision_points/x_basic/yes_no_1_0_0.json index 1a6dcdff..6a3b9b23 100644 --- a/data/json/outcomes/YES_NO.json +++ b/data/json/decision_points/x_basic/yes_no_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Yes, No", "description": "The Yes/No outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "YN", + "values": [ { "key": "N", "name": "No", @@ -15,4 +17,4 @@ "description": "Yes" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/THE_PARANOIDS.json b/data/json/decision_points/x_community/theparanoids_1_0_0.json similarity index 91% rename from data/json/outcomes/THE_PARANOIDS.json rename to data/json/decision_points/x_community/theparanoids_1_0_0.json index f19fb83d..82d8a4c5 100644 --- a/data/json/outcomes/THE_PARANOIDS.json +++ b/data/json/decision_points/x_community/theparanoids_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "theParanoids", "description": "PrioritizedRiskRemediation outcome group based on TheParanoids.", - "outcomes": [ + "namespace": "x_community", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "PARANOIDS", + "values": [ { "key": "5", "name": "Track 5", @@ -35,4 +37,4 @@ "description": "Act ASAP" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/CVSS.json b/data/json/outcomes/CVSS.json deleted file mode 100644 index 5d3d3bd2..00000000 --- a/data/json/outcomes/CVSS.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": "1.0.0", - "schemaVersion": "1-0-1", - "name": "CVSS Levels", - "description": "The CVSS outcome group.", - "outcomes": [ - { - "key": "L", - "name": "Low", - "description": "Low" - }, - { - "key": "M", - "name": "Medium", - "description": "Medium" - }, - { - "key": "H", - "name": "High", - "description": "High" - }, - { - "key": "C", - "name": "Critical", - "description": "Critical" - } - ] -} \ No newline at end of file From 13a607214741f2819440f2af36d247a779a77b05 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 14:06:11 -0400 Subject: [PATCH 96/99] add makefile target to regenerate json examples --- Makefile | 8 +++++++- src/ssvc/doctools.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) mode change 100644 => 100755 src/ssvc/doctools.py diff --git a/Makefile b/Makefile index f5e33ada..45f8bb72 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MKDOCS_PORT=8765 DOCKER_DIR=docker # Targets -.PHONY: all test docs docker_test clean help mdlint_fix up down +.PHONY: all test docs docker_test clean help mdlint_fix up down regenerate_json all: help @@ -31,6 +31,11 @@ down: @echo "Stopping Docker services..." pushd $(DOCKER_DIR) && docker-compose down +regenerate_json: + @echo "Regenerating JSON files..." + rm -rf data/json/decision_points + export PYTHONPATH=$(PWD)/src && ./src/ssvc/doctools.py --jsondir=./data/json/decision_points --overwrite + clean: @echo "Cleaning up Docker resources..." pushd $(DOCKER_DIR) && docker-compose down --rmi local || true @@ -46,6 +51,7 @@ help: @echo " docs - Build and run documentation in Docker" @echo " up - Start Docker services" @echo " down - Stop Docker services" + @echo " regenerate_json - Regenerate JSON files from python modules" @echo " clean - Clean up Docker resources" @echo " help - Display this help message" diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py old mode 100644 new mode 100755 index b5bf0fa2..b345ff26 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -223,8 +223,8 @@ def main(): overwrite = args.overwrite jsondir = args.jsondir - find_modules_to_import("./decision_points", "ssvc.decision_points") - find_modules_to_import("./outcomes", "ssvc.outcomes") + find_modules_to_import("./src/ssvc/decision_points", "ssvc.decision_points") + find_modules_to_import("./src/ssvc/outcomes", "ssvc.outcomes") # import collections to ensure they are registered too import ssvc.dp_groups.ssvc.collections # noqa: F401 From c726d788a7b5d2e895a2d4208ba0159f722892d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 14:16:45 -0400 Subject: [PATCH 97/99] fix unit tests --- src/test/decision_points/test_cvss_helpers.py | 6 +++++- src/test/dp_groups/test_dp_groups.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/test/decision_points/test_cvss_helpers.py b/src/test/decision_points/test_cvss_helpers.py index fe00b64c..dc662dee 100644 --- a/src/test/decision_points/test_cvss_helpers.py +++ b/src/test/decision_points/test_cvss_helpers.py @@ -20,7 +20,7 @@ import unittest import ssvc.decision_points.cvss.helpers as h -from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.base import DPV_REGISTRY, DP_REGISTRY, DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint @@ -56,6 +56,10 @@ def fake_ms_impacts() -> list[CvssDecisionPoint]: class TestCvssHelpers(unittest.TestCase): def setUp(self) -> None: + # reset the registry + for registry in DP_REGISTRY, DPV_REGISTRY: + registry.reset_registry() + self.dps = [] for i in range(3): dp = CvssDecisionPoint( diff --git a/src/test/dp_groups/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py index b6342b06..7feefe94 100644 --- a/src/test/dp_groups/test_dp_groups.py +++ b/src/test/dp_groups/test_dp_groups.py @@ -28,9 +28,10 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: self.dps = [] for i in range(10): - dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + dp = ssvc.decision_points.ssvc.base.DecisionPoint( name=f"Decision Point {i}", key=f"DP_{i}", + namespace="x_test", description=f"Description of Decision Point {i}", version="1.0.0", values=( From 2e56050872831c40586463f559df56603644a3d4 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 14:27:19 -0400 Subject: [PATCH 98/99] add test for registry duplicates --- src/test/decision_points/test_dp_base.py | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/test/decision_points/test_dp_base.py b/src/test/decision_points/test_dp_base.py index 9ff78118..9c340fc5 100644 --- a/src/test/decision_points/test_dp_base.py +++ b/src/test/decision_points/test_dp_base.py @@ -25,6 +25,9 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: + base.DP_REGISTRY.reset_registry() + base.DPV_REGISTRY.reset_registry() + self.original_registry = base.REGISTERED_DECISION_POINTS.copy() # add multiple values @@ -69,6 +72,55 @@ def test_registry(self): namespace="asdfasdf", values=tuple(self.values), ) + self.assertIn(dp2, base.REGISTERED_DECISION_POINTS) + + def test_registry_errors_on_duplicate_key(self): + # dp should already be registered from setUp + self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) + + # create a new decision point with the same key + with self.assertRaises(KeyError): + # the registry key is a combination of namespace, key, and version + + dp2 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, + key=self.dp.key, # same key as self.dp + version=self.dp.version, # same version as self.dp + values=tuple(self.values), + ) + + # should not be a problem if namespace, key or version are different + dp3 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace="x_test-extra", # different namespace + key=self.dp.key, # same key + version=self.dp.version, # same version + values=tuple(self.values), + ) + self.assertIn(dp3, base.REGISTERED_DECISION_POINTS) + # should not be a problem if key is different + dp4 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, # same namespace + key="different_key", # different key + version=self.dp.version, # same version + values=tuple(self.values), + ) + self.assertIn(dp4, base.REGISTERED_DECISION_POINTS) + # should not be a problem if version is different + dp5 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, # same namespace + key=self.dp.key, # same key + version="2.0.0", # different version + values=tuple(self.values), + ) + self.assertIn(dp5, base.REGISTERED_DECISION_POINTS) def test_registry(self): # just by creating the objects, they should be registered From 71ae5260210265617469b5262412e6f86e36e7f5 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Wed, 18 Jun 2025 15:12:47 -0400 Subject: [PATCH 99/99] move mission prevalence to cisa namespace (their customization was the only place it was ever used) --- .../cisa/mission_prevalence_1_0_0.json | 25 +++++++++++++++++++ .../{ssvc => cisa}/mission_prevalence.py | 4 +-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 data/json/decision_points/cisa/mission_prevalence_1_0_0.json rename src/ssvc/decision_points/{ssvc => cisa}/mission_prevalence.py (95%) diff --git a/data/json/decision_points/cisa/mission_prevalence_1_0_0.json b/data/json/decision_points/cisa/mission_prevalence_1_0_0.json new file mode 100644 index 00000000..50a06e4b --- /dev/null +++ b/data/json/decision_points/cisa/mission_prevalence_1_0_0.json @@ -0,0 +1,25 @@ +{ + "name": "Mission Prevalence", + "description": "Prevalence of the mission essential functions", + "namespace": "cisa", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "MP", + "values": [ + { + "key": "M", + "name": "Minimal", + "description": "Neither Support nor Essential apply. The vulnerable component may be used within the entities, but it is not used as a mission-essential component, nor does it provide impactful support to mission-essential functions." + }, + { + "key": "S", + "name": "Support", + "description": "The vulnerable component only supports MEFs for two or more entities." + }, + { + "key": "E", + "name": "Essential", + "description": "The vulnerable component directly provides capabilities that constitute at least one MEF for at least one entity; component failure may (but does not necessarily) lead to overall mission failure." + } + ] +} diff --git a/src/ssvc/decision_points/ssvc/mission_prevalence.py b/src/ssvc/decision_points/cisa/mission_prevalence.py similarity index 95% rename from src/ssvc/decision_points/ssvc/mission_prevalence.py rename to src/ssvc/decision_points/cisa/mission_prevalence.py index 9b3b0640..9e209c9e 100644 --- a/src/ssvc/decision_points/ssvc/mission_prevalence.py +++ b/src/ssvc/decision_points/cisa/mission_prevalence.py @@ -24,8 +24,8 @@ # DM24-0278 -from ssvc.decision_points.ssvc_.base import SsvcDecisionPoint from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs MINIMAL = DecisionPointValue( @@ -48,7 +48,7 @@ ) -MISSION_PREVALENCE = SsvcDecisionPoint( +MISSION_PREVALENCE = CisaDecisionPoint( name="Mission Prevalence", description="Prevalence of the mission essential functions", key="MP",