Skip to content

Commit 80430b2

Browse files
authored
Merge pull request #1704 from aboutcode-org/collect_fix_commits
Add models for CodeFix
2 parents 5f49a4b + e9a47e5 commit 80430b2

File tree

8 files changed

+743
-7
lines changed

8 files changed

+743
-7
lines changed

vulnerabilities/api_v2.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from rest_framework.response import Response
2222
from rest_framework.reverse import reverse
2323

24+
from vulnerabilities.models import CodeFix
2425
from vulnerabilities.models import Package
2526
from vulnerabilities.models import Vulnerability
2627
from vulnerabilities.models import VulnerabilityReference
@@ -198,14 +199,25 @@ def get_affected_by_vulnerabilities(self, obj):
198199
Return a dictionary with vulnerabilities as keys and their details, including fixed_by_packages.
199200
"""
200201
result = {}
202+
request = self.context.get("request")
201203
for vuln in getattr(obj, "prefetched_affected_vulnerabilities", []):
202204
fixed_by_package = vuln.fixed_by_packages.first()
203205
purl = None
204206
if fixed_by_package:
205207
purl = fixed_by_package.package_url
208+
# Get code fixed for a vulnerability
209+
code_fixes = CodeFix.objects.filter(
210+
affected_package_vulnerability__vulnerability=vuln
211+
).distinct()
212+
code_fix_urls = [
213+
reverse("codefix-detail", args=[code_fix.id], request=request)
214+
for code_fix in code_fixes
215+
]
216+
206217
result[vuln.vulnerability_id] = {
207218
"vulnerability_id": vuln.vulnerability_id,
208219
"fixed_by_packages": purl,
220+
"code_fixes": code_fix_urls,
209221
}
210222
return result
211223

@@ -521,3 +533,76 @@ def lookup(self, request):
521533

522534
qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
523535
return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data)
536+
537+
538+
class CodeFixSerializer(serializers.ModelSerializer):
539+
"""
540+
Serializer for the CodeFix model.
541+
Provides detailed information about a code fix.
542+
"""
543+
544+
affected_vulnerability_id = serializers.CharField(
545+
source="affected_package_vulnerability.vulnerability.vulnerability_id",
546+
read_only=True,
547+
help_text="ID of the affected vulnerability.",
548+
)
549+
affected_package_purl = serializers.CharField(
550+
source="affected_package_vulnerability.package.package_url",
551+
read_only=True,
552+
help_text="PURL of the affected package.",
553+
)
554+
fixed_package_purl = serializers.CharField(
555+
source="fixed_package_vulnerability.package.package_url",
556+
read_only=True,
557+
help_text="PURL of the fixing package (if available).",
558+
)
559+
created_at = serializers.DateTimeField(
560+
format="%Y-%m-%dT%H:%M:%SZ",
561+
read_only=True,
562+
help_text="Timestamp when the code fix was created.",
563+
)
564+
updated_at = serializers.DateTimeField(
565+
format="%Y-%m-%dT%H:%M:%SZ",
566+
read_only=True,
567+
help_text="Timestamp when the code fix was last updated.",
568+
)
569+
570+
class Meta:
571+
model = CodeFix
572+
fields = [
573+
"id",
574+
"commits",
575+
"pulls",
576+
"downloads",
577+
"patch",
578+
"affected_vulnerability_id",
579+
"affected_package_purl",
580+
"fixed_package_purl",
581+
"notes",
582+
"references",
583+
"is_reviewed",
584+
"created_at",
585+
"updated_at",
586+
]
587+
read_only_fields = ["created_at", "updated_at"]
588+
589+
590+
class CodeFixViewSet(viewsets.ReadOnlyModelViewSet):
591+
"""
592+
API endpoint that allows viewing CodeFix entries.
593+
"""
594+
595+
queryset = CodeFix.objects.all()
596+
serializer_class = CodeFixSerializer
597+
598+
def get_queryset(self):
599+
"""
600+
Optionally filter by vulnerability ID.
601+
"""
602+
queryset = super().get_queryset()
603+
vulnerability_id = self.request.query_params.get("vulnerability_id")
604+
if vulnerability_id:
605+
queryset = queryset.filter(
606+
affected_package_vulnerability__vulnerability__vulnerability_id=vulnerability_id
607+
)
608+
return queryset

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from vulnerabilities.improvers import valid_versions
1111
from vulnerabilities.improvers import vulnerability_status
1212
from vulnerabilities.pipelines import VulnerableCodePipeline
13+
from vulnerabilities.pipelines import collect_commits
1314
from vulnerabilities.pipelines import compute_package_risk
1415
from vulnerabilities.pipelines import compute_package_version_rank
1516
from vulnerabilities.pipelines import enhance_with_exploitdb
@@ -41,6 +42,7 @@
4142
enhance_with_exploitdb.ExploitDBImproverPipeline,
4243
compute_package_risk.ComputePackageRiskPipeline,
4344
compute_package_version_rank.ComputeVersionRankPipeline,
45+
collect_commits.CollectFixCommitsPipeline,
4446
]
4547

4648
IMPROVERS_REGISTRY = {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Generated by Django 4.2.16 on 2025-01-08 13:28
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", "0085_alter_package_is_ghost_alter_package_version_rank_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="CodeFix",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"commits",
25+
models.JSONField(
26+
blank=True,
27+
default=list,
28+
help_text="List of commit identifiers using VCS URLs associated with the code change.",
29+
),
30+
),
31+
(
32+
"pulls",
33+
models.JSONField(
34+
blank=True,
35+
default=list,
36+
help_text="List of pull request URLs associated with the code change.",
37+
),
38+
),
39+
(
40+
"downloads",
41+
models.JSONField(
42+
blank=True,
43+
default=list,
44+
help_text="List of download URLs for the patched code.",
45+
),
46+
),
47+
(
48+
"patch",
49+
models.TextField(
50+
blank=True,
51+
help_text="The code change as a patch in unified diff format.",
52+
null=True,
53+
),
54+
),
55+
(
56+
"notes",
57+
models.TextField(
58+
blank=True,
59+
help_text="Notes or instructions about this code change.",
60+
null=True,
61+
),
62+
),
63+
(
64+
"references",
65+
models.JSONField(
66+
blank=True,
67+
default=list,
68+
help_text="URL references related to this code change.",
69+
),
70+
),
71+
(
72+
"is_reviewed",
73+
models.BooleanField(
74+
default=False, help_text="Indicates if this code change has been reviewed."
75+
),
76+
),
77+
(
78+
"created_at",
79+
models.DateTimeField(
80+
auto_now_add=True,
81+
help_text="Timestamp indicating when this code change was created.",
82+
),
83+
),
84+
(
85+
"updated_at",
86+
models.DateTimeField(
87+
auto_now=True,
88+
help_text="Timestamp indicating when this code change was last updated.",
89+
),
90+
),
91+
(
92+
"affected_package_vulnerability",
93+
models.ForeignKey(
94+
help_text="The affected package version to which this code fix applies.",
95+
on_delete=django.db.models.deletion.CASCADE,
96+
related_name="code_fix",
97+
to="vulnerabilities.affectedbypackagerelatedvulnerability",
98+
),
99+
),
100+
(
101+
"base_package_version",
102+
models.ForeignKey(
103+
blank=True,
104+
help_text="The base package version to which this code change applies.",
105+
null=True,
106+
on_delete=django.db.models.deletion.SET_NULL,
107+
related_name="codechanges",
108+
to="vulnerabilities.package",
109+
),
110+
),
111+
(
112+
"fixed_package_vulnerability",
113+
models.ForeignKey(
114+
blank=True,
115+
help_text="The fixing package version with this code fix",
116+
null=True,
117+
on_delete=django.db.models.deletion.SET_NULL,
118+
related_name="code_fix",
119+
to="vulnerabilities.fixingpackagerelatedvulnerability",
120+
),
121+
),
122+
],
123+
options={
124+
"abstract": False,
125+
},
126+
),
127+
]

vulnerabilities/models.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,8 @@ class AffectedByPackageRelatedVulnerability(PackageRelatedVulnerabilityBase):
11011101
related_name="affected_package_vulnerability_relations",
11021102
)
11031103

1104+
objects = BaseQuerySet.as_manager()
1105+
11041106
class Meta(PackageRelatedVulnerabilityBase.Meta):
11051107
verbose_name_plural = "Affected By Package Related Vulnerabilities"
11061108

@@ -1581,3 +1583,81 @@ class Exploit(models.Model):
15811583
@property
15821584
def get_known_ransomware_campaign_use_type(self):
15831585
return "Known" if self.known_ransomware_campaign_use else "Unknown"
1586+
1587+
1588+
class CodeChange(models.Model):
1589+
"""
1590+
Abstract base model representing a change in code, either introducing or fixing a vulnerability.
1591+
This includes details about commits, patches, and related metadata.
1592+
1593+
We are tracking commits, pulls and downloads as references to the code change. The goal is to
1594+
keep track and store the actual code patch in the ``patch`` field. When not available the patch
1595+
will be inferred from these references using improvers.
1596+
"""
1597+
1598+
commits = models.JSONField(
1599+
blank=True,
1600+
default=list,
1601+
help_text="List of commit identifiers using VCS URLs associated with the code change.",
1602+
)
1603+
pulls = models.JSONField(
1604+
blank=True,
1605+
default=list,
1606+
help_text="List of pull request URLs associated with the code change.",
1607+
)
1608+
downloads = models.JSONField(
1609+
blank=True, default=list, help_text="List of download URLs for the patched code."
1610+
)
1611+
patch = models.TextField(
1612+
blank=True, null=True, help_text="The code change as a patch in unified diff format."
1613+
)
1614+
base_package_version = models.ForeignKey(
1615+
"Package",
1616+
null=True,
1617+
blank=True,
1618+
on_delete=models.SET_NULL,
1619+
related_name="codechanges",
1620+
help_text="The base package version to which this code change applies.",
1621+
)
1622+
notes = models.TextField(
1623+
blank=True, null=True, help_text="Notes or instructions about this code change."
1624+
)
1625+
references = models.JSONField(
1626+
blank=True, default=list, help_text="URL references related to this code change."
1627+
)
1628+
is_reviewed = models.BooleanField(
1629+
default=False, help_text="Indicates if this code change has been reviewed."
1630+
)
1631+
created_at = models.DateTimeField(
1632+
auto_now_add=True, help_text="Timestamp indicating when this code change was created."
1633+
)
1634+
updated_at = models.DateTimeField(
1635+
auto_now=True, help_text="Timestamp indicating when this code change was last updated."
1636+
)
1637+
1638+
class Meta:
1639+
abstract = True
1640+
1641+
1642+
class CodeFix(CodeChange):
1643+
"""
1644+
A code fix is a code change that addresses a vulnerability and is associated:
1645+
- with a specific affected package version
1646+
- optionally with a specific fixing package version when it is known
1647+
"""
1648+
1649+
affected_package_vulnerability = models.ForeignKey(
1650+
"AffectedByPackageRelatedVulnerability",
1651+
on_delete=models.CASCADE,
1652+
related_name="code_fix",
1653+
help_text="The affected package version to which this code fix applies.",
1654+
)
1655+
1656+
fixed_package_vulnerability = models.ForeignKey(
1657+
"FixingPackageRelatedVulnerability",
1658+
null=True,
1659+
blank=True,
1660+
on_delete=models.SET_NULL,
1661+
related_name="code_fix",
1662+
help_text="The fixing package version with this code fix",
1663+
)

0 commit comments

Comments
 (0)