Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ For usage in vulnerability management scenarios consider the following popular S
from ssvc.decision_tables.helpers import ascii_tree
print(ascii_tree(CISACoordinate))

#Creating an SSVC Selection for publish/export to external providers like CSAF or CVE
from datetime import datetime, timezone
from ssvc.decision_tables.cisa.cisa_coordinate_dt import LATEST as decision_table
from ssvc import selection
namespace = "ssvc"
decision_points = ["Exploitation"]
values = [["Public PoC"]]
timestamp = datetime.now()
selections = []

for dp in decision_table.decision_points.values():
if dp.namespace == namespace and dp.name in decision_points:
dp_index = decision_points.index(dp.name)
selected = selection.Selection.from_decision_point(dp)
selected.values = tuple(selection.MinimalDecisionPointValue(key=val.key,
name=val.name) for val in dp.values if val.name in values[dp_index])
selections.append(selected)

out = selection.SelectionList(selections=selections,timestamp=timestamp)
print(out.model_dump_json(exclude_none=True, indent=4))


Resources
---------

Expand Down
20 changes: 20 additions & 0 deletions src/ssvc/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,26 @@ def model_json_schema(cls, **kwargs):
schema = strip_nullable_anyof(schema)

return order_schema(schema)
def _post_process(self, data):
"""
Ensures all Selection.values are lists and removes empty array elements.
"""
for x in list(data.keys()):
if not data[x]:
print(x)
del data[x]
return data

def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
return self._post_process(data)

def model_dump_json(self, *args, **kwargs):
import json
jsontext = super().model_dump_json(*args, **kwargs)
data = self._post_process(json.loads(jsontext))
return json.dumps(data, **{k: v for k, v in kwargs.items() if k in json.dumps.__code__.co_varnames})



def main() -> None:
Expand Down
38 changes: 38 additions & 0 deletions src/test/test_selections.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import unittest
from datetime import datetime
from unittest import expectedFailure
import json

from ssvc import selection
from ssvc.selection import MinimalDecisionPointValue, SelectionList
Expand Down Expand Up @@ -219,6 +220,32 @@ def test_reference_model(self):
self.assertIn(uri, str(ref.uri))
self.assertEqual(ref.summary, "Test description")

def test_model_dump_removes_empty_values(self):
"""model_dump() should remove None or empty values."""
result_clean = self.selections.model_dump(exclude_none=True)
result_bloat = self.selections.model_dump()
self.assertNotEqual(result_clean, result_bloat)
self.assertIn("selections", result_clean)
self.assertNotIn("metadata", result_clean)

def test_model_dump_json_respects_indent(self):
"""model_dump_json() should apply JSON indentation and pruning."""
json_text = self.selections.model_dump_json(indent=4)
data = json.loads(json_text)
self.assertIn("selections", data)
self.assertNotIn("metadata", data)
self.assertIn("\n \"selections\":", json_text)

def test_model_dump_json_excludes_none(self):
"""exclude_none=True should work with post-processing."""
json_text_clean = self.selections.model_dump_json(exclude_none=True)
json_text_bloat = self.selections.model_dump_json()
self.assertNotEqual(json_text_clean, json_text_bloat)
data = json.loads(json_text_clean)
self.assertIn("selections", data)
self.assertNotIn("metadata", data)


@expectedFailure
def test_reference_model_without_summary(self):
"""Test the Reference model."""
Expand Down Expand Up @@ -381,6 +408,17 @@ def test_selection_list_minimum_selections(self):
timestamp=datetime.now(),
)

def test_model_dump_removes_required_field(self):
""" Test if a selections is dumped and breaks when items removed """
s = SelectionList(
selections=[self.s1],
timestamp=datetime.now(),
)
dumped = s.model_dump()
with self.assertRaises(Exception):
del dumped['values']



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