Skip to content

Commit 11284ee

Browse files
authored
Merge pull request #1764 from aboutcode-org/1699-advisory-todo
Add pipeline to compute Advisory ToDos
2 parents a05b65e + f147907 commit 11284ee

File tree

10 files changed

+807
-20
lines changed

10 files changed

+807
-20
lines changed

vulnerabilities/importer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,13 @@ def from_dict(cls, affected_pkg: dict):
268268
return
269269

270270
fixed_version = affected_pkg["fixed_version"]
271-
if fixed_version and affected_version_range:
272-
# TODO: revisit after https://github.com/nexB/univers/issues/10
273-
fixed_version = affected_version_range.version_class(fixed_version)
271+
if fixed_version:
272+
if affected_version_range:
273+
# TODO: revisit after https://github.com/nexB/univers/issues/10
274+
fixed_version = affected_version_range.version_class(fixed_version)
275+
elif package.type in RANGE_CLASS_BY_SCHEMES:
276+
vrc = RANGE_CLASS_BY_SCHEMES[package.type]
277+
fixed_version = vrc.version_class(fixed_version)
274278

275279
if not fixed_version and not affected_version_range:
276280
logger.error(

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from vulnerabilities.pipelines import VulnerableCodePipeline
1313
from vulnerabilities.pipelines import add_cvss31_to_CVEs
1414
from vulnerabilities.pipelines import collect_commits
15+
from vulnerabilities.pipelines import compute_advisory_todo
1516
from vulnerabilities.pipelines import compute_package_risk
1617
from vulnerabilities.pipelines import compute_package_version_rank
1718
from vulnerabilities.pipelines import enhance_with_exploitdb
@@ -49,6 +50,7 @@
4950
add_cvss31_to_CVEs.CVEAdvisoryMappingPipeline,
5051
remove_duplicate_advisories.RemoveDuplicateAdvisoriesPipeline,
5152
populate_vulnerability_summary_pipeline.PopulateVulnerabilitySummariesPipeline,
53+
compute_advisory_todo.ComputeToDo,
5254
]
5355

5456
IMPROVERS_REGISTRY = {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Generated by Django 4.2.22 on 2025-06-27 15:59
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", "0092_pipelineschedule_pipelinerun"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="AdvisoryToDo",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"related_advisories_id",
25+
models.CharField(
26+
help_text="SHA1 digest of the unique_content_id field of the applicable advisories.",
27+
max_length=40,
28+
),
29+
),
30+
(
31+
"issue_type",
32+
models.CharField(
33+
choices=[
34+
("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"),
35+
("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"),
36+
(
37+
"MISSING_AFFECTED_AND_FIXED_BY_PACKAGES",
38+
"Advisory is missing both affected and fixed-by packages",
39+
),
40+
("MISSING_SUMMARY", "Advisory is missing summary"),
41+
(
42+
"CONFLICTING_FIXED_BY_PACKAGES",
43+
"Advisories have conflicting fixed-by packages",
44+
),
45+
(
46+
"CONFLICTING_AFFECTED_PACKAGES",
47+
"Advisories have conflicting affected packages",
48+
),
49+
(
50+
"CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES",
51+
"Advisories have conflicting affected and fixed-by packages",
52+
),
53+
(
54+
"CONFLICTING_SEVERITY_SCORES",
55+
"Advisories have conflicting severity scores",
56+
),
57+
],
58+
db_index=True,
59+
help_text="Select the issue that needs to be addressed from the available options.",
60+
max_length=50,
61+
),
62+
),
63+
(
64+
"issue_detail",
65+
models.TextField(blank=True, help_text="Additional details about the issue."),
66+
),
67+
(
68+
"created_at",
69+
models.DateTimeField(
70+
auto_now_add=True,
71+
help_text="Timestamp indicating when this TODO was created.",
72+
),
73+
),
74+
(
75+
"is_resolved",
76+
models.BooleanField(
77+
db_index=True, default=False, help_text="This TODO is resolved or not."
78+
),
79+
),
80+
(
81+
"resolved_at",
82+
models.DateTimeField(
83+
blank=True,
84+
help_text="Timestamp indicating when this TODO was resolved.",
85+
null=True,
86+
),
87+
),
88+
(
89+
"resolution_detail",
90+
models.TextField(
91+
blank=True, help_text="Additional detail on how this TODO was resolved."
92+
),
93+
),
94+
],
95+
),
96+
migrations.CreateModel(
97+
name="ToDoRelatedAdvisory",
98+
fields=[
99+
(
100+
"id",
101+
models.AutoField(
102+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
103+
),
104+
),
105+
(
106+
"advisory",
107+
models.ForeignKey(
108+
on_delete=django.db.models.deletion.CASCADE, to="vulnerabilities.advisory"
109+
),
110+
),
111+
(
112+
"todo",
113+
models.ForeignKey(
114+
on_delete=django.db.models.deletion.CASCADE,
115+
to="vulnerabilities.advisorytodo",
116+
),
117+
),
118+
],
119+
options={
120+
"unique_together": {("todo", "advisory")},
121+
},
122+
),
123+
migrations.AddField(
124+
model_name="advisorytodo",
125+
name="advisories",
126+
field=models.ManyToManyField(
127+
help_text="Advisory/ies where this TODO is applicable.",
128+
related_name="advisory_todos",
129+
through="vulnerabilities.ToDoRelatedAdvisory",
130+
to="vulnerabilities.advisory",
131+
),
132+
),
133+
migrations.AlterUniqueTogether(
134+
name="advisorytodo",
135+
unique_together={("related_advisories_id", "issue_type")},
136+
),
137+
]

