Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add models for CodeFix #1704

Merged
merged 11 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions vulnerabilities/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from rest_framework.response import Response
from rest_framework.reverse import reverse

from vulnerabilities.models import CodeFix
from vulnerabilities.models import Package
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityReference
Expand Down Expand Up @@ -198,14 +199,25 @@ def get_affected_by_vulnerabilities(self, obj):
Return a dictionary with vulnerabilities as keys and their details, including fixed_by_packages.
"""
result = {}
request = self.context.get("request")
for vuln in getattr(obj, "prefetched_affected_vulnerabilities", []):
fixed_by_package = vuln.fixed_by_packages.first()
purl = None
if fixed_by_package:
purl = fixed_by_package.package_url
# Get code fixed for a vulnerability
code_fixes = CodeFix.objects.filter(
affected_package_vulnerability__vulnerability=vuln
).distinct()
code_fix_urls = [
reverse("codefix-detail", args=[code_fix.id], request=request)
for code_fix in code_fixes
]

result[vuln.vulnerability_id] = {
"vulnerability_id": vuln.vulnerability_id,
"fixed_by_packages": purl,
"code_fixes": code_fix_urls,
}
return result

Expand Down Expand Up @@ -521,3 +533,76 @@ def lookup(self, request):

qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data)


class CodeFixSerializer(serializers.ModelSerializer):
"""
Serializer for the CodeFix model.
Provides detailed information about a code fix.
"""

affected_vulnerability_id = serializers.CharField(
source="affected_package_vulnerability.vulnerability.vulnerability_id",
read_only=True,
help_text="ID of the affected vulnerability.",
)
affected_package_purl = serializers.CharField(
source="affected_package_vulnerability.package.package_url",
read_only=True,
help_text="PURL of the affected package.",
)
fixed_package_purl = serializers.CharField(
source="fixed_package_vulnerability.package.package_url",
read_only=True,
help_text="PURL of the fixing package (if available).",
)
created_at = serializers.DateTimeField(
format="%Y-%m-%dT%H:%M:%SZ",
read_only=True,
help_text="Timestamp when the code fix was created.",
)
updated_at = serializers.DateTimeField(
format="%Y-%m-%dT%H:%M:%SZ",
read_only=True,
help_text="Timestamp when the code fix was last updated.",
)

class Meta:
model = CodeFix
fields = [
"id",
"commits",
"pulls",
"downloads",
"patch",
"affected_vulnerability_id",
"affected_package_purl",
"fixed_package_purl",
"notes",
"references",
"is_reviewed",
"created_at",
"updated_at",
]
read_only_fields = ["created_at", "updated_at"]


class CodeFixViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint that allows viewing CodeFix entries.
"""

queryset = CodeFix.objects.all()
serializer_class = CodeFixSerializer

def get_queryset(self):
"""
Optionally filter by vulnerability ID.
"""
queryset = super().get_queryset()
vulnerability_id = self.request.query_params.get("vulnerability_id")
if vulnerability_id:
queryset = queryset.filter(
affected_package_vulnerability__vulnerability__vulnerability_id=vulnerability_id
)
return queryset
2 changes: 2 additions & 0 deletions vulnerabilities/improvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from vulnerabilities.improvers import valid_versions
from vulnerabilities.improvers import vulnerability_status
from vulnerabilities.pipelines import VulnerableCodePipeline
from vulnerabilities.pipelines import collect_commits
from vulnerabilities.pipelines import compute_package_risk
from vulnerabilities.pipelines import compute_package_version_rank
from vulnerabilities.pipelines import enhance_with_exploitdb
Expand Down Expand Up @@ -41,6 +42,7 @@
enhance_with_exploitdb.ExploitDBImproverPipeline,
compute_package_risk.ComputePackageRiskPipeline,
compute_package_version_rank.ComputeVersionRankPipeline,
collect_commits.CollectFixCommitsPipeline,
]

IMPROVERS_REGISTRY = {
Expand Down
127 changes: 127 additions & 0 deletions vulnerabilities/migrations/0086_codefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Generated by Django 4.2.16 on 2025-01-08 13:28

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0085_alter_package_is_ghost_alter_package_version_rank_and_more"),
]

