Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 99 additions & 11 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,108 @@
# Code
CERTCC SSVC
===========

This directory holds helper scripts that can make managing or using SSVC easier.
This is the official Python package for the CERT/CC Stakeholder-Specific Vulnerability Categorization (SSVC) project.

## csv-to-latex
Installation
------------
You can install the latest release from PyPI:

This python script takes a CSV of the format in the `../data` directory and gets you (most of the way) to a pretty decision tree visualization. It creates a LaTeX file that can create a PDF (and from there, a PNG or whatever you want).
pip install certcc-ssvc

`python SSVC_csv-to-latex.py --help` works and should explain all your options.
When the script finishes, it will also print a message with instructions for creating the PDF or PNG from the tex. A potential future improvement is to call `latexmk` directly from the python script.
Demo to explore SSVC decision making
-----
After installation, import the package and explore the examples:

Example usage:
import ssvc

# Example decision point usage. A Weather Forecast and Humidity Value decision point
from ssvc.decision_points.example import weather
print(weather.LATEST.model_dump_json(indent=2))
from ssvc.decision_points.example import humidity
print(humidity.LATEST.model_dump_json(indent=2))


# Example decision table usage
from ssvc.decision_tables.example import to_play
print(to_play.LATEST.model_dump_json(indent=2))

#Show decision tree in ascii text art
from ssvc.decision_tables.base import ascii_tree
print(ascii_tree(to_play.LATEST))

Explanation
------

This demo is a simple decision tree that provides an Outcome based on two conditions: the weather forecast and the humidity level.

Imagine the decision tree as a series of questions. To find the outcome (the YesNo column), you start at the first question (Decision Point), which is the root node of the tree: What is the Weather Forecast?

* Step 1: Look at the Weather Forecast column (e.g., rain, overcast, sunny).
* Step 2: Look at the Humidity Value above 40% column (e.g., high, low).
* Step 3: Based on the combination of these two conditions, the YesNo column will give you the Decision as "Yes" to play and "No" to not to play.

The YesNo column is the Outcome Decision Point, and the other two Decision Points are inputs that will be collected. This decision tree looks like below in ascii form

```
python SSVC_csv-to-latex.py --input=../data/ssvc_2_deployer_simplified.csv --output=tmp.tex --delim="," --columns="0,2,1" --label="3" --header-row --priorities="defer, scheduled, out-of-cycle, immediate"
Weather Fore.. | Humidity Val.. | YesNo v1.0.0.. |
---------------------------------------------------
├── rain
│ ├── high
│ │ └── [no]
│ └── low
│ └── [no]
├── overcast
│ ├── high
│ │ └── [no]
│ └── low
│ └── [yes]
└── sunny
├── high
│ └── [no]
└── low
└── [yes]
```

Dependencies: LaTeX.
To install latex, see <https://www.latex-project.org/get/>
`latexmk` is a helper script that is not included in all distributions by default; if you need it, see <https://ctan.org/pkg/latexmk/?lang=en>
Usage
---------

For usage in vulnerability management scenarios consider the following popular SSVC decisions

import ssvc

# Example decision point usage. Exploitation as a Decision Point
from ssvc.decision_points.ssvc.exploitation import LATEST as Exploitation
print(Exploitation.model_dump_json(indent=2))
# Try a CVSS metic Attack Vector using SSVC
from ssvc.decision_points.cvss.attack_vector import LATEST as AttackVector
print(AttackVector.model_dump_json(indent=2))
from ssvc.decision_points.cisa.in_kev import LATEST as InKEV
print(InKEV.model_dump_json(indent=2))

# Example decision table for a Supplier deciding Patch Development Priority
from ssvc.decision_tables.ssvc.supplier_dt import LATEST as SupplierDT
print(SupplierDT.model_dump_json(indent=2))

