Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 4.2.22 on 2025-10-31 14:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0102_alter_impactedpackage_affecting_vers_and_more"),
]

operations = [
migrations.CreateModel(
name="CodeCommit",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"commit_hash",
models.CharField(
help_text="Unique commit identifier (e.g., SHA).", max_length=64
),
),
(
"vcs_url",
models.URLField(
help_text="URL of the repository containing the commit.", max_length=1024
),
),
(
"commit_rank",
models.IntegerField(
default=0,
help_text="Rank of the commit to support ordering by commit. Rank zero means the rank has not been defined yet",
),
),
(
"commit_author",
models.CharField(
blank=True, help_text="Author of the commit.", max_length=100, null=True
),
),
(
"commit_date",
models.DateTimeField(
blank=True,
help_text="Timestamp indicating when this commit was created.",
null=True,
),
),
(
"commit_message",
models.TextField(
blank=True, help_text="Commit message or description.", null=True
),
),
],
options={
"unique_together": {("commit_hash", "vcs_url")},
},
),
migrations.AddField(
model_name="impactedpackage",
name="affecting_commits",
field=models.ManyToManyField(
help_text="Commits introducing this impact.",
related_name="affecting_commits_in_impacts",
to="vulnerabilities.codecommit",
),
),
migrations.AddField(
model_name="impactedpackage",
name="fixed_by_commits",
field=models.ManyToManyField(
help_text="Commits fixing this impact.",
related_name="fixing_commits_in_impacts",
to="vulnerabilities.codecommit",
),
),
]
41 changes: 41 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2957,6 +2957,18 @@ class ImpactedPackage(models.Model):
help_text="Packages vulnerable to this impact.",
)

affecting_commits = models.ManyToManyField(
"CodeCommit",
related_name="affecting_commits_in_impacts",
help_text="Commits introducing this impact.",
)

fixed_by_commits = models.ManyToManyField(
"CodeCommit",
related_name="fixing_commits_in_impacts",
help_text="Commits fixing this impact.",
)

created_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
Expand Down Expand Up @@ -3373,3 +3385,32 @@ class AdvisoryExploit(models.Model):
@property
def get_known_ransomware_campaign_use_type(self):
return "Known" if self.known_ransomware_campaign_use else "Unknown"


class CodeCommit(models.Model):
"""
A CodeCommit Represents a single VCS commit (e.g., Git) related to a ImpactedPackage.
"""

commit_hash = models.CharField(max_length=64, help_text="Unique commit identifier (e.g., SHA).")
vcs_url = models.URLField(
max_length=1024, help_text="URL of the repository containing the commit."
)

commit_rank = models.IntegerField(
default=0,
help_text="Rank of the commit to support ordering by commit. Rank "
"zero means the rank has not been defined yet",
)
commit_author = models.CharField(
max_length=100, null=True, blank=True, help_text="Author of the commit."
)
commit_date = models.DateTimeField(
null=True, blank=True, help_text="Timestamp indicating when this commit was created."
)
commit_message = models.TextField(
null=True, blank=True, help_text="Commit message or description."
)

class Meta:
unique_together = ("commit_hash", "vcs_url")
58 changes: 58 additions & 0 deletions vulnerabilities/tests/test_commit_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import datetime

import pytest

from vulnerabilities.models import AdvisoryV2
from vulnerabilities.models import CodeCommit
from vulnerabilities.models import ImpactedPackage


@pytest.mark.django_db
class TestCodeCommit:
def setup_method(self):
date = datetime.now()
adv = AdvisoryV2.objects.create(
unique_content_id="test_id",
url="https://example.com",
summary="summary",
date_imported=date,
date_collected=date,
advisory_id="test_id",
avid="test_pipeline/test_id",
datasource_id="test_pipeline",
)

self.impacted = ImpactedPackage.objects.create(
advisory=adv,
base_purl="pkg:pypi/redis",
)

self.code_commit1 = CodeCommit.objects.create(
commit_hash="8c001a11dbcb3eb6d851e18f4cefa080af5fb398",
vcs_url="https://github.com/aboutcode-org/test1/",
commit_author="tester1",
commit_message="test message1",
commit_date=datetime.now(),
)

