From da6466d2d7a99c99e5557558a06d16248e8de4d5 Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Fri, 19 Sep 2025 12:35:25 -0400 Subject: [PATCH 01/81] Move pyproject.toml to root level dir --- .github/workflows/deploy_site.yml | 4 ++-- .github/workflows/link_checker.yml | 8 ++++---- .github/workflows/python-app.yml | 4 ++-- src/pyproject.toml => pyproject.toml | 6 +++--- requirements.txt | 2 +- src/uv.lock => uv.lock | 0 6 files changed, 12 insertions(+), 12 deletions(-) rename src/pyproject.toml => pyproject.toml (94%) rename src/uv.lock => uv.lock (100%) diff --git a/.github/workflows/deploy_site.yml b/.github/workflows/deploy_site.yml index 1b51a7f3..d8c50bff 100644 --- a/.github/workflows/deploy_site.yml +++ b/.github/workflows/deploy_site.yml @@ -43,7 +43,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install uv - uv sync --project=src --no-dev + uv sync --no-dev - name: Setup Pages uses: actions/configure-pages@v5 @@ -51,7 +51,7 @@ jobs: - name: Build Site run: | export PYTHONPATH=src:$PYTHONPATH - uv run --project=src mkdocs build --clean --config-file mkdocs.yml + uv run mkdocs build --clean --config-file mkdocs.yml - name: Upload artifact uses: actions/upload-pages-artifact@v4 diff --git a/.github/workflows/link_checker.yml b/.github/workflows/link_checker.yml index 92f87994..c9207966 100644 --- a/.github/workflows/link_checker.yml +++ b/.github/workflows/link_checker.yml @@ -12,7 +12,7 @@ on: - .github/workflows/linkchecker.yml # run on any PR that changes the pip requirements - requirements.txt - - src/pyproject.toml + - pyproject.toml # let us trigger it manually workflow_dispatch: @@ -31,13 +31,13 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip uv - uv sync --dev --project=src + uv sync --dev - name: Build Site run: | - uv run --project=src mkdocs build --verbose --clean --config-file mkdocs.yml + uv run mkdocs build --verbose --clean --config-file mkdocs.yml - name: Check links run: | - uv run --project=src linkchecker site/index.html + uv run linkchecker site/index.html diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 05f5f658..887d6bff 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -28,11 +28,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip uv - uv sync --project=src --dev --frozen + uv sync --dev --frozen # - uses: psf/black@stable - name: Test with pytest run: | - uv run --project=src pytest + uv run pytest - name: Build run: | uv build --project=src diff --git a/src/pyproject.toml b/pyproject.toml similarity index 94% rename from src/pyproject.toml rename to pyproject.toml index f32675dc..054da464 100644 --- a/src/pyproject.toml +++ b/pyproject.toml @@ -62,13 +62,13 @@ ssvc_doctools="ssvc.doctools:main" [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) -include = ["ssvc*"] # package names should match these glob patterns (["*"] by default) +include = ["src*"] # package names should match these glob patterns (["*"] by default) exclude = ["test*"] # exclude packages matching these glob patterns (empty by default) #namespaces = false # to disable scanning PEP 420 namespaces (true by default) [tool.setuptools_scm] -version_file = "ssvc/_version.py" -root = ".." +version_file = "./src/ssvc/_version.py" +root = "." local_scheme = "no-local-version" version_scheme = "no-guess-dev" diff --git a/requirements.txt b/requirements.txt index 9e875a7d..a374b6cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv export --project=src --no-editable -o ./requirements.txt +# uv export --no-editable -o ./requirements.txt . annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ diff --git a/src/uv.lock b/uv.lock similarity index 100% rename from src/uv.lock rename to uv.lock From 2849dd0d9e671d6bd1fc160f389e7689b8d00a8b Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Fri, 19 Sep 2025 12:38:47 -0400 Subject: [PATCH 02/81] Fix python build --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 887d6bff..75b2c691 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -35,7 +35,7 @@ jobs: uv run pytest - name: Build run: | - uv build --project=src + uv build - name: Upload Artifacts uses: actions/upload-artifact@v4 with: From e8cdc476b70b28146fa4f8afb4c2de4c97ac99ad Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Fri, 19 Sep 2025 16:39:17 -0400 Subject: [PATCH 03/81] Adjust Dockerfile for relocated pyproject.toml and uv.lock. I am not convinced that this works. --- docker/Dockerfile | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index df7f668b..2b9cb51d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,29 +9,23 @@ ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" RUN python -m venv "${VIRTUAL_ENV}" FROM base AS dependencies -ARG BASE_DIR=.. -ARG SRC_DIR=${BASE_DIR}/src - # Copy the files we need -COPY ${BASE_DIR}/ /app +COPY . /app # Set the environment variable ENV PYTHONPATH=/app/src -COPY ${SRC_DIR}/pyproject.toml /app/src/pyproject.toml -COPY ${SRC_DIR}/uv.lock /app/src/uv.lock # install requirements -RUN uv sync --project=/app/src --frozen - +RUN uv sync --frozen FROM dependencies AS test ENV PYTHONPATH=/app/src # Install pytest and dev dependencies -RUN uv sync --project=/app/src --frozen --dev +RUN uv sync --frozen --dev # Run the unit tests -CMD ["uv", "run", "--project=/app/src", "pytest"] +CMD ["uv", "run", "pytest"] FROM dependencies AS docs -CMD ["uv", "run", "--project=/app/src", "mkdocs", "serve", "--dev-addr", "0.0.0.0:8000"] +CMD ["uv", "run", "mkdocs", "serve", "--dev-addr", "0.0.0.0:8000"] FROM dependencies AS registry_api -CMD ["uv", "run", "--project=/app/src", "uvicorn", "ssvc.api.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uv", "run", "uvicorn", "ssvc.api.main:app", "--host", "0.0.0.0", "--port", "8000"] From 420f14320aaa04483dc6dc086c8b2bab719ca6b0 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 7 Oct 2025 15:49:53 -0400 Subject: [PATCH 04/81] Extend methods to generate Selection Schema for CVE and other projects --- src/ssvc/selection.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 752e495d..2c455e68 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -312,7 +312,51 @@ 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. + """ + def fix_selection(selection): + # Convert tuple to list and filter out empty items + values = selection.get("values", []) + # Ensure it's a list, filter out empty/falsy items + selection["values"] = [v for v in list(values) if v] + return selection + + # If this is a dict with selections, process each selection + if isinstance(data, dict) and "selections" in data: + data["selections"] = [ + fix_selection(sel) for sel in data["selections"] if sel + ] + # Remove empty array fields from the top level + keys_to_delete = [k for k, v in data.items() if isinstance(v, list) and not v] + for k in keys_to_delete: + del data[k] + 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): + # Dump to python dict first, post-process, then dump to JSON + import json + from datetime import timezone + model_dump_kwargs = kwargs.copy() + json_kwargs = {} + # List of json.dumps kwargs you want to support + json_kwarg_names = ['indent', 'sort_keys', 'separators', 'ensure_ascii'] + for key in json_kwarg_names: + if key in model_dump_kwargs: + json_kwargs[key] = model_dump_kwargs.pop(key) + # Get dict with Pydantic's processing (exclude_none, etc.) + data = super().model_dump(*args, **model_dump_kwargs) + data = self._post_process(data) + # Format timestamp as UTC RFC3339 string + if "timestamp" in data and isinstance(data["timestamp"], datetime): + utc_dt = data["timestamp"].astimezone(timezone.utc) + data["timestamp"] = utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + return json.dumps(data, **json_kwargs) def main() -> None: print( From 7131c0dbb4a91b0faff056257583f2bfa2b92ee7 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 7 Oct 2025 16:03:22 -0400 Subject: [PATCH 05/81] Added mode="json" for reliable super model Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ssvc/selection.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 2c455e68..c2a72d47 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -350,12 +350,9 @@ def model_dump_json(self, *args, **kwargs): if key in model_dump_kwargs: json_kwargs[key] = model_dump_kwargs.pop(key) # Get dict with Pydantic's processing (exclude_none, etc.) - data = super().model_dump(*args, **model_dump_kwargs) + data = super().model_dump(*args, mode="json", **model_dump_kwargs) data = self._post_process(data) - # Format timestamp as UTC RFC3339 string - if "timestamp" in data and isinstance(data["timestamp"], datetime): - utc_dt = data["timestamp"].astimezone(timezone.utc) - data["timestamp"] = utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + # No need to manually format timestamp; mode="json" handles it. return json.dumps(data, **json_kwargs) def main() -> None: From 205c508f893a6f8d6b8da7bdae0f6663de557251 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 7 Oct 2025 16:28:20 -0400 Subject: [PATCH 06/81] Revert mode=json and add timezone checks --- src/ssvc/selection.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index c2a72d47..c2458e0c 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -350,9 +350,16 @@ def model_dump_json(self, *args, **kwargs): if key in model_dump_kwargs: json_kwargs[key] = model_dump_kwargs.pop(key) # Get dict with Pydantic's processing (exclude_none, etc.) - data = super().model_dump(*args, mode="json", **model_dump_kwargs) + data = super().model_dump(*args, **model_dump_kwargs) data = self._post_process(data) - # No need to manually format timestamp; mode="json" handles it. + # Format timestamp as UTC RFC3339 string + if "timestamp" in data and isinstance(data["timestamp"], datetime): + ts = data["timestamp"] + if ts.tzinfo is None or ts.tzinfo.utcoffset(ts) is None: + utc_dt = ts.replace(tzinfo=timezone.utc) + else: + utc_dt = ts.astimezone(timezone.utc) + data["timestamp"] = utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ") return json.dumps(data, **json_kwargs) def main() -> None: From 7a9bf3b15c559f482b80254aaea0caabb7eaaf11 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 7 Oct 2025 16:46:48 -0400 Subject: [PATCH 07/81] Updated README.md for pypi project --- src/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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 --------- From c05fa152adc1d77e9713dec1c0351c48b40ffa93 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Thu, 9 Oct 2025 15:12:30 -0400 Subject: [PATCH 08/81] Updated to add tests and simplified model_dumpers --- src/ssvc/selection.py | 46 ++++++++----------------------------- src/test/test_selections.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index c2458e0c..68223377 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -316,22 +316,10 @@ def _post_process(self, data): """ Ensures all Selection.values are lists and removes empty array elements. """ - def fix_selection(selection): - # Convert tuple to list and filter out empty items - values = selection.get("values", []) - # Ensure it's a list, filter out empty/falsy items - selection["values"] = [v for v in list(values) if v] - return selection - - # If this is a dict with selections, process each selection - if isinstance(data, dict) and "selections" in data: - data["selections"] = [ - fix_selection(sel) for sel in data["selections"] if sel - ] - # Remove empty array fields from the top level - keys_to_delete = [k for k, v in data.items() if isinstance(v, list) and not v] - for k in keys_to_delete: - del data[k] + for x in list(data.keys()): + if not data[x]: + print(x) + del data[x] return data def model_dump(self, *args, **kwargs): @@ -339,28 +327,12 @@ def model_dump(self, *args, **kwargs): return self._post_process(data) def model_dump_json(self, *args, **kwargs): - # Dump to python dict first, post-process, then dump to JSON import json - from datetime import timezone - model_dump_kwargs = kwargs.copy() - json_kwargs = {} - # List of json.dumps kwargs you want to support - json_kwarg_names = ['indent', 'sort_keys', 'separators', 'ensure_ascii'] - for key in json_kwarg_names: - if key in model_dump_kwargs: - json_kwargs[key] = model_dump_kwargs.pop(key) - # Get dict with Pydantic's processing (exclude_none, etc.) - data = super().model_dump(*args, **model_dump_kwargs) - data = self._post_process(data) - # Format timestamp as UTC RFC3339 string - if "timestamp" in data and isinstance(data["timestamp"], datetime): - ts = data["timestamp"] - if ts.tzinfo is None or ts.tzinfo.utcoffset(ts) is None: - utc_dt = ts.replace(tzinfo=timezone.utc) - else: - utc_dt = ts.astimezone(timezone.utc) - data["timestamp"] = utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ") - return json.dumps(data, **json_kwargs) + 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: print( 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() From fc7b9d2cd98f429d689e887f15c05c73bb9b3c1f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Oct 2025 15:22:38 -0400 Subject: [PATCH 09/81] add examples to api --- src/ssvc/api/v1/routers/examples.py | 262 +++++++++++++++++++++++++++ src/ssvc/api/v1/routers/v1_router.py | 2 + src/ssvc/examples.py | 133 ++++++++++++++ 3 files changed, 397 insertions(+) create mode 100644 src/ssvc/api/v1/routers/examples.py create mode 100644 src/ssvc/examples.py diff --git a/src/ssvc/api/v1/routers/examples.py b/src/ssvc/api/v1/routers/examples.py new file mode 100644 index 00000000..46a47af0 --- /dev/null +++ b/src/ssvc/api/v1/routers/examples.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +""" +SSVC API v1 Examples Router +""" + +# 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 fastapi import APIRouter + +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_tables.base import DecisionTable +from ssvc.examples import ( + EXAMPLE_DECISION_POINT_1, + EXAMPLE_DECISION_TABLE, + EXAMPLE_SELECTION_1, + EXAMPLE_SELECTION_LIST, +) +from ssvc.selection import ( + MinimalDecisionPointValue, + Selection, + SelectionList, +) + +# 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 + +# 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 + +router = APIRouter(prefix="/examples", tags=["Examples"]) + +# GET to retrieve a sample object +# POST to validate an object against the pydantic model + +# TODO move these examples to a separate examples module and import them here + + +# Decision Point Values +@router.get( + "/decision-point-values", + response_model=DecisionPointValue, + response_model_exclude_none=True, + summary="Get a sample Decision Point Value", + description="Retrieve a sample Decision Point Value object.", +) +def get_example_decision_point_value() -> DecisionPointValue: + """ + Retrieve a sample Decision Point Value object. + """ + return EXAMPLE_DECISION_POINT_1.values[0] + + +@router.post( + "/decision-point-values", + response_model=DecisionPointValue, + response_model_exclude_none=True, + summary="Validate a Decision Point Value", + description="Validate a Decision Point Value object against the pydantic model.", +) +def validate_decision_point_value( + decision_point_value: DecisionPointValue, +) -> DecisionPointValue: + """ + Validate a Decision Point Value object against the pydantic model. + """ + return decision_point_value + + +# Decision Points +@router.get( + "/decision-points", + response_model=DecisionPoint, + response_model_exclude_none=True, + summary="Get a sample Decision Point", + description="Retrieve a sample Decision Point object.", +) +def get_example_decision_point() -> DecisionPoint: + """ + Retrieve a sample Decision Point object. + """ + return EXAMPLE_DECISION_POINT_1 + + +@router.post( + "/decision-points", + response_model=DecisionPoint, + response_model_exclude_none=True, + summary="Validate a Decision Point", + description="Validate a Decision Point object against the pydantic model.", +) +def validate_decision_point(decision_point: DecisionPoint) -> DecisionPoint: + """ + Validate a Decision Point object against the pydantic model. + """ + return decision_point + + +# Decision Tables +@router.get( + "/decision-tables", + response_model=DecisionTable, + response_model_exclude_none=True, + summary="Get a sample Decision Table", + description="Retrieve a sample Decision Table object.", +) +def get_example_decision_table() -> DecisionTable: + """ + Retrieve a sample Decision Table object. + """ + return EXAMPLE_DECISION_TABLE + + +@router.post( + "/decision-tables", + response_model=DecisionTable, + response_model_exclude_none=True, + summary="Validate a Decision Table", + description="Validate a Decision Table object against the pydantic model.", +) +def validate_decision_table(decision_table: DecisionTable) -> DecisionTable: + """ + Validate a Decision Table object against the pydantic model. + """ + return decision_table + + +# minimal decision point values +@router.get( + "/decision-point-values-minimal", + response_model=MinimalDecisionPointValue, + response_model_exclude_none=True, + summary="Get a minimal Decision Point Value", + description="Retrieve a minimal Decision Point Value object.", +) +def get_minimal_decision_point_value() -> MinimalDecisionPointValue: + """ + Retrieve a minimal Decision Point Value object. + """ + return MinimalDecisionPointValue(key="KEY1") + + +@router.post( + "/decision-point-values-minimal", + response_model=MinimalDecisionPointValue, + response_model_exclude_none=True, + summary="Validate a minimal Decision Point Value", + description="Validate a minimal Decision Point Value object against the pydantic model.", +) +def validate_minimal_decision_point_value( + minimal_decision_point_value: MinimalDecisionPointValue, +) -> MinimalDecisionPointValue: + """ + Validate a minimal Decision Point Value object against the pydantic model. + """ + return minimal_decision_point_value + + +# selection +@router.get( + "/selections", + response_model=Selection, + response_model_exclude_none=True, + summary="Get a sample Selection", + description="Retrieve a sample Selection object.", +) +def get_example_selection() -> Selection: + """ + Retrieve a sample Selection object. + """ + return EXAMPLE_SELECTION_1 + + +@router.post( + "/selections", + response_model=Selection, + response_model_exclude_none=True, + summary="Validate a Selection", + description="Validate a Selection object against the pydantic model.", +) +def validate_selection(selection: Selection) -> Selection: + """ + Validate a Selection object against the pydantic model. + """ + return selection + + +# Selection lists +@router.get( + "/selection-lists", + response_model=SelectionList, + response_model_exclude_none=True, + summary="Get a sample Selection List", + description="Retrieve a sample Selection List object.", +) +def get_example_selection_list() -> SelectionList: + """ + Retrieve a sample Selection List object. + """ + return EXAMPLE_SELECTION_LIST + + +@router.post( + "/selection-lists", + response_model=SelectionList, + response_model_exclude_none=True, + summary="Validate a Selection List", + description="Validate a Selection List object against the pydantic model.", +) +def validate_selection_list(selection_list: SelectionList) -> SelectionList: + """ + Validate a Selection List object against the pydantic model. + """ + return selection_list diff --git a/src/ssvc/api/v1/routers/v1_router.py b/src/ssvc/api/v1/routers/v1_router.py index 9031079f..4aea1346 100644 --- a/src/ssvc/api/v1/routers/v1_router.py +++ b/src/ssvc/api/v1/routers/v1_router.py @@ -26,6 +26,7 @@ decision_point, decision_table, decision_tables, + examples, objects, ) from ssvc.api.v1.routers import ( @@ -37,6 +38,7 @@ ) router_v1 = APIRouter(prefix="/v1", tags=["v1"]) +router_v1.include_router(examples.router) router_v1.include_router(decision_point.router) router_v1.include_router(decision_points.router) router_v1.include_router(decision_table.router) diff --git a/src/ssvc/examples.py b/src/ssvc/examples.py new file mode 100644 index 00000000..c5807cf3 --- /dev/null +++ b/src/ssvc/examples.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +""" +Example SSVC object instances +""" + +# 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 datetime + +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_tables.base import DecisionTable +from ssvc.selection import Reference, Selection, SelectionList + +EXAMPLE_DECISION_POINT_1 = DecisionPoint( + namespace="example", + key="KEY1", + version="1.0.0", + name="Example Decision Point", + definition="This is a sample decision point for demonstration purposes. Values must be an ordered list.", + values=( + DecisionPointValue( + key="V1", + name="Value One", + definition="Value One definition.", + ), + DecisionPointValue( + key="V2", + name="Value Two", + definition="Value Two definition.", + ), + ), + registered=False, +) + +EXAMPLE_DECISION_POINT_2 = DecisionPoint( + namespace="example", + key="KEY2", + version="1.0.0", + name="Example Decision Point 2", + definition="This is another sample decision point for demonstration purposes. Values must be an ordered list.", + values=( + DecisionPointValue( + key="A", + name="Value A", + definition="Value A definition.", + ), + DecisionPointValue( + key="B", + name="Value B", + definition="Value B definition.", + ), + ), + registered=False, +) +EXAMPLE_OUTCOME_DECISION_POINT = DecisionPoint( + namespace="example", + key="OUTCOME", + version="1.0.0", + name="Example Outcome Decision Point", + definition="This is a sample outcome decision point for demonstration purposes. Values must be an ordered list.", + values=( + DecisionPointValue( + key="O1", + name="Outcome One", + definition="Outcome One definition.", + ), + DecisionPointValue( + key="O2", + name="Outcome Two", + definition="Outcome Two definition.", + ), + DecisionPointValue( + key="O3", + name="Outcome Three", + definition="Outcome Three definition.", + ), + ), + registered=False, +) + +EXAMPLE_DECISION_TABLE = DecisionTable( + namespace="example", + key="DT1", + version="1.0.0", + name="Example Decision Table", + definition="This is a sample decision table for demonstration purposes.", + decision_points={ + dp.id: dp + for dp in [ + EXAMPLE_DECISION_POINT_1, + EXAMPLE_DECISION_POINT_2, + EXAMPLE_OUTCOME_DECISION_POINT, + ] + }, + outcome=EXAMPLE_OUTCOME_DECISION_POINT.id, + registered=False, +) +EXAMPLE_SELECTION_1 = Selection.from_decision_point( + decision_point=EXAMPLE_DECISION_POINT_1 +) +EXAMPLE_SELECTION_1.values = [ + EXAMPLE_SELECTION_1.values[0], +] + + +EXAMPLE_SELECTION_2 = Selection.from_decision_point( + decision_point=EXAMPLE_DECISION_POINT_2 +) +EXAMPLE_SELECTION_LIST = SelectionList( + target_ids=["VU#9999999", "CVE-1900-0001"], + selections=[EXAMPLE_SELECTION_1, EXAMPLE_SELECTION_2], + timestamp=datetime.datetime.now(tz=datetime.timezone.utc), + references=[ + Reference(uri="https://example.com", summary="Example reference"), + ], +) From 87224c2cb2fa5391a4ee178a04c6e776000b134c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Oct 2025 15:50:39 -0400 Subject: [PATCH 10/81] add EXAMPLE_MINIMAL_DECISION_POINT_VALUE to examples --- src/ssvc/api/v1/routers/examples.py | 43 ++--------------------------- src/ssvc/examples.py | 11 +++++++- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/src/ssvc/api/v1/routers/examples.py b/src/ssvc/api/v1/routers/examples.py index 46a47af0..b3cbc964 100644 --- a/src/ssvc/api/v1/routers/examples.py +++ b/src/ssvc/api/v1/routers/examples.py @@ -29,6 +29,7 @@ from ssvc.examples import ( EXAMPLE_DECISION_POINT_1, EXAMPLE_DECISION_TABLE, + EXAMPLE_MINIMAL_DECISION_POINT_VALUE, EXAMPLE_SELECTION_1, EXAMPLE_SELECTION_LIST, ) @@ -38,51 +39,11 @@ SelectionList, ) -# 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 - -# 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 - router = APIRouter(prefix="/examples", tags=["Examples"]) # GET to retrieve a sample object # POST to validate an object against the pydantic model -# TODO move these examples to a separate examples module and import them here - # Decision Point Values @router.get( @@ -185,7 +146,7 @@ def get_minimal_decision_point_value() -> MinimalDecisionPointValue: """ Retrieve a minimal Decision Point Value object. """ - return MinimalDecisionPointValue(key="KEY1") + return EXAMPLE_MINIMAL_DECISION_POINT_VALUE @router.post( diff --git a/src/ssvc/examples.py b/src/ssvc/examples.py index c5807cf3..1f4939a6 100644 --- a/src/ssvc/examples.py +++ b/src/ssvc/examples.py @@ -26,7 +26,12 @@ from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_tables.base import DecisionTable -from ssvc.selection import Reference, Selection, SelectionList +from ssvc.selection import ( + MinimalDecisionPointValue, + Reference, + Selection, + SelectionList, +) EXAMPLE_DECISION_POINT_1 = DecisionPoint( namespace="example", @@ -131,3 +136,7 @@ Reference(uri="https://example.com", summary="Example reference"), ], ) + +EXAMPLE_MINIMAL_DECISION_POINT_VALUE = MinimalDecisionPointValue( + key="KEY_REQUIRED", +) From 7ecedd318641012d9ba3f520d1c3571f22cb7383 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Oct 2025 16:31:49 -0400 Subject: [PATCH 11/81] add test for examples in app --- src/test/api/test_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/api/test_main.py b/src/test/api/test_main.py index be81a600..c5d0546b 100644 --- a/src/test/api/test_main.py +++ b/src/test/api/test_main.py @@ -42,6 +42,7 @@ def test_expected_routers(self): "namespaces", "keys", "versions", + "examples", ] routes = [r.path for r in app.routes] for expected in expected_routers: From c20e4a2cf33cb98857eba32a9328d5d6f874ffdc Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Oct 2025 16:32:08 -0400 Subject: [PATCH 12/81] add endpoint to retrieve sample references and corresponding tests --- src/ssvc/api/v1/routers/examples.py | 16 ++++++ src/test/api/routers/test_examples.py | 83 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/test/api/routers/test_examples.py diff --git a/src/ssvc/api/v1/routers/examples.py b/src/ssvc/api/v1/routers/examples.py index b3cbc964..f34f543f 100644 --- a/src/ssvc/api/v1/routers/examples.py +++ b/src/ssvc/api/v1/routers/examples.py @@ -35,6 +35,7 @@ ) from ssvc.selection import ( MinimalDecisionPointValue, + Reference, Selection, SelectionList, ) @@ -221,3 +222,18 @@ def validate_selection_list(selection_list: SelectionList) -> SelectionList: Validate a Selection List object against the pydantic model. """ return selection_list + + +# references +@router.get( + "/references", + response_model=Reference, + response_model_exclude_none=True, + summary="Get sample References", + description="Retrieve a list of sample Reference URIs.", +) +def get_example_references() -> Reference: + """ + Retrieve a list of sample Reference URIs. + """ + return EXAMPLE_SELECTION_LIST.references[0] diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py new file mode 100644 index 00000000..9da5bd8e --- /dev/null +++ b/src/test/api/routers/test_examples.py @@ -0,0 +1,83 @@ +# 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 unittest + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ssvc.api.v1.routers import examples +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_tables.base import DecisionTable +from ssvc.selection import ( + MinimalDecisionPointValue, + Reference, + Selection, + SelectionList, +) + + +class MyTestCase(unittest.TestCase): + def setUp(self): + self.app = FastAPI() + self.app.include_router(examples.router) + self.client = TestClient(self.app) + + def tearDown(self): + pass + + def _test_get_example(self, endpoint: str, model: type): + response = self.client.get(endpoint) + self.assertEqual(response.status_code, 200) + data = response.json() + + try: + model.model_validate(data) + except Exception as e: + self.fail(f"Validation failed: {e}") + + def test_get_decision_point_values(self): + self._test_get_example( + "/examples/decision-point-values", DecisionPointValue + ) + + def test_get_decision_points(self): + self._test_get_example("/examples/decision-points", DecisionPoint) + + def test_get_decision_tables(self): + self._test_get_example("/examples/decision-tables", DecisionTable) + + def test_get_minimal_decision_point_values(self): + self._test_get_example( + "/examples/decision-point-values-minimal", + MinimalDecisionPointValue, + ) + + def test_get_selections(self): + self._test_get_example("/examples/selections", Selection) + + def test_get_selection_lists(self): + self._test_get_example("/examples/selection-lists", SelectionList) + + def test_get_references(self): + self._test_get_example("/examples/references", Reference) + + +if __name__ == "__main__": + unittest.main() From 8e59824d251d7a0cdc9f94089d196f451cf4032c Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 9 Oct 2025 16:36:11 -0400 Subject: [PATCH 13/81] mark test_get_selection_lists as expected failure and add TODO for POST tests --- src/test/api/routers/test_examples.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py index 9da5bd8e..b9d3eef5 100644 --- a/src/test/api/routers/test_examples.py +++ b/src/test/api/routers/test_examples.py @@ -72,12 +72,16 @@ def test_get_minimal_decision_point_values(self): def test_get_selections(self): self._test_get_example("/examples/selections", Selection) + # FIXME: currently fails due to an issue with the example data containing empty lists + @unittest.expectedFailure def test_get_selection_lists(self): self._test_get_example("/examples/selection-lists", SelectionList) def test_get_references(self): self._test_get_example("/examples/references", Reference) + # TODO write POST tests + if __name__ == "__main__": unittest.main() From 17b6845725334a031e46f496904e0d8b61a03222 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 11:09:48 -0400 Subject: [PATCH 14/81] add POST endpoint to validate Reference and corresponding tests --- src/ssvc/api/v1/routers/examples.py | 13 ++++ src/test/api/routers/test_examples.py | 100 +++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/ssvc/api/v1/routers/examples.py b/src/ssvc/api/v1/routers/examples.py index f34f543f..f990a88b 100644 --- a/src/ssvc/api/v1/routers/examples.py +++ b/src/ssvc/api/v1/routers/examples.py @@ -237,3 +237,16 @@ def get_example_references() -> Reference: Retrieve a list of sample Reference URIs. """ return EXAMPLE_SELECTION_LIST.references[0] + +@router.post( + "/references", + response_model=Reference, + response_model_exclude_none=True, + summary="Validate a Reference", + description="Validate a Reference object against the pydantic model.", +) +def validate_reference(reference: Reference) -> Reference: + """ + Validate a Reference object against the pydantic model. + """ + return reference \ No newline at end of file diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py index b9d3eef5..bfa51452 100644 --- a/src/test/api/routers/test_examples.py +++ b/src/test/api/routers/test_examples.py @@ -16,7 +16,8 @@ # This Software includes and/or makes use of Third-Party Software each # subject to its own license. # DM24-0278 - +import datetime +import random import unittest from fastapi import FastAPI @@ -25,6 +26,7 @@ from ssvc.api.v1.routers import examples from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_tables.base import DecisionTable +from ssvc.decision_tables.example.to_play import TOPLAY_1 from ssvc.selection import ( MinimalDecisionPointValue, Reference, @@ -39,6 +41,9 @@ def setUp(self): self.app.include_router(examples.router) self.client = TestClient(self.app) + # set up a decision table from which we can derive other objects in post-tests + self.dt = TOPLAY_1 + def tearDown(self): pass @@ -52,6 +57,19 @@ def _test_get_example(self, endpoint: str, model: type): except Exception as e: self.fail(f"Validation failed: {e}") + + def _test_post_example(self, endpoint: str, model: type, obj: object): + response = self.client.post(endpoint, json=obj.model_dump(mode="json")) + self.assertEqual(200, response.status_code, f"POST to {endpoint} failed: {response}") + data = response.json() + + try: + model.model_validate(data) + except Exception as e: + self.fail(f"Validation failed: {e}") + + + def test_get_decision_point_values(self): self._test_get_example( "/examples/decision-point-values", DecisionPointValue @@ -82,6 +100,86 @@ def test_get_references(self): # TODO write POST tests + def test_post_decision_points(self): + for dp in self.dt.decision_points.values(): + self._test_post_example( + "/examples/decision-points", DecisionPoint, dp + ) + # TODO test bad data + + def test_post_decision_point_values(self): + for dp in self.dt.decision_points.values(): + for dpv in dp.values: + self._test_post_example( + "/examples/decision-point-values", DecisionPointValue, dpv + ) + # TODO test bad data + + def test_post_decision_tables(self): + self._test_post_example( + "/examples/decision-tables", DecisionTable, self.dt + ) + # TODO test bad data + + def test_post_minimal_decision_point_values(self): + for dp in self.dt.decision_points.values(): + for dpv in dp.values: + mdpv = MinimalDecisionPointValue( + key=dpv.key, + ) + self._test_post_example( + "/examples/decision-point-values-minimal", + MinimalDecisionPointValue, + mdpv, + ) + # TODO test bad data + + def test_post_selections(self): + for dp in self.dt.decision_points.values(): + sel = Selection.from_decision_point(dp) + # randomly sample 1 or more values from the selection + sample_size = random.randint(1, len(sel.values)) + sel.values = random.sample(sel.values, sample_size) + self._test_post_example("/examples/selections", Selection, sel) + + # TODO test bad data + + # FIXME: currently fails due to an issue with the example data containing empty lists. (see #1002) + @unittest.expectedFailure + def test_post_selection_lists(self): + sels = [] + for dp in self.dt.decision_points.values(): + sel = Selection.from_decision_point(dp) + # randomly sample 1 or more values from the selection + sample_size = random.randint(1, len(sel.values)) + sel.values = random.sample(sel.values, sample_size) + sels.append(sel) + + sel_list = SelectionList( + target_ids=["TK-421","TK-710"], + selections=sels, + timestamp=datetime.datetime.now(), + references = [ + Reference( + uri="https://starwars.fandom.com/wiki/TK-421", + summary="Alongside TK-710, TK-421's first security assignment was to guard the Millennium Falcon in Docking Bay 327 after the Death Star captured it." + ) + + ] + ) + print("---") + print(sel_list.model_dump_json()) + print("---") + self._test_post_example( + "/examples/selection-lists", SelectionList, sel_list + ) + # TODO test bad data + + def test_post_references(self): + ref = Reference(uri="http://some/reference",summary="An example reference") + self._test_post_example("/examples/references", Reference, ref) + # TODO test bad data + if __name__ == "__main__": unittest.main() From bb414163c6d1576fe0175c55a15aa366e6f59ac6 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 11:36:57 -0400 Subject: [PATCH 15/81] remove debug print statement from data cleanup function --- src/ssvc/selection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 68223377..9e9e602c 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -318,7 +318,6 @@ def _post_process(self, data): """ for x in list(data.keys()): if not data[x]: - print(x) del data[x] return data From 029975360d902b3ac978607f9e1f16dd7b82d2c0 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Fri, 10 Oct 2025 12:15:31 -0400 Subject: [PATCH 16/81] Removed debug print statement --- src/ssvc/selection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 68223377..9e9e602c 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -318,7 +318,6 @@ def _post_process(self, data): """ for x in list(data.keys()): if not data[x]: - print(x) del data[x] return data From 650e391d561a171ebe67a79992d781a64c1cfb26 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:13:25 -0400 Subject: [PATCH 17/81] add tests for SelectionList to validate empty lists and model dumping behavior --- src/ssvc/selection.py | 32 +++++------------------- src/test/test_selections.py | 50 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 9e9e602c..64db706d 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -24,14 +24,7 @@ from datetime import datetime from typing import ClassVar, Literal, Optional -from pydantic import ( - AnyUrl, - BaseModel, - ConfigDict, - Field, - field_validator, - model_validator, -) +from pydantic import (AnyUrl, BaseModel, ConfigDict, Field, field_validator, model_serializer, model_validator) from ssvc._mixins import ( _GenericOptionalSsvcObject, @@ -238,6 +231,11 @@ class SelectionList(_SchemaVersioned, _Timestamped, BaseModel): ], ) + @model_serializer(mode="wrap") + def remove_falsy_fields(self, handler): + data = handler(self) + return {k: v for k, v in data.items() if v} + @model_validator(mode="before") def set_schema_version(cls, data): if "schemaVersion" not in data: @@ -312,24 +310,6 @@ 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]: - 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}) diff --git a/src/test/test_selections.py b/src/test/test_selections.py index b5c532aa..bff4ff2a 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -17,10 +17,10 @@ # subject to its own license. # DM24-0278 +import json import unittest from datetime import datetime from unittest import expectedFailure -import json from ssvc import selection from ssvc.selection import MinimalDecisionPointValue, SelectionList @@ -366,6 +366,54 @@ def test_selection_list_optional_fields(self): self.assertEqual(len(sel_list.references), 1) self.assertEqual(sel_list.decision_point_resources[0].uri, ref.uri) + def test_missing_lists_are_empty_after_init(self): + # if decision_point_resources is not included, it should still validate. + sel_list_no_dpr = SelectionList( + selections=[self.s1, self.s2], + timestamp=datetime.now(), + ) + for attribute in ["decision_point_resources", "references", "target_ids"]: + self.assertTrue( + hasattr(sel_list_no_dpr, attribute), + f"Attribute {attribute} is missing", + ) + _value = getattr(sel_list_no_dpr, attribute) + self.assertIsInstance(_value, list) + self.assertEqual(0,len(_value)) + + # but they should not appear in the model dump to JSON + dumped = sel_list_no_dpr.model_dump() + self.assertNotIn(attribute, dumped) + + def test_validation_when_empty_lists_provided(self): + sel_list_no_dpr = SelectionList( + selections=[self.s1, self.s2], + timestamp=datetime.now(), + ) + json_data = sel_list_no_dpr.model_dump_json(exclude_none=True) + + data = json.loads(json_data) + + check_attrs = [ + "decision_point_resources", + "references", + "target_ids",] + + for attr in check_attrs: + self.assertNotIn(attr, data) + + new_obj = SelectionList.model_validate(data) + + for attr in check_attrs: + self.assertTrue( + hasattr(new_obj, attr), + f"Attribute {attr} is missing after re-validation", + ) + _value = getattr(new_obj, attr) + self.assertIsInstance(_value, list) + self.assertEqual(0,len(_value)) + + def test_model_json_schema_customization(self): """Test that JSON schema is properly customized.""" schema = SelectionList.model_json_schema() From 054261fa03494bd846b4e7da9c20908fab720726 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:14:00 -0400 Subject: [PATCH 18/81] refactor examples and tests for clarity and structure --- src/ssvc/examples.py | 3 +-- src/test/api/routers/test_examples.py | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/ssvc/examples.py b/src/ssvc/examples.py index 1f4939a6..f96497fa 100644 --- a/src/ssvc/examples.py +++ b/src/ssvc/examples.py @@ -28,8 +28,7 @@ from ssvc.decision_tables.base import DecisionTable from ssvc.selection import ( MinimalDecisionPointValue, - Reference, - Selection, + Reference, Selection, SelectionList, ) diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py index bfa51452..3bd83188 100644 --- a/src/test/api/routers/test_examples.py +++ b/src/test/api/routers/test_examples.py @@ -52,6 +52,8 @@ def _test_get_example(self, endpoint: str, model: type): self.assertEqual(response.status_code, 200) data = response.json() + self.assertIsInstance(data, dict) + try: model.model_validate(data) except Exception as e: @@ -90,16 +92,12 @@ def test_get_minimal_decision_point_values(self): def test_get_selections(self): self._test_get_example("/examples/selections", Selection) - # FIXME: currently fails due to an issue with the example data containing empty lists - @unittest.expectedFailure def test_get_selection_lists(self): self._test_get_example("/examples/selection-lists", SelectionList) def test_get_references(self): self._test_get_example("/examples/references", Reference) - # TODO write POST tests - def test_post_decision_points(self): for dp in self.dt.decision_points.values(): self._test_post_example( @@ -144,8 +142,6 @@ def test_post_selections(self): # TODO test bad data - # FIXME: currently fails due to an issue with the example data containing empty lists. (see #1002) - @unittest.expectedFailure def test_post_selection_lists(self): sels = [] for dp in self.dt.decision_points.values(): From e2a67bc97750a987a87dcfb2ea453274682ec5d3 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:22:48 -0400 Subject: [PATCH 19/81] add model validators to handle default summary and remove falsy fields --- src/ssvc/selection.py | 22 ++++++++++++++++++++++ src/test/test_selections.py | 12 ++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 64db706d..046a6dc8 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -133,6 +133,28 @@ class Reference(BaseModel): uri: AnyUrl summary: str + @model_serializer(mode="wrap") + def remove_falsy_fields(self, handler): + data = handler(self) + return {k: v for k, v in data.items() if v} + + @model_validator(mode="before") + @classmethod + def set_default_summary(cls, data): + """ + Ensure that summary is set to an empty string if not provided. + + Args: + data: The input data dictionary. + + Returns: + The modified data dictionary with summary set to an empty string if it was missing. + + """ + if "summary" not in data or not data["summary"]: + data["summary"] = "" + return data + # override schema generation to ensure that description is not required def model_json_schema(cls, **kwargs): schema = super().model_json_schema(**kwargs) diff --git a/src/test/test_selections.py b/src/test/test_selections.py index bff4ff2a..30d46f24 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -20,7 +20,6 @@ import json import unittest from datetime import datetime -from unittest import expectedFailure from ssvc import selection from ssvc.selection import MinimalDecisionPointValue, SelectionList @@ -246,7 +245,6 @@ def test_model_dump_json_excludes_none(self): self.assertNotIn("metadata", data) - @expectedFailure def test_reference_model_without_summary(self): """Test the Reference model.""" uris = [ @@ -275,6 +273,16 @@ def test_reference_model_without_summary(self): self.assertIn(uri, str(ref.uri)) + # while ref might have an empty string summary, + self.assertTrue(hasattr(ref, "summary")) + self.assertEqual("", ref.summary) + # the json export should not include it + json_data = ref.model_dump_json(exclude_none=True) + data = json.loads(json_data) + self.assertIn("uri", data) + self.assertNotIn("summary", data) + + def test_selection_list_validators(self): """Test SelectionList validators.""" # Test schema version is set automatically From 20405dc0945f3cf7415f08e353a33f05b9b95f6d Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:45:49 -0400 Subject: [PATCH 20/81] add prefix and tags to API router and enhance root redirect description --- src/ssvc/api/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ssvc/api/main.py b/src/ssvc/api/main.py index 9b4f58fc..bd68419b 100644 --- a/src/ssvc/api/main.py +++ b/src/ssvc/api/main.py @@ -3,7 +3,6 @@ API for SSVC """ - # Copyright (c) 2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE # ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. @@ -45,12 +44,14 @@ }, ) -app.include_router(router_v1) +app.include_router(router_v1, prefix="/ssvc/api/v1", tags=["SSVC API v1"]) + # root should redirect to docs # at least until we have something better to show -@app.get("/", include_in_schema=False) +@app.get("/", include_in_schema=False, + description="Redirect to API docs") async def redirect_root_to_docs(): return RedirectResponse(url="/docs") From 87041a6a604ddf34967126652bdc3d3ec4abe958 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:46:08 -0400 Subject: [PATCH 21/81] remove prefix and tags from v1 API router configuration --- src/ssvc/api/v1/routers/v1_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssvc/api/v1/routers/v1_router.py b/src/ssvc/api/v1/routers/v1_router.py index 4aea1346..52422c05 100644 --- a/src/ssvc/api/v1/routers/v1_router.py +++ b/src/ssvc/api/v1/routers/v1_router.py @@ -37,7 +37,7 @@ versions, ) -router_v1 = APIRouter(prefix="/v1", tags=["v1"]) +router_v1 = APIRouter() router_v1.include_router(examples.router) router_v1.include_router(decision_point.router) router_v1.include_router(decision_points.router) From 2a88e43a93669ec190562fb1e0cf747849b7a682 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 13:46:19 -0400 Subject: [PATCH 22/81] add README.md for SSVC API setup and usage instructions --- src/ssvc/api/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/ssvc/api/README.md diff --git a/src/ssvc/api/README.md b/src/ssvc/api/README.md new file mode 100644 index 00000000..33417d04 --- /dev/null +++ b/src/ssvc/api/README.md @@ -0,0 +1,56 @@ +# SSVC API Readme + +This directory contains source code for the SSVC API. + +## Prerequisites + +- `uv` CLI tool installed. You can install it via pip: + +```shell +pip install uv +``` + +We recommend using `uv` to manage your Python environment and dependencies, +so you don't need to manually create and activate virtual environments or +worry about Python versions. + +## Running a local instance in development mode + +From the project root, run: + +```shell +uv --project=src run uvicorn ssvc.api.main:app --reload --port=7777 +``` + +> [!TIP] +> Adjust the port as needed. + +> [!NOTE] +> We're planning to move our `pyproject.toml` to the top level of the project, +> so in the future you may be able to run this command without the `--project` flag. + +This will start the FastAPI server with auto-reload enabled, allowing you to +see changes immediately. + +## Running a local instance in production mode + +From the project root, run: + +```shell +cd docker +docker-compose up api +``` + +This will start the FastAPI server in a Docker container. + +> [!NOTE] +> Docker and Docker Compose must be installed on your machine to use this method. +> Make sure to adjust the `docker-compose.yml` file if you want to change +> the port or other settings. + +> [!TIP] +> The `api` docker target copies the code into the container at build time. +> If you make changes to the code, you'll need to rebuild the Docker image +> using `docker-compose build api` before restarting the container. Or else +> use `docker-compose up --build api` to build and start in one command. + From f44a2f3e8096394ebf49fd2ee5665e09966a9d50 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 14:00:03 -0400 Subject: [PATCH 23/81] Update src/test/api/routers/test_examples.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/test/api/routers/test_examples.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py index 3bd83188..794cf348 100644 --- a/src/test/api/routers/test_examples.py +++ b/src/test/api/routers/test_examples.py @@ -163,9 +163,6 @@ def test_post_selection_lists(self): ] ) - print("---") - print(sel_list.model_dump_json()) - print("---") self._test_post_example( "/examples/selection-lists", SelectionList, sel_list ) From 83161ad183ffea72bc1280addba037a6075188f7 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 14:00:52 -0400 Subject: [PATCH 24/81] black formatter --- src/ssvc/api/main.py | 4 +--- src/ssvc/api/v1/routers/examples.py | 3 ++- src/ssvc/examples.py | 3 ++- src/ssvc/selection.py | 11 +++++++++-- src/test/api/routers/test_examples.py | 20 ++++++++++---------- src/test/test_selections.py | 25 +++++++++++++------------ 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/ssvc/api/main.py b/src/ssvc/api/main.py index bd68419b..05a592e7 100644 --- a/src/ssvc/api/main.py +++ b/src/ssvc/api/main.py @@ -47,11 +47,9 @@ app.include_router(router_v1, prefix="/ssvc/api/v1", tags=["SSVC API v1"]) - # root should redirect to docs # at least until we have something better to show -@app.get("/", include_in_schema=False, - description="Redirect to API docs") +@app.get("/", include_in_schema=False, description="Redirect to API docs") async def redirect_root_to_docs(): return RedirectResponse(url="/docs") diff --git a/src/ssvc/api/v1/routers/examples.py b/src/ssvc/api/v1/routers/examples.py index f990a88b..090ba715 100644 --- a/src/ssvc/api/v1/routers/examples.py +++ b/src/ssvc/api/v1/routers/examples.py @@ -238,6 +238,7 @@ def get_example_references() -> Reference: """ return EXAMPLE_SELECTION_LIST.references[0] + @router.post( "/references", response_model=Reference, @@ -249,4 +250,4 @@ def validate_reference(reference: Reference) -> Reference: """ Validate a Reference object against the pydantic model. """ - return reference \ No newline at end of file + return reference diff --git a/src/ssvc/examples.py b/src/ssvc/examples.py index f96497fa..1f4939a6 100644 --- a/src/ssvc/examples.py +++ b/src/ssvc/examples.py @@ -28,7 +28,8 @@ from ssvc.decision_tables.base import DecisionTable from ssvc.selection import ( MinimalDecisionPointValue, - Reference, Selection, + Reference, + Selection, SelectionList, ) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 046a6dc8..89e20c93 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -24,7 +24,15 @@ from datetime import datetime from typing import ClassVar, Literal, Optional -from pydantic import (AnyUrl, BaseModel, ConfigDict, Field, field_validator, model_serializer, model_validator) +from pydantic import ( + AnyUrl, + BaseModel, + ConfigDict, + Field, + field_validator, + model_serializer, + model_validator, +) from ssvc._mixins import ( _GenericOptionalSsvcObject, @@ -334,7 +342,6 @@ def model_json_schema(cls, **kwargs): return order_schema(schema) - def main() -> None: print( "Please use doctools.py for schema generation and unit tests for verification" diff --git a/src/test/api/routers/test_examples.py b/src/test/api/routers/test_examples.py index 3bd83188..4542c3da 100644 --- a/src/test/api/routers/test_examples.py +++ b/src/test/api/routers/test_examples.py @@ -59,10 +59,11 @@ def _test_get_example(self, endpoint: str, model: type): except Exception as e: self.fail(f"Validation failed: {e}") - def _test_post_example(self, endpoint: str, model: type, obj: object): response = self.client.post(endpoint, json=obj.model_dump(mode="json")) - self.assertEqual(200, response.status_code, f"POST to {endpoint} failed: {response}") + self.assertEqual( + 200, response.status_code, f"POST to {endpoint} failed: {response}" + ) data = response.json() try: @@ -70,8 +71,6 @@ def _test_post_example(self, endpoint: str, model: type, obj: object): except Exception as e: self.fail(f"Validation failed: {e}") - - def test_get_decision_point_values(self): self._test_get_example( "/examples/decision-point-values", DecisionPointValue @@ -152,16 +151,15 @@ def test_post_selection_lists(self): sels.append(sel) sel_list = SelectionList( - target_ids=["TK-421","TK-710"], + target_ids=["TK-421", "TK-710"], selections=sels, timestamp=datetime.datetime.now(), - references = [ + references=[ Reference( uri="https://starwars.fandom.com/wiki/TK-421", - summary="Alongside TK-710, TK-421's first security assignment was to guard the Millennium Falcon in Docking Bay 327 after the Death Star captured it." + summary="Alongside TK-710, TK-421's first security assignment was to guard the Millennium Falcon in Docking Bay 327 after the Death Star captured it.", ) - - ] + ], ) print("---") print(sel_list.model_dump_json()) @@ -172,7 +170,9 @@ def test_post_selection_lists(self): # TODO test bad data def test_post_references(self): - ref = Reference(uri="http://some/reference",summary="An example reference") + ref = Reference( + uri="http://some/reference", summary="An example reference" + ) self._test_post_example("/examples/references", Reference, ref) # TODO test bad data diff --git a/src/test/test_selections.py b/src/test/test_selections.py index 30d46f24..5d1391c0 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -233,7 +233,7 @@ def test_model_dump_json_respects_indent(self): data = json.loads(json_text) self.assertIn("selections", data) self.assertNotIn("metadata", data) - self.assertIn("\n \"selections\":", json_text) + self.assertIn('\n "selections":', json_text) def test_model_dump_json_excludes_none(self): """exclude_none=True should work with post-processing.""" @@ -244,7 +244,6 @@ def test_model_dump_json_excludes_none(self): self.assertIn("selections", data) self.assertNotIn("metadata", data) - def test_reference_model_without_summary(self): """Test the Reference model.""" uris = [ @@ -282,7 +281,6 @@ def test_reference_model_without_summary(self): self.assertIn("uri", data) self.assertNotIn("summary", data) - def test_selection_list_validators(self): """Test SelectionList validators.""" # Test schema version is set automatically @@ -380,14 +378,18 @@ def test_missing_lists_are_empty_after_init(self): selections=[self.s1, self.s2], timestamp=datetime.now(), ) - for attribute in ["decision_point_resources", "references", "target_ids"]: + for attribute in [ + "decision_point_resources", + "references", + "target_ids", + ]: self.assertTrue( hasattr(sel_list_no_dpr, attribute), f"Attribute {attribute} is missing", ) _value = getattr(sel_list_no_dpr, attribute) self.assertIsInstance(_value, list) - self.assertEqual(0,len(_value)) + self.assertEqual(0, len(_value)) # but they should not appear in the model dump to JSON dumped = sel_list_no_dpr.model_dump() @@ -405,7 +407,8 @@ def test_validation_when_empty_lists_provided(self): check_attrs = [ "decision_point_resources", "references", - "target_ids",] + "target_ids", + ] for attr in check_attrs: self.assertNotIn(attr, data) @@ -419,8 +422,7 @@ def test_validation_when_empty_lists_provided(self): ) _value = getattr(new_obj, attr) self.assertIsInstance(_value, list) - self.assertEqual(0,len(_value)) - + self.assertEqual(0, len(_value)) def test_model_json_schema_customization(self): """Test that JSON schema is properly customized.""" @@ -465,15 +467,14 @@ def test_selection_list_minimum_selections(self): ) def test_model_dump_removes_required_field(self): - """ Test if a selections is dumped and breaks when items removed """ + """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'] - + del dumped["values"] if __name__ == "__main__": From 96b7b53beacab986a087dab8967cc738966e722f Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 14:30:07 -0400 Subject: [PATCH 25/81] Update src/ssvc/selection.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ssvc/selection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ssvc/selection.py b/src/ssvc/selection.py index 89e20c93..e2ac2e5f 100644 --- a/src/ssvc/selection.py +++ b/src/ssvc/selection.py @@ -159,7 +159,9 @@ def set_default_summary(cls, data): The modified data dictionary with summary set to an empty string if it was missing. """ - if "summary" not in data or not data["summary"]: + if "summary" not in data: + data["summary"] = "" + elif not data["summary"]: data["summary"] = "" return data From d29d624e671ff4cd689b53f76fb7b08cdde25111 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 14:31:21 -0400 Subject: [PATCH 26/81] Update src/test/test_selections.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/test/test_selections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/test_selections.py b/src/test/test_selections.py index 5d1391c0..0908cae0 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -233,7 +233,7 @@ def test_model_dump_json_respects_indent(self): data = json.loads(json_text) self.assertIn("selections", data) self.assertNotIn("metadata", data) - self.assertIn('\n "selections":', json_text) + self.assertIn("\n \"selections\":", json_text) def test_model_dump_json_excludes_none(self): """exclude_none=True should work with post-processing.""" From 695779beb8a774fc5d0f5609087a8c9fe238b976 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 14:32:53 -0400 Subject: [PATCH 27/81] Update src/test/test_selections.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/test/test_selections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/test_selections.py b/src/test/test_selections.py index 0908cae0..f5d2e5c0 100644 --- a/src/test/test_selections.py +++ b/src/test/test_selections.py @@ -474,7 +474,7 @@ def test_model_dump_removes_required_field(self): ) dumped = s.model_dump() with self.assertRaises(Exception): - del dumped["values"] + del dumped['values'] if __name__ == "__main__": From 55fab92b06ca6f6ed056937aa107623881854a56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:42:38 +0000 Subject: [PATCH 28/81] Initial plan From 4f6bc3d77b042d2124d9d83183be69d1c5a6df30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:45:08 +0000 Subject: [PATCH 29/81] Initial plan From 85c34d6fcd8c41b7e08c9575d28a3bb79502c459 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:48:43 +0000 Subject: [PATCH 30/81] Add GitHub Copilot instructions for SSVC repository Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- .github/copilot-instructions.md | 203 ++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..bc63f977 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,203 @@ +# GitHub Copilot Instructions for SSVC + +This repository contains the **Stakeholder-Specific Vulnerability Categorization (SSVC)** project, which provides a system for prioritizing actions during vulnerability management. + +## Project Overview + +SSVC is a modular decision-making framework for vulnerability management that includes: +- Python modules for decision points, decision tables, and outcomes +- MkDocs-based documentation website +- Interactive calculators and policy explorers +- JSON/CSV data files for decision tables +- Docker-based development and deployment + +## Technology Stack + +- **Primary Language**: Python 3.x +- **Package Management**: uv (package and project manager) +- **Documentation**: MkDocs with Material theme +- **Testing**: pytest +- **Data Models**: Pydantic for JSON schema validation +- **Scientific Computing**: NumPy, SciPy, scikit-learn +- **Web Framework**: FastAPI (for API endpoints) +- **Containerization**: Docker and Docker Compose + +## Project Structure + +- `/src/ssvc/` - Core Python modules for SSVC functionality +- `/docs/` - Markdown documentation source files +- `/data/` - JSON and CSV data files for decision tables +- `/test/` - Unit tests (located in `/src/test/`) +- `/docker/` - Docker configurations +- `/obsolete/` - Deprecated code (do not modify) + +## Development Workflow + +### Getting Started + +1. **Set up development environment**: `make dev` or `uv sync --dev --project src` +2. **Run local documentation server**: `make docs_local` or `make docs` (Docker) +3. **Run tests**: `make test` (local) or `make docker_test` (Docker) +4. **Run API locally**: `make api_dev` + +### Building and Testing + +- Always run tests before committing: `make test` or `uv run --project src pytest -v` +- Run markdown linting with auto-fix: `make mdlint_fix` +- Preview documentation changes: `make docs_local` +- Build documentation in Docker: `make docs` + +### Common Commands + +```bash +# Show available make targets +make help + +# Set up dev environment +make dev + +# Run tests locally +make test + +# Run tests in Docker +make docker_test + +# Serve documentation locally +make docs_local + +# Regenerate JSON data files +make regenerate_json +``` + +## Coding Conventions + +### Python Code + +- Follow PEP 8 style guidelines +- Use type hints for function signatures +- Use Pydantic models for data validation +- Document classes and functions with docstrings +- Prefer explicit imports over wildcard imports +- Module structure uses absolute imports from `ssvc` package + +### File Organization + +- Decision points are in `/src/ssvc/decision_points/` +- Decision tables are in `/src/ssvc/decision_tables/` +- Tests mirror the source structure in `/src/test/` +- Documentation pages are in `/docs/` + +### Naming Conventions + +- Python files: `snake_case.py` +- Classes: `PascalCase` +- Functions/variables: `snake_case` +- Constants: `UPPER_SNAKE_CASE` +- JSON/CSV files: `kebab-case.json` + +## Testing Requirements + +### Test Structure + +- Unit tests use pytest framework +- Tests are located in `/src/test/` +- Test files follow pattern: `test_*.py` +- Run tests with: `make test` or `uv run --project src pytest -v` + +### Test Coverage + +- Write tests for new Python modules +- Ensure decision points and tables have corresponding tests +- Test JSON schema validation +- Validate data model serialization/deserialization + +### Before Committing + +1. Run all tests: `make test` +2. Ensure no test failures +3. Fix any linting issues: `make mdlint_fix` +4. Verify documentation builds: `make docs_local` + +## Documentation + +### Writing Documentation + +- Documentation uses MkDocs with Material theme +- Files are in Markdown format in `/docs/` +- Use Python exec blocks for dynamic content generation +- Include examples and code snippets +- Follow existing documentation structure + +### Documentation Features + +- Automatic API documentation via mkdocstrings +- Code execution in Markdown via markdown-exec +- BibTeX citations via mkdocs-bibtex +- Include markdown files with mkdocs-include-markdown-plugin + +### Building Documentation + +```bash +# Local preview (with hot reload) +make docs_local + +# Docker build (production-like) +make docs + +# Access at: http://localhost:8000/SSVC/ +``` + +## Data Files + +### JSON Files + +- Located in `/data/json/` +- Generated from Python Pydantic models +- Use JSON schema validation +- Regenerate with: `make regenerate_json` + +### CSV Files + +- Located in `/data/csv/` +- Generated from Python modules +- Define decision table outcomes +- Primary way to customize SSVC for specific environments + +## Common Pitfalls + +1. **Import Paths**: Use absolute imports like `from ssvc.module import Class`, not relative imports +2. **PYTHONPATH**: When running scripts directly, set `export PYTHONPATH=$PYTHONPATH:$(pwd)/src` +3. **JSON Regeneration**: After modifying decision points/tables, regenerate JSON with `make regenerate_json` +4. **Docker Context**: Some make targets use Docker, others run locally - check the Makefile +5. **Package Management**: Use `uv` commands with `--project src` flag, not pip directly +6. **Obsolete Code**: Never modify files in `/obsolete/` directory + +## API Development + +- FastAPI application is in `/src/ssvc/api/` +- Run locally with auto-reload: `make api_dev` +- Run in Docker: `make api` +- API documentation available at `/docs` endpoint when running + +## Git Workflow + +- Create feature branches for new work +- Write descriptive commit messages +- Reference issue numbers in commits when applicable +- Keep commits focused and atomic +- Run tests before pushing + +## Additional Resources + +- Main documentation: https://certcc.github.io/SSVC/ +- Source repository: https://github.com/CERTCC/SSVC +- SSVC Calculator: https://certcc.github.io/SSVC/ssvc-calc/ +- Contributing guide: See CONTRIBUTING.md +- Project wiki: https://github.com/CERTCC/SSVC/wiki + +## Special Notes + +- This project uses Carnegie Mellon University licensing (see LICENSE file) +- Decision points and tables follow SSVC specification +- Backward compatibility is important for existing data files +- Documentation changes should be reflected in both `/docs/` and `/src/README.md` when applicable From cd1fa69ef88d456e2c51e618959ecc28a9600979 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:49:49 +0000 Subject: [PATCH 31/81] Refactor Default Automatable Values into includable file Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- docs/_includes/default_automatable_values.md | 5 +++++ docs/howto/bootstrap/collect.md | 6 +----- docs/howto/gathering_info/automatable.md | 2 ++ docs/reference/decision_points/automatable.md | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 docs/_includes/default_automatable_values.md diff --git a/docs/_includes/default_automatable_values.md b/docs/_includes/default_automatable_values.md new file mode 100644 index 00000000..c2b6f0bc --- /dev/null +++ b/docs/_includes/default_automatable_values.md @@ -0,0 +1,5 @@ +!!! tip "Default Automatable Values" + + If nothing is known about [*Automatable*](../reference/decision_points/automatable.md), the safer answer to assume is [*yes*](../reference/decision_points/automatable.md). + [*Value Density*](../reference/decision_points/value_density.md) should always be answerable; if the product is uncommon, it is probably + [*diffuse*](../reference/decision_points/value_density.md). diff --git a/docs/howto/bootstrap/collect.md b/docs/howto/bootstrap/collect.md index cc28d073..075d764d 100644 --- a/docs/howto/bootstrap/collect.md +++ b/docs/howto/bootstrap/collect.md @@ -105,11 +105,7 @@ we can suggest something like defaults for some decision points. means they do not know where the devices are or how they are controlled, so they should assume [*System Exposure*](../../reference/decision_points/system_exposure.md) is [*open*](../../reference/decision_points/system_exposure.md). -!!! tip "Default Automatable Values" - - If nothing is known about [*Automatable*](../../reference/decision_points/automatable.md), the safer answer to assume is [*yes*](../../reference/decision_points/automatable.md). - [*Value Density*](../../reference/decision_points/value_density.md) should always be answerable; if the product is uncommon, it is probably - [*diffuse*](../../reference/decision_points/value_density.md). +{% include-markdown "../../_includes/default_automatable_values.md" %} !!! tip "Default Safety Values" diff --git a/docs/howto/gathering_info/automatable.md b/docs/howto/gathering_info/automatable.md index d8ba917c..aa5a1c10 100644 --- a/docs/howto/gathering_info/automatable.md +++ b/docs/howto/gathering_info/automatable.md @@ -20,3 +20,5 @@ Liveness of Internet-connected services means quite a few overlapping things [@b For most vulnerabilities, an open port does not automatically mean that reconnaissance, weaponization, and delivery are automatable. Furthermore, discovery of a vulnerable service is not automatable in a situation where only two hosts are misconfigured to expose the service out of 2 million hosts that are properly configured. As discussed in in [Reasoning Steps Forward](../../topics/scope.md), the analyst should consider *credible* effects based on *known* use cases of the software system to be pragmatic about scope and providing values to decision points. + +{% include-markdown "../../_includes/default_automatable_values.md" %} diff --git a/docs/reference/decision_points/automatable.md b/docs/reference/decision_points/automatable.md index c20ce7c1..1a2b527f 100644 --- a/docs/reference/decision_points/automatable.md +++ b/docs/reference/decision_points/automatable.md @@ -11,6 +11,8 @@ print(example_block(LATEST)) See this [HowTo](../../howto/gathering_info/automatable.md) for advice on gathering information about the Automatable decision point. +{% include-markdown "../../_includes/default_automatable_values.md" %} + !!! tip "See also" Automatable combines with [Value Density](./value_density.md) to inform From 3e32f225875c3d02177d7f1c03d091c4722540ad Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Fri, 10 Oct 2025 15:52:55 -0400 Subject: [PATCH 32/81] fix clipping --- docs/ssvc-explorer/simple.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/ssvc-explorer/simple.js b/docs/ssvc-explorer/simple.js index 58228f6e..408ef3a2 100644 --- a/docs/ssvc-explorer/simple.js +++ b/docs/ssvc-explorer/simple.js @@ -89,9 +89,9 @@ const graphModule = (function() { height = 800 - margin.top - margin.bottom if(showFullTree) { var add_offset = 0 - if(raw.length > 60 ) - add_offset = (raw.length - 60)*5 - height = 1300 - margin.top - margin.bottom + add_offset + if(raw.length > 60) + add_offset = (raw.length - 60)*10 + height = Math.max(1300, raw.length * 20) - margin.top - margin.bottom + add_offset } duration = 750 tree = d3.layout.tree() @@ -309,6 +309,10 @@ const graphModule = (function() { var yOffset = 90; var xOffset = -xMin + yOffset; + var newHeight = xMax - xMin + 2 * yOffset; + if (newHeight > parseInt($('svg.mgraph').attr("height"))) { + $('svg.mgraph').attr("height", newHeight); + } svg.attr("transform", "translate(" + 100 + "," + xOffset + ")"); } function check_children(d,a,b) { From 243b44aff88caca68b99b0471c0b89e233f07c0a Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Fri, 10 Oct 2025 16:20:36 -0400 Subject: [PATCH 33/81] Update docs/_includes/default_automatable_values.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/_includes/default_automatable_values.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_includes/default_automatable_values.md b/docs/_includes/default_automatable_values.md index c2b6f0bc..3f18db82 100644 --- a/docs/_includes/default_automatable_values.md +++ b/docs/_includes/default_automatable_values.md @@ -1,5 +1,5 @@ !!! tip "Default Automatable Values" - If nothing is known about [*Automatable*](../reference/decision_points/automatable.md), the safer answer to assume is [*yes*](../reference/decision_points/automatable.md). - [*Value Density*](../reference/decision_points/value_density.md) should always be answerable; if the product is uncommon, it is probably - [*diffuse*](../reference/decision_points/value_density.md). + If nothing is known about [*Automatable*](/reference/decision_points/automatable.md), the safer answer to assume is [*yes*](/reference/decision_points/automatable.md). + [*Value Density*](/reference/decision_points/value_density.md) should always be answerable; if the product is uncommon, it is probably + [*diffuse*](/reference/decision_points/value_density.md). From c5724f70a8e8e94bd7e426ff71fe22a85b3750fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:25:46 +0000 Subject: [PATCH 34/81] Initial plan From cbebd9ec5e3a91842a7141b7fecb7399b5ad797e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:31:13 +0000 Subject: [PATCH 35/81] Initial plan From 26c56967691ba79615115958b2b7da73c401fb3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:31:24 +0000 Subject: [PATCH 36/81] Refactor default exploitation values to includable file Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- docs/_includes/default_exploitation_values.md | 4 ++++ docs/howto/bootstrap/collect.md | 5 +---- docs/howto/gathering_info/exploitation.md | 2 ++ docs/reference/decision_points/exploitation.md | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 docs/_includes/default_exploitation_values.md diff --git a/docs/_includes/default_exploitation_values.md b/docs/_includes/default_exploitation_values.md new file mode 100644 index 00000000..cdecbd66 --- /dev/null +++ b/docs/_includes/default_exploitation_values.md @@ -0,0 +1,4 @@ +!!! tip "Default Exploitation Values" + + [*Exploitation*](../reference/decision_points/exploitation.md) needs no special default; if adequate searches are made for exploit code and none is + found, the answer is [*none*](../reference/decision_points/exploitation.md). diff --git a/docs/howto/bootstrap/collect.md b/docs/howto/bootstrap/collect.md index cc28d073..f7cb6be2 100644 --- a/docs/howto/bootstrap/collect.md +++ b/docs/howto/bootstrap/collect.md @@ -94,10 +94,7 @@ deployer may want to use that information to favor the latter. In the case where no information is available or the organization has not yet matured its initial situational analysis, we can suggest something like defaults for some decision points. -!!! tip "Default Exploitation Values" - - [*Exploitation*](../../reference/decision_points/exploitation.md) needs no special default; if adequate searches are made for exploit code and none is - found, the answer is [*none*](../../reference/decision_points/exploitation.md). +{% include-markdown "../../_includes/default_exploitation_values.md" %} !!! tip "Default System Exposure Values" diff --git a/docs/howto/gathering_info/exploitation.md b/docs/howto/gathering_info/exploitation.md index 9b391a52..7196f0f5 100644 --- a/docs/howto/gathering_info/exploitation.md +++ b/docs/howto/gathering_info/exploitation.md @@ -7,6 +7,8 @@ from ssvc.doc_helpers import example_block print(example_block(LATEST)) ``` +{% include-markdown "../../_includes/default_exploitation_values.md" %} + ## Public PoC [Historical Analysis of Exploit Availability Timelines](https://dl.acm.org/doi/10.5555/3485754.3485760) presents a method for searching the GitHub repositories of open-source exploit databases. This method could be employed to gather information about whether *PoC* is true. diff --git a/docs/reference/decision_points/exploitation.md b/docs/reference/decision_points/exploitation.md index d2c0be4d..793a0232 100644 --- a/docs/reference/decision_points/exploitation.md +++ b/docs/reference/decision_points/exploitation.md @@ -11,6 +11,8 @@ print(example_block(LATEST)) See this [HowTo](../../howto/gathering_info/exploitation.md) for advice on gathering information about the Exploitation decision point. +{% include-markdown "../../_includes/default_exploitation_values.md" %} + The intent of this measure is the present state of exploitation of the vulnerability. The intent is not to predict future exploitation but only to acknowledge the current state of affairs. Predictive systems, such as EPSS, could be used to augment this decision or to notify stakeholders of likely changes [@jacobs2021epss]. ## CWE-IDs for *PoC* From eab18e4150b9fe93e24b5f118f58b44f91a1b52d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:31:41 +0000 Subject: [PATCH 37/81] Initial plan From 6ea27263b3f58306d7c3db2dcc44a66de96bb718 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:32:10 +0000 Subject: [PATCH 38/81] Initial plan From c8141657cc1dade4de62b4b887300964df0550fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:39:23 +0000 Subject: [PATCH 39/81] Refactor Default System Exposure Values into an includable Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- docs/_includes/default_system_exposure_values.md | 5 +++++ docs/howto/bootstrap/collect.md | 6 +----- docs/howto/gathering_info/system_exposure.md | 2 ++ docs/reference/decision_points/system_exposure.md | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 docs/_includes/default_system_exposure_values.md diff --git a/docs/_includes/default_system_exposure_values.md b/docs/_includes/default_system_exposure_values.md new file mode 100644 index 00000000..248419c7 --- /dev/null +++ b/docs/_includes/default_system_exposure_values.md @@ -0,0 +1,5 @@ +!!! tip "Default System Exposure Values" + + If the deployer does not know their exposure, that + means they do not know where the devices are or how they are controlled, so they should assume + [*System Exposure*](../reference/decision_points/system_exposure.md) is [*open*](../reference/decision_points/system_exposure.md). diff --git a/docs/howto/bootstrap/collect.md b/docs/howto/bootstrap/collect.md index cc28d073..ed84ddbc 100644 --- a/docs/howto/bootstrap/collect.md +++ b/docs/howto/bootstrap/collect.md @@ -99,11 +99,7 @@ we can suggest something like defaults for some decision points. [*Exploitation*](../../reference/decision_points/exploitation.md) needs no special default; if adequate searches are made for exploit code and none is found, the answer is [*none*](../../reference/decision_points/exploitation.md). -!!! tip "Default System Exposure Values" - - If the deployer does not know their exposure, that - means they do not know where the devices are or how they are controlled, so they should assume - [*System Exposure*](../../reference/decision_points/system_exposure.md) is [*open*](../../reference/decision_points/system_exposure.md). +{% include-markdown "../../_includes/default_system_exposure_values.md" %} !!! tip "Default Automatable Values" diff --git a/docs/howto/gathering_info/system_exposure.md b/docs/howto/gathering_info/system_exposure.md index baf7a76c..ebc8f935 100644 --- a/docs/howto/gathering_info/system_exposure.md +++ b/docs/howto/gathering_info/system_exposure.md @@ -7,6 +7,8 @@ from ssvc.doc_helpers import example_block print(example_block(LATEST)) ``` +{% include-markdown "../../_includes/default_system_exposure_values.md" %} + *System Exposure* is primarily used by [Deployers](../../deployer_tree), so the question is about whether some specific system is in fact exposed, not a hypothetical or aggregate question about systems of that type. Therefore, it generally has a concrete answer, even though it may vary from vulnerable component to vulnerable component, based on their respective configurations. diff --git a/docs/reference/decision_points/system_exposure.md b/docs/reference/decision_points/system_exposure.md index 32742d87..7fcd75d2 100644 --- a/docs/reference/decision_points/system_exposure.md +++ b/docs/reference/decision_points/system_exposure.md @@ -11,6 +11,8 @@ print(example_block(LATEST)) See this [HowTo](../../howto/gathering_info/system_exposure.md) for advice on gathering information about the System Exposure decision point. +{% include-markdown "../../_includes/default_system_exposure_values.md" %} + Measuring the attack surface precisely is difficult, and we do not propose to perfectly delineate between small and controlled access. Exposure should be judged against the system in its deployed context, which may differ from how it is commonly expected to be deployed. For example, the exposure of a device on a vehicle's CAN bus will vary depending on the presence of a cellular telemetry device on the same bus. From 0234feffe1a30493b833063115140953004c782f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:39:46 +0000 Subject: [PATCH 40/81] Refactor default mission impact values into reusable include file Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- docs/_includes/default_mission_impact_values.md | 5 +++++ docs/howto/bootstrap/collect.md | 6 +----- docs/howto/gathering_info/mission_impact.md | 2 ++ docs/reference/decision_points/mission_impact.md | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 docs/_includes/default_mission_impact_values.md diff --git a/docs/_includes/default_mission_impact_values.md b/docs/_includes/default_mission_impact_values.md new file mode 100644 index 00000000..75fbba1b --- /dev/null +++ b/docs/_includes/default_mission_impact_values.md @@ -0,0 +1,5 @@ +!!! tip "Default Mission Impact Values" + + Similarly, with [*Mission Impact*](/reference/decision_points/mission_impact.md), the deployer should assume that the software is in use at the + organization for a reason, and that it supports essential functions unless they have evidence otherwise. + With a total lack of information, assume [*support crippled*](/reference/decision_points/mission_impact.md) as a default. diff --git a/docs/howto/bootstrap/collect.md b/docs/howto/bootstrap/collect.md index cc28d073..bdd8a10e 100644 --- a/docs/howto/bootstrap/collect.md +++ b/docs/howto/bootstrap/collect.md @@ -118,11 +118,7 @@ we can suggest something like defaults for some decision points. This position is conservative, but software is thoroughly embedded in daily life now, so we suggest that the decision maker provide evidence that no one’s well-being will suffer. -!!! tip "Default Mission Impact Values" - - Similarly, with [*Mission Impact*](../../reference/decision_points/mission_impact.md), the deployer should assume that the software is in use at the - organization for a reason, and that it supports essential functions unless they have evidence otherwise. - With a total lack of information, assume [*support crippled*](../../reference/decision_points/mission_impact.md) as a default. +{% include-markdown "../../_includes/default_mission_impact_values.md" %} !!! example "Using Defaults" diff --git a/docs/howto/gathering_info/mission_impact.md b/docs/howto/gathering_info/mission_impact.md index 13936a51..1a70b5dd 100644 --- a/docs/howto/gathering_info/mission_impact.md +++ b/docs/howto/gathering_info/mission_impact.md @@ -12,3 +12,5 @@ At a minimum, understanding mission impact should include gathering information There are various sources of guidance on how to gather this information; see for example the FEMA guidance in [Continuity Directive 2](https://www.fema.gov/sites/default/files/2020-07/Federal_Continuity_Directive-2_June132017.pdf) or [OCTAVE FORTE](https://insights.sei.cmu.edu/insider-threat/2018/06/octave-forte-and-fair-connect-cyber-risk-practitioners-with-the-boardroom.html). This is part of risk management more broadly. It should require the vulnerability management team to interact with more senior management to understand mission priorities and other aspects of risk mitigation. + +{% include-markdown "../../_includes/default_mission_impact_values.md" %} diff --git a/docs/reference/decision_points/mission_impact.md b/docs/reference/decision_points/mission_impact.md index 85b234e4..a480c300 100644 --- a/docs/reference/decision_points/mission_impact.md +++ b/docs/reference/decision_points/mission_impact.md @@ -11,6 +11,8 @@ print(example_block(LATEST)) See this [HowTo](../../howto/gathering_info/mission_impact.md) for advice on gathering information about the Mission Impact decision point. +{% include-markdown "../../_includes/default_mission_impact_values.md" %} + !!! tip "See also" Mission Impact combines with [Safety Impact](./safety_impact.md) to inform From f9314339f6ea694090617231cf33b6d91e86ef42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:43:58 +0000 Subject: [PATCH 41/81] Refactor default safety values tip into includable file Co-authored-by: ahouseholder <2594236+ahouseholder@users.noreply.github.com> --- docs/_includes/default_safety_values.md | 6 ++++++ docs/howto/bootstrap/collect.md | 7 +------ docs/reference/decision_points/safety_impact.md | 4 ++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 docs/_includes/default_safety_values.md diff --git a/docs/_includes/default_safety_values.md b/docs/_includes/default_safety_values.md new file mode 100644 index 00000000..1e1404ca --- /dev/null +++ b/docs/_includes/default_safety_values.md @@ -0,0 +1,6 @@ +!!! tip "Default Safety Values" + + If the decision maker knows nothing about the environment in which the device is used, we suggest assuming a + [*marginal*](../reference/decision_points/safety_impact.md) [*Safety Impact*](../reference/decision_points/safety_impact.md). + This position is conservative, but software is thoroughly embedded in daily life now, so we suggest that the decision + maker provide evidence that no one's well-being will suffer. diff --git a/docs/howto/bootstrap/collect.md b/docs/howto/bootstrap/collect.md index cc28d073..9691ce95 100644 --- a/docs/howto/bootstrap/collect.md +++ b/docs/howto/bootstrap/collect.md @@ -111,12 +111,7 @@ we can suggest something like defaults for some decision points. [*Value Density*](../../reference/decision_points/value_density.md) should always be answerable; if the product is uncommon, it is probably [*diffuse*](../../reference/decision_points/value_density.md). -!!! tip "Default Safety Values" - - If the decision maker knows nothing about the environment in which the device is used, we suggest assuming a - [*marginal*](../../reference/decision_points/safety_impact.md) [*Safety Impact*](../../reference/decision_points/safety_impact.md). - This position is conservative, but software is thoroughly embedded in daily life now, so we suggest that the decision - maker provide evidence that no one’s well-being will suffer. +{% include-markdown "../../_includes/default_safety_values.md" %} !!! tip "Default Mission Impact Values" diff --git a/docs/reference/decision_points/safety_impact.md b/docs/reference/decision_points/safety_impact.md index 128275ba..047abd05 100644 --- a/docs/reference/decision_points/safety_impact.md +++ b/docs/reference/decision_points/safety_impact.md @@ -14,6 +14,8 @@ print(example_block(LATEST)) - Safety Impact combines with [Mission Impact](./mission_impact.md) to inform [Human Impact](./human_impact.md). +{% include-markdown "../../_includes/default_safety_values.md" %} + We take an expansive view of safety, in which a safety violation is a violation of what the United States [Centers for Disease Control (CDC)](https://www.cdc.gov/hrqol/wellbeing.htm) calls **well-being**. Physical well-being violations are common safety violations, but we also consider economic, social, emotional, and psychological well-being to be important. Weighing fine differences among these categories is probably not possible, so we will not try. Each decision option lists examples of the effects that qualify for that value/answer in the various types of violations of well-being. These examples should not be considered comprehensive or exhaustive, but rather as suggestive.