vulnerabilities/models.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2258,3 +2258,96 @@ def create_new_job(self, execute_now=False):
22582258
schedules.clear_job(self.schedule_work_id)
22592259

22602260
return schedules.schedule_execution(self, execute_now) if self.is_active else None
2261+
2262+
2263+
ISSUE_TYPE_CHOICES = [
2264+
("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"),
2265+
("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"),
2266+
(
2267+
"MISSING_AFFECTED_AND_FIXED_BY_PACKAGES",
2268+
"Advisory is missing both affected and fixed-by packages",
2269+
),
2270+
("MISSING_SUMMARY", "Advisory is missing summary"),
2271+
("CONFLICTING_FIXED_BY_PACKAGES", "Advisories have conflicting fixed-by packages"),
2272+
("CONFLICTING_AFFECTED_PACKAGES", "Advisories have conflicting affected packages"),
2273+
(
2274+
"CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES",
2275+
"Advisories have conflicting affected and fixed-by packages",
2276+
),
2277+
("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"),
2278+
]
2279+
2280+
2281+
class AdvisoryToDo(models.Model):
2282+
"""Track the TODOs for advisory/ies that need to be addressed."""
2283+
2284+
# Since we can not make advisories field (M2M field) unique
2285+
# (see https://code.djangoproject.com/ticket/702), we use related_advisories_id
2286+
# to avoid creating duplicate issue for same set of advisories,
2287+
related_advisories_id = models.CharField(
2288+
max_length=40,
2289+
help_text="SHA1 digest of the unique_content_id field of the applicable advisories.",
2290+
)
2291+
2292+
advisories = models.ManyToManyField(
2293+
Advisory,
2294+
through="ToDoRelatedAdvisory",
2295+
related_name="advisory_todos",
2296+
help_text="Advisory/ies where this TODO is applicable.",
2297+
)
2298+
2299+
issue_type = models.CharField(
2300+
max_length=50,
2301+
choices=ISSUE_TYPE_CHOICES,
2302+
db_index=True,
2303+
help_text="Select the issue that needs to be addressed from the available options.",
2304+
)
2305+
2306+
issue_detail = models.TextField(
2307+
blank=True,
2308+
help_text="Additional details about the issue.",
2309+
)
2310+
2311+
created_at = models.DateTimeField(
2312+
auto_now_add=True,
2313+
help_text="Timestamp indicating when this TODO was created.",
2314+
)
2315+
2316+
is_resolved = models.BooleanField(
2317+
default=False,
2318+
db_index=True,
2319+
help_text="This TODO is resolved or not.",
2320+
)
2321+
2322+
resolved_at = models.DateTimeField(
2323+
null=True,
2324+
blank=True,
2325+
help_text="Timestamp indicating when this TODO was resolved.",
2326+
)
2327+
2328+
resolution_detail = models.TextField(
2329+
blank=True,
2330+
help_text="Additional detail on how this TODO was resolved.",
2331+
)
2332+
2333+
class Meta:
2334+
unique_together = ("related_advisories_id", "issue_type")
2335+
2336+
def save(self, *args, **kwargs):
2337+
self.full_clean()
2338+
return super().save(*args, **kwargs)
2339+
2340+
2341+
class ToDoRelatedAdvisory(models.Model):
2342+
todo = models.ForeignKey(
2343+
AdvisoryToDo,
2344+
on_delete=models.CASCADE,
2345+
)
2346+
2347+
advisory = models.ForeignKey(
2348+
Advisory,
2349+
on_delete=models.CASCADE,
2350+
)
2351+
2352+
class Meta:
2353+
unique_together = ("todo", "advisory")

0 commit comments

Comments
 (0)