Skip to content

Commit 48645cf

Browse files
authored
Collect SSVC trees (#2050)
* Collect SSVC trees Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Add pipeline to collect SSVC Trees Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> --------- Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent be89117 commit 48645cf

File tree

11 files changed

+623
-245
lines changed

11 files changed

+623
-245
lines changed

vulnerabilities/api_v2.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,31 @@ class AdvisoryV2Serializer(serializers.ModelSerializer):
146146
references = AdvisoryReferenceSerializer(many=True)
147147
severities = AdvisorySeveritySerializer(many=True)
148148
advisory_id = serializers.CharField(source="avid", read_only=True)
149+
related_ssvc_trees = serializers.SerializerMethodField()
150+
151+
def get_related_ssvc_trees(self, obj):
152+
related_ssvcs = obj.related_ssvcs.all().select_related("source_advisory")
153+
source_ssvcs = obj.source_ssvcs.all().select_related("source_advisory")
154+
155+
seen = set()
156+
result = []
157+
158+
for ssvc in list(related_ssvcs) + list(source_ssvcs):
159+
key = (ssvc.vector, ssvc.source_advisory_id)
160+
if key in seen:
161+
continue
162+
seen.add(key)
163+
164+
result.append(
165+
{
166+
"vector": ssvc.vector,
167+
"decision": ssvc.decision,
168+
"options": ssvc.options,
169+
"source_url": ssvc.source_advisory.url,
170+
}
171+
)
172+
173+
return result
149174

150175
class Meta:
151176
model = AdvisoryV2
@@ -160,6 +185,7 @@ class Meta:
160185
"exploitability",
161186
"weighted_severity",
162187
"risk_score",
188+
"related_ssvc_trees",
163189
]
164190

165191
def get_aliases(self, obj):

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from vulnerabilities.pipelines import flag_ghost_packages
2020
from vulnerabilities.pipelines import populate_vulnerability_summary_pipeline
2121
from vulnerabilities.pipelines import remove_duplicate_advisories
22+
from vulnerabilities.pipelines.v2_improvers import collect_ssvc_trees
2223
from vulnerabilities.pipelines.v2_improvers import compute_advisory_todo as compute_advisory_todo_v2
2324
from vulnerabilities.pipelines.v2_improvers import compute_package_risk as compute_package_risk_v2
2425
from vulnerabilities.pipelines.v2_improvers import (
@@ -70,5 +71,6 @@
7071
compute_advisory_todo_v2.ComputeToDo,
7172
unfurl_version_range_v2.UnfurlVersionRangePipeline,
7273
compute_advisory_todo.ComputeToDo,
74+
collect_ssvc_trees.CollectSSVCPipeline,
7375
]
7476
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 4.2.25 on 2025-12-15 15:15
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("vulnerabilities", "0103_codecommit_impactedpackage_affecting_commits_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="SSVC",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"vector",
25+
models.CharField(
26+
help_text="The vector string representing the SSVC.", max_length=255
27+
),
28+
),
29+
(
30+
"options",
31+
models.JSONField(help_text="A JSON object containing the SSVC options."),
32+
),
33+
(
34+
"decision",
35+
models.CharField(help_text="The decision string for the SSVC.", max_length=255),
36+
),
37+
(
38+
"related_advisories",
39+
models.ManyToManyField(
40+
help_text="Advisories associated with this SSVC.",
41+
related_name="related_ssvcs",
42+
to="vulnerabilities.advisoryv2",
43+
),
44+
),
45+
(
46+
"source_advisory",
47+
models.ForeignKey(
48+
help_text="The advisory that was used to generate this SSVC decision.",
49+
on_delete=django.db.models.deletion.CASCADE,
50+
related_name="source_ssvcs",
51+
to="vulnerabilities.advisoryv2",
52+
),
53+
),
54+
],
55+
options={
56+
"unique_together": {("vector", "source_advisory")},
57+
},
58+
),
59+
]

vulnerabilities/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3414,3 +3414,26 @@ class CodeCommit(models.Model):
34143414

34153415
class Meta:
34163416
unique_together = ("commit_hash", "vcs_url")
3417+
3418+
3419+
class SSVC(models.Model):
3420+
vector = models.CharField(max_length=255, help_text="The vector string representing the SSVC.")
3421+
options = models.JSONField(help_text="A JSON object containing the SSVC options.")
3422+
decision = models.CharField(max_length=255, help_text="The decision string for the SSVC.")
3423+
related_advisories = models.ManyToManyField(
3424+
AdvisoryV2,
3425+
related_name="related_ssvcs",
3426+
help_text="Advisories associated with this SSVC.",
3427+
)
3428+
source_advisory = models.ForeignKey(
3429+
AdvisoryV2,
3430+
on_delete=models.CASCADE,
3431+
related_name="source_ssvcs",
3432+
help_text="The advisory that was used to generate this SSVC decision.",
3433+
)
3434+
3435+
def __str__(self):
3436+
return f"SSVC Decision: {self.vector} -> {self.decision}"
3437+
3438+
class Meta:
3439+
unique_together = ("vector", "source_advisory")

vulnerabilities/pipelines/v2_importers/vulnrichment_importer.py

Lines changed: 1 addition & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from vulnerabilities.utils import get_advisory_url
1717
from vulnerabilities.utils import get_cwe_id
1818
from vulnerabilities.utils import get_reference_id
19+
from vulnerabilities.utils import ssvc_calculator
1920

2021
logger = logging.getLogger(__name__)
2122

@@ -210,117 +211,3 @@ def clean_downloads(self):
210211

