From e8e4c5e9bf815f9091b47231e6d5d8a6c4a96037 Mon Sep 17 00:00:00 2001 From: Shrish0098 Date: Mon, 10 Feb 2025 02:16:10 +0530 Subject: [PATCH 1/4] Fix: Do not leak codefix IDs in API Signed-off-by: Shrish0098 --- vulnerabilities/api_v2.py | 4 +++- vulnerabilities/models.py | 10 ++++++++++ vulnerablecode/urls.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 10ffb6d98..1a3310655 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -570,7 +570,7 @@ class CodeFixSerializer(serializers.ModelSerializer): class Meta: model = CodeFix fields = [ - "id", + "uuid", "commits", "pulls", "downloads", @@ -594,6 +594,8 @@ class CodeFixViewSet(viewsets.ReadOnlyModelViewSet): queryset = CodeFix.objects.all() serializer_class = CodeFixSerializer + lookup_field = 'uuid' + lookup_url_kwarg = 'uuid' def get_queryset(self): """ diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 9b6df7c13..9206a1a7c 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -11,12 +11,14 @@ import hashlib import json import logging +import uuid import xml.etree.ElementTree as ET from contextlib import suppress from functools import cached_property from itertools import groupby from operator import attrgetter from typing import Union +from django.db import models from cvss.exceptions import CVSS2MalformedError from cvss.exceptions import CVSS3MalformedError @@ -1784,6 +1786,14 @@ class CodeFix(CodeChange): - optionally with a specific fixing package version when it is known """ + uuid = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + unique=True, + help_text="Unique identifier for this code fix" + ) + affected_package_vulnerability = models.ForeignKey( "AffectedByPackageRelatedVulnerability", on_delete=models.CASCADE, diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index c6dd3da44..b335c4dbc 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -50,7 +50,7 @@ def __init__(self, *args, **kwargs): api_v2_router = OptionalSlashRouter() api_v2_router.register("packages", PackageV2ViewSet, basename="package-v2") api_v2_router.register("vulnerabilities", VulnerabilityV2ViewSet, basename="vulnerability-v2") -api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix") +api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix", lookup_field="uuid",) urlpatterns = [ From f7be093f57450d62c73a6aa344c5ef60516c1c31 Mon Sep 17 00:00:00 2001 From: Shrish0098 Date: Sat, 15 Feb 2025 18:46:45 +0530 Subject: [PATCH 2/4] Added test for checking codefix_id Signed-off-by: Shrish0098 --- vulnerabilities/tests/test_codefix_id | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 vulnerabilities/tests/test_codefix_id diff --git a/vulnerabilities/tests/test_codefix_id b/vulnerabilities/tests/test_codefix_id new file mode 100644 index 000000000..0381483e9 --- /dev/null +++ b/vulnerabilities/tests/test_codefix_id @@ -0,0 +1,46 @@ +import re +import json +import uuid + +def is_valid_uuid(value): + try: + uuid.UUID(value, version=4) + return True + except ValueError: + return False + +def test_api_invalid_codefix_ids(test_client, mocker): + # Mock an invalid response + mock_data = { + "vulnerabilities": [ + {"codefix_id": "invalid-uuid-format", "name": "Fake Vuln"}, + ] + } + # Use a simple mock response instead of the real API call + mocker.patch.object(test_client, "get", return_value=mocker.Mock(json=lambda: mock_data, status_code=200)) + + response = test_client.get("/api/vulnerabilities/") + data = response.json() + + # Verify that the test fails if an invalid UUID is present + for vulnerability in data.get("vulnerabilities", []): + if "codefix_id" in vulnerability: + codefix_id = vulnerability["codefix_id"] + assert not is_valid_uuid(codefix_id), ( + f"'{codefix_id}' was unexpectedly treated as a valid UUID." + ) + + + for vulnerability in data.get("vulnerabilities", []): + if "codefix_id" in vulnerability: + codefix_id = vulnerability["codefix_id"] + # Check that it is a valid UUID + assert is_valid_uuid(codefix_id), ( + f"codefix_id '{codefix_id}' is not a valid UUID." + ) + + + response_str = json.dumps(data) + + leaked_pattern = re.compile(r"id\d+") + assert not leaked_pattern.search(response_str), "Old codefix ID format still present in the response." From ab7f19dbedda3e18a94f242b7916a15f4cdfd628 Mon Sep 17 00:00:00 2001 From: Shrish0098 Date: Sat, 15 Feb 2025 22:37:35 +0530 Subject: [PATCH 3/4] Added license header in test_codefix_id Signed-off-by: Shrish0098 --- vulnerabilities/tests/test_codefix_id | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vulnerabilities/tests/test_codefix_id b/vulnerabilities/tests/test_codefix_id index 0381483e9..fa76f976f 100644 --- a/vulnerabilities/tests/test_codefix_id +++ b/vulnerabilities/tests/test_codefix_id @@ -1,3 +1,12 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + import re import json import uuid From ad9f58bea89509327688ba5a83b07fa6a69fd4de Mon Sep 17 00:00:00 2001 From: Shrish0098 Date: Sat, 1 Mar 2025 23:20:06 +0530 Subject: [PATCH 4/4] Fixed the errors Signed-off-by: Shrish0098 --- vulnerabilities/api_v2.py | 4 +- .../0089_remove_codefix_id_codefix_uuid.py | 30 +++++++++++++++ vulnerabilities/models.py | 23 +++++++++++- vulnerabilities/views.py | 37 +++++++++++++++++++ vulnerablecode/urls.py | 2 +- 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 vulnerabilities/migrations/0089_remove_codefix_id_codefix_uuid.py diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 1a3310655..eb43982ed 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -594,8 +594,8 @@ class CodeFixViewSet(viewsets.ReadOnlyModelViewSet): queryset = CodeFix.objects.all() serializer_class = CodeFixSerializer - lookup_field = 'uuid' - lookup_url_kwarg = 'uuid' + lookup_field = "uuid" + lookup_url_kwarg = "uuid" def get_queryset(self): """ diff --git a/vulnerabilities/migrations/0089_remove_codefix_id_codefix_uuid.py b/vulnerabilities/migrations/0089_remove_codefix_id_codefix_uuid.py new file mode 100644 index 000000000..9b7d55d02 --- /dev/null +++ b/vulnerabilities/migrations/0089_remove_codefix_id_codefix_uuid.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.17 on 2025-03-01 17:14 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0088_fix_alpine_purl_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="codefix", + name="id", + ), + migrations.AddField( + model_name="codefix", + name="uuid", + field=models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="Unique identifier for this code fix", + primary_key=True, + serialize=False, + unique=True, + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 9206a1a7c..badd4d3b6 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -18,7 +18,6 @@ from itertools import groupby from operator import attrgetter from typing import Union -from django.db import models from cvss.exceptions import CVSS2MalformedError from cvss.exceptions import CVSS3MalformedError @@ -68,6 +67,26 @@ RANGE_CLASS_BY_SCHEMES["apk"] = AlpineLinuxVersionRange +class CodeFix(models.Model): + + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + vulnerability = models.ForeignKey( + "Vulnerability", on_delete=models.CASCADE, related_name="code_fixes" + ) + package = models.ForeignKey("Package", on_delete=models.CASCADE, related_name="code_fixes") + commit_hash = models.CharField(max_length=64) + commit_url = models.URLField(max_length=1024) + patch_url = models.URLField(max_length=1024, blank=True) + description = models.TextField(blank=True) + created_date = models.DateTimeField(auto_now_add=True) + modified_date = models.DateTimeField(auto_now=True) + created_at = models.DateTimeField(auto_now_add=True) # Keep existing created_at + updated_at = models.DateTimeField(auto_now=True) # Keep existing updated_at + + def __str__(self): + return f"Fix for {self.vulnerability} in {self.package}" + + class BaseQuerySet(models.QuerySet): def get_or_none(self, *args, **kwargs): """ @@ -1791,7 +1810,7 @@ class CodeFix(CodeChange): default=uuid.uuid4, editable=False, unique=True, - help_text="Unique identifier for this code fix" + help_text="Unique identifier for this code fix", ) affected_package_vulnerability = models.ForeignKey( diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index a2df48634..2301810c0 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -23,6 +23,8 @@ from django.views import generic from django.views.generic.detail import DetailView from django.views.generic.list import ListView +from rest_framework import serializers +from rest_framework import viewsets from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import AlpineLinuxVersionRange @@ -38,6 +40,41 @@ PAGE_SIZE = 20 +class CodeFixSerializer(serializers.ModelSerializer): + class Meta: + model = models.CodeFix + fields = [ + "uuid", + "vulnerability", + "package", + "commit_hash", + "commit_url", + "patch_url", + "description", + "created_date", + "modified_date", + "created_at", + "updated_at", + ] + read_only_fields = ["uuid", "created_at", "updated_at"] + + +class CodeFixViewSet(viewsets.ModelViewSet): + lookup_field = "uuid" # This sets the UUID as the lookup parameter + queryset = models.CodeFix.objects.all().order_by("-updated_at") + serializer_class = CodeFixSerializer + filterset_fields = { + "vulnerability": ["exact"], + "package": ["exact"], + "created_date": ["gte", "lte"], + } + search_fields = [ + "vulnerability__vulnerability_id", + "package__purl", + "description", + ] + + class PackageSearch(ListView): model = models.Package template_name = "packages.html" diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index b335c4dbc..c6dd3da44 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -50,7 +50,7 @@ def __init__(self, *args, **kwargs): api_v2_router = OptionalSlashRouter() api_v2_router.register("packages", PackageV2ViewSet, basename="package-v2") api_v2_router.register("vulnerabilities", VulnerabilityV2ViewSet, basename="vulnerability-v2") -api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix", lookup_field="uuid",) +api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix") urlpatterns = [