# Example decision table for a Deployer decision Patch Application Priority
from ssvc.decision_tables.ssvc.deployer_dt import LATEST as DeployerDT
print(DeployerDT.model_dump_json(indent=2))

# Example CISA Decision Table as Coordinator for Vulnerability Management writ large
from ssvc.decision_tables.cisa.cisa_coordinate_dt import LATEST as CISACoordinate
print(CISACoordinate.model_dump_json(indent=2))

#Print CISA Decision Table as an ascii tree
from ssvc.decision_tables.base import ascii_tree
print(ascii_tree(CISACoordinate))


Resources
---------
Source code and full documentation:
https://github.com/CERTCC/SSVC

SSVC Policy Explorer:
https://certcc.github.io/SSVC/ssvc-explorer/

SSVC Calculator:
https://certcc.github.io/SSVC/ssvc-calc/
11 changes: 5 additions & 6 deletions src/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,17 @@ build-backend = "setuptools.build_meta"
#build-backend = "pdm.backend"

[project]
name = "ssvc"
name = "certcc-ssvc"
authors = [
{ name = "Allen D. Householder", email="[email protected]" },
{ name = "Vijay Sarvepalli", email="[email protected]"}
{ name = "CERT/CC SSVC", email="[email protected]"}
]
description = "Tools for working with a Stakeholder Specific Vulnerability Categorization (SSVC)"
readme = {file="README.md", content-type="text/markdown"}
requires-python = ">=3.12"
keywords =["ssvc","vulnerability management","vulnerability management"]
license = {file="LICENSE.md"}
license-files = ["LICENSE"]
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Topic :: Security",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down Expand Up @@ -71,7 +69,8 @@ exclude = ["test*"] # exclude packages matching these glob patterns (empty by d
[tool.setuptools_scm]
version_file = "ssvc/_version.py"
root = ".."
relative_to = "pyproject.toml"
local_scheme = "no-local-version"
version_scheme = "no-guess-dev"


#[tools.setuptools.dynamic]
Expand Down
60 changes: 60 additions & 0 deletions src/ssvc/decision_points/example/humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python
"""
Provides example decision point for humidity values
"""
# Copyright (c) 2024-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
# [email protected] for full terms.
# [DISTRIBUTION STATEMENT A] This material has been approved for
# public release and unlimited distribution. Please see Copyright notice
# for non-US Government use and distribution.
# This Software includes and/or makes use of Third-Party Software each
# subject to its own license.
# DM24-0278

from ssvc.decision_points.base import DecisionPointValue
from ssvc.decision_points.helpers import print_versions_and_diffs
from ssvc.decision_points.ssvc.base import SsvcDecisionPoint

LOW = DecisionPointValue(
name="Low",
key="L",
definition="Humidity is low, below 40%."
)

HIGH = DecisionPointValue(
name="High",
key="H",
definition="Humidity is high, above 40%"
)
HUMIDITY_1 = SsvcDecisionPoint(
name="Humidity Value above 40% ",
namespace="x_example.test#forecast",
definition="Humidity is the amount of water vapor in the air. Above 40% is High in this context.",
key="H",
version="1.0.0",
values=(
HIGH,
LOW
),
)

VERSIONS = (HUMIDITY_1,)
LATEST = VERSIONS[-1]


def main():
print_versions_and_diffs(VERSIONS)


if __name__ == "__main__":
main()
68 changes: 68 additions & 0 deletions src/ssvc/decision_points/example/weather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python
"""
Provides example decision point for weather forecast
"""
# Copyright (c) 2024-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
# [email protected] for full terms.
# [DISTRIBUTION STATEMENT A] This material has been approved for
# public release and unlimited distribution. Please see Copyright notice
# for non-US Government use and distribution.
# This Software includes and/or makes use of Third-Party Software each
# subject to its own license.
# DM24-0278

from ssvc.decision_points.base import DecisionPointValue
from ssvc.decision_points.helpers import print_versions_and_diffs
from ssvc.decision_points.ssvc.base import SsvcDecisionPoint

