Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add CVSSv4 Decision Points #377

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add dp_diff helper
ahouseholder committed Nov 7, 2023
commit 5ea56976b544896db4bd26b997c9e7b6df68a292
196 changes: 196 additions & 0 deletions src/ssvc/decision_points/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env python
"""
Provides helper functions for working with SSVC decision points.
"""
# Copyright (c) 2023 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 [email protected] 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 typing import List

from ssvc.decision_points import SsvcDecisionPoint


def dp_diff(dp1: SsvcDecisionPoint, dp2: SsvcDecisionPoint) -> List[str]:
"""
Compares two decision points and returns a list of differences.

Args:
dp1: the first decision point to compare
dp2: the second decision point to compare

Returns:
A list of differences between the two decision points
"""

major = False
maybe_major = False
minor = False
maybe_minor = False
patch = False

diffs = []

name_change = False
desc_change = False
key_change = False

# did the name change?
if dp1.name != dp2.name:
name_change = True

# was it a big change?
from thefuzz import fuzz

# fuzz ratio is 100 when exact match, 0 when no match
if fuzz.ratio(dp1.name, dp2.name) < 50:
diffs.append(f"(minor) {dp2.name} name changed from {dp1.name}")
minor = True
else:
diffs.append(
f"(patch / maybe minor) {dp2.name} name changed from {dp1.name}"
)
# It was a small change so maybe minor but probably patch
patch = True
maybe_minor = True

# did the description change?
desc1 = dp1.description.strip()
desc2 = dp2.description.strip()

if desc1 != desc2:
diffs.append(f"(patch) {dp2.name} v{dp2.version} description changed")
patch = True
desc_change = True
else:
diffs.append(f"{dp2.name} v{dp2.version} description did not change")

# did the key change?
key1 = dp1.key.strip()
key2 = dp2.key.strip()

if key1 != key2:
diffs.append(f"(major) {dp2.name} v{dp2.version} key changed")
major = True
key_change = True
else:
diffs.append(f"{dp2.name} v{dp2.version} key did not change")

maybe_new_obj = all([name_change, desc_change, key_change])

# did the version change?
version1 = dp1.version.strip()
version2 = dp2.version.strip()

if version1 != version2:
diffs.append(f"{dp2.name} v{version2} version changed from {version1}")
else:
diffs.append(f"{dp2.name} version did not change")

# did the values change?
# did the value names change?
dp1_names = set([v.name for v in dp1.values])
dp2_names = set([v.name for v in dp2.values])

intersection = dp1_names.intersection(dp2_names)

if dp1_names == dp2_names:
diffs.append(f"{dp2.name} v{dp2.version} value names did not change")

# names removed from dp1 in dp2:
for name in dp1_names.difference(dp2_names):
diffs.append(f"(major) {dp2.name} v{dp2.version} removes value {name}")
major = True

for name in dp2_names.difference(dp1_names):
diffs.append(f"(major or minor) {dp2.name} v{dp2.version} adds value {name}")
maybe_major = True
maybe_minor = True

# did the value keys change?
for name in intersection:
v1 = {value["name"]: value["key"] for value in dp1.to_dict()["values"]}
v1 = v1[name]

v2 = {value["name"]: value["key"] for value in dp2.to_dict()["values"]}
v2 = v2[name]

if v1 != v2:
diffs.append(f"(minor) {dp2.name} v{dp2.version} value {name} key changed")
minor = True
else:
diffs.append(f"{dp2.name} v{dp2.version} value {name} key did not change")

# did the value descriptions change?
for name in intersection:
v1 = {value["name"]: value["description"] for value in dp1.to_dict()["values"]}
v1 = v1[name]

v2 = {value["name"]: value["description"] for value in dp2.to_dict()["values"]}
v2 = v2[name]

if v1 != v2:
diffs.append(
f"(patch) {dp2.name} v{dp2.version} value {name} description changed"
)
patch = True
else:
diffs.append(
f"{dp2.name} v{dp2.version} value {name} description did not change"
)

if major:
diffs.append(f"{dp2.name} v{dp2.version} appears to be a major change")
elif minor:
diffs.append(f"{dp2.name} v{dp2.version} appears to be a minor change")
elif patch:
diffs.append(f"{dp2.name} v{dp2.version} appears to be a patch change")

if maybe_new_obj:
diffs.append(
f"(maybe_new_obj) {dp2.name} v{dp2.version} changed name, description, and key. Potentially new object "
f"depending on context."
)

