Skip to content

Commit 7bb44be

Browse files
committed
Add CodeFix in API
Signed-off-by: Tushar Goel <[email protected]>
1 parent 991fbeb commit 7bb44be

File tree

6 files changed

+133
-17
lines changed

6 files changed

+133
-17
lines changed

vulnerabilities/api_v2.py

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

vulnerabilities/models.py

Lines changed: 2 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

vulnerabilities/pipelines/collect_commits.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def is_vcs_url_already_processed(commit_id):
2020
"""
2121
Check if a VCS URL exists in a CodeFix entry.
2222
"""
23-
return CodeFix.objects.filter(commits__contains=[commit_id]).exists()
23+
if "commit" in commit_id:
24+
return CodeFix.objects.filter(commits__contains=[commit_id]).exists()
2425

2526

2627
class CollectFixCommitsPipeline(VulnerableCodePipeline):
@@ -70,17 +71,19 @@ def collect_and_store_fix_commits(self):
7071
f"Skipping already processed reference: {reference.url} with VCS URL {vcs_url}"
7172
)
7273
continue
73-
code_fix, created = CodeFix.objects.get_or_create(
74-
commits=[vcs_url],
75-
affected_package_vulnerability=apv,
76-
)
77-
78-
if created:
79-
created_fix_count += 1
80-
self.log(
81-
f"Created CodeFix entry for reference: {reference.url} with VCS URL {vcs_url}"
74+
# check if vcs_url has commit
75+
if "/commit/" in vcs_url:
76+
code_fix, created = CodeFix.objects.get_or_create(
77+
commits=[vcs_url],
78+
affected_package_vulnerability=apv,
8279
)
8380

81+
if created:
82+
created_fix_count += 1
83+
self.log(
84+
f"Created CodeFix entry for reference: {reference.url} with VCS URL {vcs_url}"
85+
)
86+
8487
self.log(f"Successfully created {created_fix_count:,d} CodeFix entries.")
8588

8689

vulnerabilities/tests/test_api_v2.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def test_list_packages(self):
216216
Should return a list of packages with their details and associated vulnerabilities.
217217
"""
218218
url = reverse("package-v2-list")
219-
with self.assertNumQueries(31):
219+
with self.assertNumQueries(32):
220220
response = self.client.get(url, format="json")
221221
self.assertEqual(response.status_code, status.HTTP_200_OK)
222222
self.assertIn("results", response.data)
@@ -238,7 +238,7 @@ def test_filter_packages_by_purl(self):
238238
Test filtering packages by one or more PURLs.
239239
"""
240240
url = reverse("package-v2-list")
241-
with self.assertNumQueries(19):
241+
with self.assertNumQueries(20):
242242
response = self.client.get(url, {"purl": "pkg:pypi/[email protected]"}, format="json")
243243
self.assertEqual(response.status_code, status.HTTP_200_OK)
244244
self.assertEqual(len(response.data["results"]["packages"]), 1)
@@ -249,7 +249,7 @@ def test_filter_packages_by_affected_vulnerability(self):
249249
Test filtering packages by affected_by_vulnerability.
250250
"""
251251
url = reverse("package-v2-list")
252-
with self.assertNumQueries(19):
252+
with self.assertNumQueries(20):
253253
response = self.client.get(
254254
url, {"affected_by_vulnerability": "VCID-1234"}, format="json"
255255
)
@@ -308,7 +308,11 @@ def test_package_serializer_fields(self):
308308

309309
# Verify affected_by_vulnerabilities structure
310310
expected_affected_by_vulnerabilities = {
311-
"VCID-1234": {"vulnerability_id": "VCID-1234", "fixed_by_packages": None}
311+
"VCID-1234": {
312+
"code_fixes": [],
313+
"vulnerability_id": "VCID-1234",
314+
"fixed_by_packages": None,
315+
}
312316
}
313317
self.assertEqual(data["affected_by_vulnerabilities"], expected_affected_by_vulnerabilities)
314318

@@ -387,7 +391,13 @@ def test_get_affected_by_vulnerabilities(self):
387391
vulnerabilities = serializer.get_affected_by_vulnerabilities(package)
388392
self.assertEqual(
389393
vulnerabilities,
390-
{"VCID-1234": {"vulnerability_id": "VCID-1234", "fixed_by_packages": None}},
394+
{
395+
"VCID-1234": {
396+
"code_fixes": [],
397+
"vulnerability_id": "VCID-1234",
398+
"fixed_by_packages": None,
399+
}
400+
},
391401
)
392402

393403
def test_get_fixing_vulnerabilities(self):
@@ -591,7 +601,7 @@ def test_lookup_with_valid_purl(self):
591601
"""
592602
url = reverse("package-v2-lookup")
593603
data = {"purl": "pkg:pypi/[email protected]"}
594-
with self.assertNumQueries(12):
604+
with self.assertNumQueries(13):
595605
response = self.client.post(url, data, format="json")
596606
self.assertEqual(response.status_code, status.HTTP_200_OK)
597607
self.assertEqual(1, len(response.data))
@@ -603,7 +613,13 @@ def test_lookup_with_valid_purl(self):
603613
self.assertEqual(response.data[0]["purl"], "pkg:pypi/[email protected]")
604614
self.assertEqual(
605615
response.data[0]["affected_by_vulnerabilities"],
606-
{"VCID-1234": {"vulnerability_id": "VCID-1234", "fixed_by_packages": None}},
616+
{
617+
"VCID-1234": {
618+
"code_fixes": [],
619+
"vulnerability_id": "VCID-1234",
620+
"fixed_by_packages": None,
621+
}
622+
},
607623
)
608624
self.assertEqual(response.data[0]["fixing_vulnerabilities"], [])
609625

vulnerablecode/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from vulnerabilities.api import CPEViewSet
2121
from vulnerabilities.api import PackageViewSet
2222
from vulnerabilities.api import VulnerabilityViewSet
23+
from vulnerabilities.api_v2 import CodeFixViewSet
2324
from vulnerabilities.api_v2 import PackageV2ViewSet
2425
from vulnerabilities.api_v2 import VulnerabilityV2ViewSet
2526
from vulnerabilities.views import ApiUserCreateView
@@ -48,6 +49,8 @@ def __init__(self, *args, **kwargs):
4849
api_v2_router = OptionalSlashRouter()
4950
api_v2_router.register("packages", PackageV2ViewSet, basename="package-v2")
5051
api_v2_router.register("vulnerabilities", VulnerabilityV2ViewSet, basename="vulnerability-v2")
52+
api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix")
53+
5154

5255
urlpatterns = [
5356
path("api/v2/", include(api_v2_router.urls)),

0 commit comments

Comments
 (0)