diff --git a/src/README.md b/src/README.md index 0e070feb..a244f011 100644 --- a/src/README.md +++ b/src/README.md @@ -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 --------- diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 752e495d..68223377 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -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: diff --git a/src/test/test_selections.py b/src/test/test_selections.py index bc6977d0..b5c532aa 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -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 @@ -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.""" @@ -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()