SUNNY = DecisionPointValue(
name="Sunny",
key="S",
definition="Weather is sunny."
)

OVERCAST = DecisionPointValue(
name="Overcast",
key="O",
definition="Weather is overcast."
)

RAIN = DecisionPointValue(
name="Rain",
key="R",
definition="Weather is rainy."
)

WEATHER_FORECAST_1 = SsvcDecisionPoint(
name="Weather Forecast",
namespace="x_example.test#forecast",
definition="Weather is the forecast that describes general weather patterns ",
key="W",
version="1.0.0",
values=(
RAIN,
OVERCAST,
SUNNY,
),
)

VERSIONS = (WEATHER_FORECAST_1,)
LATEST = VERSIONS[-1]


def main():
print_versions_and_diffs(VERSIONS)


if __name__ == "__main__":
main()
86 changes: 86 additions & 0 deletions src/ssvc/decision_tables/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,89 @@ def check_topological_order(dt: DecisionTable) -> list[dict]:
return check_topological_order(
df, target=target, target_value_order=target_value_order
)

def build_tree(df: pd.DataFrame, columns: pd.Index | list[str]) -> dict[str, dict[str, str] | list[str]] | list[str]:
"""
Recursively build a nested dict:
{feature_value: subtree_or_list_of_outcomes}

This tree should preserve the original row order of the DataFrame.
"""
# Base case: if only one column is left, it's the outcome.
if len(columns) == 1:
# Last column: return a list of outcomes.
return df[columns[0]].astype(str).tolist()

# Get the first feature column and the rest of the columns.
first, rest = columns[0], columns[1:]
tree = {}

# Iterate through the unique values of the first column in the order they appear.
# This is the key change to preserve the original CSV order.
for val in df[first].unique():
# Filter the DataFrame to get the group for the current value.
group = df[df[first] == val]
# Recursively build the subtree for this group.
tree[str(val)] = build_tree(group, rest)

return tree

def draw_tree(node: dict | list, prefix: str="", lines: list | None = None) -> list:
"""
Pretty-print nested dict/list as a tree.
"""
if lines is None:
lines = []

if isinstance(node, dict):
items = list(node.items())
for i, (k, v) in enumerate(items):
# Determine the branch characters for the tree.
branch = "└── " if i == len(items) - 1 else "├── "
lines.append(prefix + branch + k + " " * 4)

# Calculate the prefix for the next level of the tree.
next_prefix = prefix + (" " * 16 if i == len(items) - 1 else "│" + " " * 15)
# Recursively draw the subtree.
draw_tree(v, next_prefix, lines)
else: # list of outcomes
for i, leaf in enumerate(node):
# Determine the branch characters for the leaves.
branch = "└── " if i == len(node) - 1 else "├── "
lines.append(prefix + branch + f"[{leaf}]")

return lines

def ascii_tree(dt: DecisionTable, df: pd.DataFrame | None = None) -> str:
"""
Reads a Pandas data frame, builds a decision tree, and returns its ASCII representation.
"""
# Check for the optional 'row' column and drop it if it exists.
if df == None:
df = decision_table_to_longform_df(dt)

if 'row' in df.columns:
df.drop(columns='row', inplace=True)

# Separate feature columns from the outcome column.
feature_cols = list(df.columns[:-1])
outcome_col = df.columns[-1]

# Build the tree structure.
tree = build_tree(df, feature_cols + [outcome_col])
# Draw the tree into a list of strings.
lines = draw_tree(tree)

# Generate the header line.
header = ""
for item in df.columns:
if len(item) > 14:
header = header + item[0:12] + ".." + " | "
else:
header = header + item + " " * (14 - len(item)) + " | "

# Generate the separator line.
sep = "-" * len(header)

# Combine the header, separator, and tree lines into a single string.
return "\n".join([header, sep] + lines)
Loading
Loading