211212
def on_failure(self):
212213
self.clean_downloads()
213-
214-
215-
def ssvc_calculator(ssvc_data):
216-
"""
217-
Return the ssvc vector and the decision value
218-
"""
219-
options = ssvc_data.get("options", [])
220-
timestamp = ssvc_data.get("timestamp")
221-
222-
# Extract the options into a dictionary
223-
options_dict = {k: v.lower() for option in options for k, v in option.items()}
224-
225-
# We copied the table value from this link.
226-
# https://www.cisa.gov/sites/default/files/publications/cisa-ssvc-guide%20508c.pdf
227-
228-
# Determining Mission and Well-Being Impact Value
229-
mission_well_being_table = {
230-
# (Mission Prevalence, Public Well-being Impact) : "Mission & Well-being"
231-
("minimal", "minimal"): "low",
232-
("minimal", "material"): "medium",
233-
("minimal", "irreversible"): "high",
234-
("support", "minimal"): "medium",
235-
("support", "material"): "medium",
236-
("support", "irreversible"): "high",
237-
("essential", "minimal"): "high",
238-
("essential", "material"): "high",
239-
("essential", "irreversible"): "high",
240-
}
241-
242-
if "Mission Prevalence" not in options_dict:
243-
options_dict["Mission Prevalence"] = "minimal"
244-
245-
if "Public Well-being Impact" not in options_dict:
246-
options_dict["Public Well-being Impact"] = "material"
247-
248-
options_dict["Mission & Well-being"] = mission_well_being_table[
249-
(options_dict["Mission Prevalence"], options_dict["Public Well-being Impact"])
250-
]
251-
252-
decision_key = (
253-
options_dict.get("Exploitation"),
254-
options_dict.get("Automatable"),
255-
options_dict.get("Technical Impact"),
256-
options_dict.get("Mission & Well-being"),
257-
)
258-
259-
decision_points = {
260-
"Exploitation": {"E": {"none": "N", "poc": "P", "active": "A"}},
261-
"Automatable": {"A": {"no": "N", "yes": "Y"}},
262-
"Technical Impact": {"T": {"partial": "P", "total": "T"}},
263-
"Public Well-being Impact": {"B": {"minimal": "M", "material": "A", "irreversible": "I"}},
264-
"Mission Prevalence": {"P": {"minimal": "M", "support": "S", "essential": "E"}},
265-
"Mission & Well-being": {"M": {"low": "L", "medium": "M", "high": "H"}},
266-
}
267-
268-
# Create the SSVC vector
269-
ssvc_vector = "SSVCv2/"
270-
for key, value_map in options_dict.items():
271-
options_key = decision_points.get(key)
272-
for lhs, rhs_map in options_key.items():
273-
ssvc_vector += f"{lhs}:{rhs_map.get(value_map)}/"
274-
275-
# "Decision": {"D": {"Track": "T", "Track*": "R", "Attend": "A", "Act": "C"}},
276-
decision_values = {"Track": "T", "Track*": "R", "Attend": "A", "Act": "C"}
277-
278-
decision_lookup = {
279-
("none", "no", "partial", "low"): "Track",
280-
("none", "no", "partial", "medium"): "Track",
281-
("none", "no", "partial", "high"): "Track",
282-
("none", "no", "total", "low"): "Track",
283-
("none", "no", "total", "medium"): "Track",
284-
("none", "no", "total", "high"): "Track*",
285-
("none", "yes", "partial", "low"): "Track",
286-
("none", "yes", "partial", "medium"): "Track",
287-
("none", "yes", "partial", "high"): "Attend",
288-
("none", "yes", "total", "low"): "Track",
289-
("none", "yes", "total", "medium"): "Track",
290-
("none", "yes", "total", "high"): "Attend",
291-
("poc", "no", "partial", "low"): "Track",
292-
("poc", "no", "partial", "medium"): "Track",
293-
("poc", "no", "partial", "high"): "Track*",
294-
("poc", "no", "total", "low"): "Track",
295-
("poc", "no", "total", "medium"): "Track*",
296-
("poc", "no", "total", "high"): "Attend",
297-
("poc", "yes", "partial", "low"): "Track",
298-
("poc", "yes", "partial", "medium"): "Track",
299-
("poc", "yes", "partial", "high"): "Attend",
300-
("poc", "yes", "total", "low"): "Track",
301-
("poc", "yes", "total", "medium"): "Track*",
302-
("poc", "yes", "total", "high"): "Attend",
303-
("active", "no", "partial", "low"): "Track",
304-
("active", "no", "partial", "medium"): "Track",
305-
("active", "no", "partial", "high"): "Attend",
306-
("active", "no", "total", "low"): "Track",
307-
("active", "no", "total", "medium"): "Attend",
308-
("active", "no", "total", "high"): "Act",
309-
("active", "yes", "partial", "low"): "Attend",
310-
("active", "yes", "partial", "medium"): "Attend",
311-
("active", "yes", "partial", "high"): "Act",
312-
("active", "yes", "total", "low"): "Attend",
313-
("active", "yes", "total", "medium"): "Act",
314-
("active", "yes", "total", "high"): "Act",
315-
}
316-
317-
decision = decision_lookup.get(decision_key, "")
318-
319-
if decision:
320-
ssvc_vector += f"D:{decision_values.get(decision)}/"
321-
322-
if timestamp:
323-
timestamp_formatted = dateparser.parse(timestamp).strftime("%Y-%m-%dT%H:%M:%SZ")
324-
325-
ssvc_vector += f"{timestamp_formatted}/"
326-
return ssvc_vector, decision

0 commit comments

Comments
 (0)