operations = [
migrations.CreateModel(
name="CodeFix",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"commits",
models.JSONField(
blank=True,
default=list,
help_text="List of commit identifiers using VCS URLs associated with the code change.",
),
),
(
"pulls",
models.JSONField(
blank=True,
default=list,
help_text="List of pull request URLs associated with the code change.",
),
),
(
"downloads",
models.JSONField(
blank=True,
default=list,
help_text="List of download URLs for the patched code.",
),
),
(
"patch",
models.TextField(
blank=True,
help_text="The code change as a patch in unified diff format.",
null=True,
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Notes or instructions about this code change.",
null=True,
),
),
(
"references",
models.JSONField(
blank=True,
default=list,
help_text="URL references related to this code change.",
),
),
(
"is_reviewed",
models.BooleanField(
default=False, help_text="Indicates if this code change has been reviewed."
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="Timestamp indicating when this code change was created.",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="Timestamp indicating when this code change was last updated.",
),
),
(
"affected_package_vulnerability",
models.ForeignKey(
help_text="The affected package version to which this code fix applies.",
on_delete=django.db.models.deletion.CASCADE,
related_name="code_fix",
to="vulnerabilities.affectedbypackagerelatedvulnerability",
),
),
(
"base_package_version",
models.ForeignKey(
blank=True,
help_text="The base package version to which this code change applies.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="codechanges",
to="vulnerabilities.package",
),
),
(
"fixed_package_vulnerability",
models.ForeignKey(
blank=True,
help_text="The fixing package version with this code fix",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="code_fix",
to="vulnerabilities.fixingpackagerelatedvulnerability",
),
),
],
options={
"abstract": False,
},
),
]
80 changes: 80 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,8 @@ class AffectedByPackageRelatedVulnerability(PackageRelatedVulnerabilityBase):
related_name="affected_package_vulnerability_relations",
)

objects = BaseQuerySet.as_manager()

class Meta(PackageRelatedVulnerabilityBase.Meta):
verbose_name_plural = "Affected By Package Related Vulnerabilities"

Expand Down Expand Up @@ -1581,3 +1583,81 @@ class Exploit(models.Model):
@property
def get_known_ransomware_campaign_use_type(self):
return "Known" if self.known_ransomware_campaign_use else "Unknown"


class CodeChange(models.Model):
"""
Abstract base model representing a change in code, either introducing or fixing a vulnerability.
This includes details about commits, patches, and related metadata.

We are tracking commits, pulls and downloads as references to the code change. The goal is to
keep track and store the actual code patch in the ``patch`` field. When not available the patch
will be inferred from these references using improvers.
"""

commits = models.JSONField(
blank=True,
default=list,
help_text="List of commit identifiers using VCS URLs associated with the code change.",
)
pulls = models.JSONField(
blank=True,
default=list,
help_text="List of pull request URLs associated with the code change.",
)
downloads = models.JSONField(
blank=True, default=list, help_text="List of download URLs for the patched code."
)
patch = models.TextField(
blank=True, null=True, help_text="The code change as a patch in unified diff format."
)
base_package_version = models.ForeignKey(
"Package",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="codechanges",
help_text="The base package version to which this code change applies.",
)
notes = models.TextField(
blank=True, null=True, help_text="Notes or instructions about this code change."
)
references = models.JSONField(
blank=True, default=list, help_text="URL references related to this code change."
)
is_reviewed = models.BooleanField(
default=False, help_text="Indicates if this code change has been reviewed."
)
created_at = models.DateTimeField(
auto_now_add=True, help_text="Timestamp indicating when this code change was created."
)
updated_at = models.DateTimeField(
auto_now=True, help_text="Timestamp indicating when this code change was last updated."
)

class Meta:
abstract = True


class CodeFix(CodeChange):
"""
A code fix is a code change that addresses a vulnerability and is associated:
- with a specific affected package version
- optionally with a specific fixing package version when it is known
"""

affected_package_vulnerability = models.ForeignKey(
"AffectedByPackageRelatedVulnerability",
on_delete=models.CASCADE,
related_name="code_fix",
help_text="The affected package version to which this code fix applies.",
)

fixed_package_vulnerability = models.ForeignKey(
"FixingPackageRelatedVulnerability",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="code_fix",
help_text="The fixing package version with this code fix",
)
Loading