if not major:
if maybe_major:
diffs.append(
f"{dp2.name} v{dp2.version} could be a major change depending on context"
)
if maybe_minor:
diffs.append(
f"{dp2.name} v{dp2.version} could be a minor change depending on context"
)

if not any([major, minor, patch, maybe_major, maybe_minor]):
diffs.append(f"{dp2.name} v{dp2.version} appears to be a no-op change")

return diffs


def show_diffs(versions):
if len(versions) < 2:
print("Not enough versions to compare")
return

for a, b in zip(versions[:-1], versions[1:]):
diff = dp_diff(a, b)
print("\n".join(diff))
print()


def main():
pass


if __name__ == "__main__":
main()
172 changes: 172 additions & 0 deletions src/test/test_dp_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Copyright (c) 2023 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 [email protected] 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 copy import deepcopy

from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue
from ssvc.decision_points.helpers import dp_diff


class MyTestCase(unittest.TestCase):
def setUp(self) -> None:
self.dp1 = SsvcDecisionPoint(
name="Test DP",
key="test_dp",
description="This is a test decision point",
version="1.0.0",
values=[
SsvcDecisionPointValue(
name="Yes",
key="yes",
description="Yes",
),
SsvcDecisionPointValue(
name="No",
key="no",
description="No",
),
],
)
self.dp2 = deepcopy(self.dp1)

def test_maybe_new_obj(self):
# ### Create a new object when
# * A different or new concept is being represented

# if name, key, and description are the same, then it's not a new object
self.assertEqual(self.dp1.name, self.dp2.name)
self.assertEqual(self.dp1.key, self.dp2.key)
self.assertEqual(self.dp1.description, self.dp2.description)

results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertNotIn("new object", text)

# if name, key, AND description are different, then it is maybe a new object
self.dp2.name = "New Test DP"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertNotIn("new object", text)

self.dp2.key = "new_test_dp"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertNotIn("new object", text)

self.dp2.description = "This is a new test decision point"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)

# now that all three are different, it should suggest a new object
self.assertIn("new object", text)

def test_major_version(self):
# ### Increment the Major Version when
#
# * Criteria for creating a new object are not met, _AND_
# * existing values are removed, _OR_
# * value semantics change in a way that older answers are no longer usable,
# _OR_
# * new values are added that divide previous value semantics ambiguously

# remove one
self.dp2.values = self.dp2.values[:-1]
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("major", text)

# add one
self.dp2.values = list(self.dp1.values)
self.dp2.values.append(
SsvcDecisionPointValue(
name="Maybe",
key="maybe",
description="Maybe",
)
)

results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("major", text)

def test_minor_version_when_new_option_added(self):
# ### Increment the Minor Version when
# * Criteria for incrementing the Major Version are not met, _AND_
# * new options are added, _OR_
# add one
self.dp2.values = list(self.dp1.values)
self.dp2.values.append(
SsvcDecisionPointValue(
name="Maybe",
key="maybe",
description="Maybe",
)
)

results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("minor", text)

def test_minor_version_when_value_name_change(self):
# * value names or keys are changed, _OR_

self.dp2.values = deepcopy(self.dp1.values)
self.dp2.values[0].name = "New Yes"

results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("minor", text)

def test_minor_version_when_value_key_changes(self):
self.dp2.values = deepcopy(self.dp1.values)
self.dp2.values[0].key = "new_yes"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("minor", text)

def test_minor_version_when_decision_point_name_changes(self):
# * the decision point name is changed
self.dp2.name = "New Test DP"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("minor", text)

def test_patch_version_when_typo_fixed(self):
# * typo fixes in option names or decision point name, _OR_
self.dp2.name = "Test Dp"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("patch", text)

def test_patch_version_when_description_changes(self):
# ### Increment the Patch Version when
# * the decision point description changes in a way that does not affect
# semantics, _OR_

self.dp2.description = "This is a new test decision point"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("patch", text)

def test_patch_version_when_value_description_changes(self):
# * a value description changes in a way that does not affect semantics
self.dp2.values = deepcopy(self.dp1.values)
self.dp2.values[0].description = "New Yes"
results = dp_diff(self.dp1, self.dp2)
text = "\n".join(results)
self.assertIn("patch", text)


if __name__ == "__main__":
unittest.main()