self.code_commit2 = CodeCommit.objects.create(
commit_hash="8c001a1",
vcs_url="https://github.com/aboutcode-org/test1/",
)

self.impacted.fixed_by_commits.add(self.code_commit1)
self.impacted.affecting_commits.add(self.code_commit2)

def test_commits_are_created(self):
commits = CodeCommit.objects.all()
assert commits.count() == 2

def test_commit_fields(self):
commit = CodeCommit.objects.get(commit_hash="8c001a11dbcb3eb6d851e18f4cefa080af5fb398")
assert commit.commit_author == "tester1"
assert "test message1" == commit.commit_message
assert commit.commit_date is not None

def test_impacted_packages_creation(self):
assert ImpactedPackage.objects.count() == 1
assert self.code_commit1 == self.impacted.fixed_by_commits.first()
81 changes: 81 additions & 0 deletions vulnerabilities/tests/test_data_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
from datetime import datetime

from django.apps import apps
from django.db import IntegrityError
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import TestCase
Expand Down Expand Up @@ -951,3 +953,82 @@ def test_fix_alpine_purl(self):

assert package.filter(type="alpine").count() == 0
assert package.filter(type="apk").count() == 1


class TestCodeCommitMigration(TestMigrations):
"""
Tests the migration that introduces the CodeCommit model
and adds new ManyToMany fields to ImpactedPackage ( affecting_commits, fixed_by_commits ).
"""

app_name = "vulnerabilities"
migrate_from = "0102_alter_impactedpackage_affecting_vers_and_more"
migrate_to = "0103_codecommit_impactedpackage_affecting_commits_and_more"

def setUpBeforeMigration(self, apps):
"""
Prepare old data before migration — this should be destroyed afterward.
"""
ImpactedPackage = apps.get_model("vulnerabilities", "ImpactedPackage")
AdvisoryV2 = apps.get_model("vulnerabilities", "AdvisoryV2")

date = datetime.now()
adv = AdvisoryV2.objects.create(
unique_content_id="old_adv",
url="https://old.example.com",
summary="Old advisory",
date_imported=date,
date_collected=date,
advisory_id="old_adv",
avid="test_pipeline/old_adv",
datasource_id="test_pipeline",
)
ImpactedPackage.objects.create(advisory=adv, base_purl="pkg:pypi/oldpkg")

def test_unique_constraint_on_commit_hash_and_vcs_url(self):
"""Ensure the (commit_hash, vcs_url) uniqueness constraint works."""
CodeCommit = self.apps.get_model("vulnerabilities", "CodeCommit")

CodeCommit.objects.create(
commit_hash="abc123",
vcs_url="https://github.com/example/repo.git",
commit_rank="0",
commit_author="tester",
commit_message="message 1",
commit_date=datetime.now(),
)

with self.assertRaises(IntegrityError):
CodeCommit.objects.create(
commit_hash="abc123",
vcs_url="https://github.com/example/repo.git",
)

def test_m2m_relationships_work(self):
"""Ensure that the new M2M relationships can store data."""
ImpactedPackage = self.apps.get_model("vulnerabilities", "ImpactedPackage")
AdvisoryV2 = self.apps.get_model("vulnerabilities", "AdvisoryV2")
CodeCommit = self.apps.get_model("vulnerabilities", "CodeCommit")

adv = AdvisoryV2.objects.get(
unique_content_id="old_adv",
advisory_id="old_adv",
avid="test_pipeline/old_adv",
datasource_id="test_pipeline",
)

impacted = ImpactedPackage.objects.get(advisory=adv, base_purl="pkg:pypi/oldpkg")
commit1 = CodeCommit.objects.create(
commit_hash="def456",
vcs_url="https://example.com/repo.git",
)
commit2 = CodeCommit.objects.create(
commit_hash="eef456",
vcs_url="https://example.com/repo.git",
)

impacted.affecting_commits.add(commit1)
impacted.fixed_by_commits.add(commit2)

self.assertIn(commit1, impacted.affecting_commits.all())
self.assertIn(commit2, impacted.fixed_by_commits.all())