Skip to content

Commit ab99939

Browse files
authored
Merge pull request #2007 from ziadhany/advisory-fix-commit
Update CodeFixV2 model
2 parents 8c001a1 + 9ff29db commit ab99939

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Generated by Django 4.2.22 on 2025-10-31 14:49
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0102_alter_impactedpackage_affecting_vers_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="CodeCommit",
15+
fields=[
16+
(
17+
"id",
18+
models.AutoField(
19+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
20+
),
21+
),
22+
(
23+
"commit_hash",
24+
models.CharField(
25+
help_text="Unique commit identifier (e.g., SHA).", max_length=64
26+
),
27+
),
28+
(
29+
"vcs_url",
30+
models.URLField(
31+
help_text="URL of the repository containing the commit.", max_length=1024
32+
),
33+
),
34+
(
35+
"commit_rank",
36+
models.IntegerField(
37+
default=0,
38+
help_text="Rank of the commit to support ordering by commit. Rank zero means the rank has not been defined yet",
39+
),
40+
),
41+
(
42+
"commit_author",
43+
models.CharField(
44+
blank=True, help_text="Author of the commit.", max_length=100, null=True
45+
),
46+
),
47+
(
48+
"commit_date",
49+
models.DateTimeField(
50+
blank=True,
51+
help_text="Timestamp indicating when this commit was created.",
52+
null=True,
53+
),
54+
),
55+
(
56+
"commit_message",
57+
models.TextField(
58+
blank=True, help_text="Commit message or description.", null=True
59+
),
60+
),
61+
],
62+
options={
63+
"unique_together": {("commit_hash", "vcs_url")},
64+
},
65+
),
66+
migrations.AddField(
67+
model_name="impactedpackage",
68+
name="affecting_commits",
69+
field=models.ManyToManyField(
70+
help_text="Commits introducing this impact.",
71+
related_name="affecting_commits_in_impacts",
72+
to="vulnerabilities.codecommit",
73+
),
74+
),
75+
migrations.AddField(
76+
model_name="impactedpackage",
77+
name="fixed_by_commits",
78+
field=models.ManyToManyField(
79+
help_text="Commits fixing this impact.",
80+
related_name="fixing_commits_in_impacts",
81+
to="vulnerabilities.codecommit",
82+
),
83+
),
84+
]

vulnerabilities/models.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,6 +2957,18 @@ class ImpactedPackage(models.Model):
29572957
help_text="Packages vulnerable to this impact.",
29582958
)
29592959

2960+
affecting_commits = models.ManyToManyField(
2961+
"CodeCommit",
2962+
related_name="affecting_commits_in_impacts",
2963+
help_text="Commits introducing this impact.",
2964+
)
2965+
2966+
fixed_by_commits = models.ManyToManyField(
2967+
"CodeCommit",
2968+
related_name="fixing_commits_in_impacts",
2969+
help_text="Commits fixing this impact.",
2970+
)
2971+
29602972
created_at = models.DateTimeField(
29612973
auto_now_add=True,
29622974
db_index=True,
@@ -3373,3 +3385,32 @@ class AdvisoryExploit(models.Model):
33733385
@property
33743386
def get_known_ransomware_campaign_use_type(self):
33753387
return "Known" if self.known_ransomware_campaign_use else "Unknown"
3388+
3389+
3390+
class CodeCommit(models.Model):
3391+
"""
3392+
A CodeCommit Represents a single VCS commit (e.g., Git) related to a ImpactedPackage.
3393+
"""
3394+
3395+
commit_hash = models.CharField(max_length=64, help_text="Unique commit identifier (e.g., SHA).")
3396+
vcs_url = models.URLField(
3397+
max_length=1024, help_text="URL of the repository containing the commit."
3398+
)
3399+
3400+
commit_rank = models.IntegerField(
3401+
default=0,
3402+
help_text="Rank of the commit to support ordering by commit. Rank "
3403+
"zero means the rank has not been defined yet",
3404+
)
3405+
commit_author = models.CharField(
3406+
max_length=100, null=True, blank=True, help_text="Author of the commit."
3407+
)
3408+
commit_date = models.DateTimeField(
3409+
null=True, blank=True, help_text="Timestamp indicating when this commit was created."
3410+
)
3411+
commit_message = models.TextField(
3412+
null=True, blank=True, help_text="Commit message or description."
3413+
)
3414+
3415+
class Meta:
3416+
unique_together = ("commit_hash", "vcs_url")
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from datetime import datetime
2+
3+
import pytest
4+
5+
from vulnerabilities.models import AdvisoryV2
6+
from vulnerabilities.models import CodeCommit
7+
from vulnerabilities.models import ImpactedPackage
8+
9+
10+
@pytest.mark.django_db
11+
class TestCodeCommit:
12+
def setup_method(self):
13+
date = datetime.now()
14+
adv = AdvisoryV2.objects.create(
15+
unique_content_id="test_id",
16+
url="https://example.com",
17+
summary="summary",
18+
date_imported=date,
19+
date_collected=date,
20+
advisory_id="test_id",
21+
avid="test_pipeline/test_id",
22+
datasource_id="test_pipeline",
23+
)
24+
25+
self.impacted = ImpactedPackage.objects.create(
26+
advisory=adv,
27+
base_purl="pkg:pypi/redis",
28+
)
29+
30+
self.code_commit1 = CodeCommit.objects.create(
31+
commit_hash="8c001a11dbcb3eb6d851e18f4cefa080af5fb398",
32+
vcs_url="https://github.com/aboutcode-org/test1/",
33+
commit_author="tester1",
34+
commit_message="test message1",
35+
commit_date=datetime.now(),
36+
)
37+
38+
self.code_commit2 = CodeCommit.objects.create(
39+
commit_hash="8c001a1",
40+
vcs_url="https://github.com/aboutcode-org/test1/",
41+
)
42+
43+
self.impacted.fixed_by_commits.add(self.code_commit1)
44+
self.impacted.affecting_commits.add(self.code_commit2)
45+
46+
def test_commits_are_created(self):
47+
commits = CodeCommit.objects.all()
48+
assert commits.count() == 2
49+
50+
def test_commit_fields(self):
51+
commit = CodeCommit.objects.get(commit_hash="8c001a11dbcb3eb6d851e18f4cefa080af5fb398")
52+
assert commit.commit_author == "tester1"
53+
assert "test message1" == commit.commit_message
54+
assert commit.commit_date is not None
55+
56+
def test_impacted_packages_creation(self):
57+
assert ImpactedPackage.objects.count() == 1
58+
assert self.code_commit1 == self.impacted.fixed_by_commits.first()

vulnerabilities/tests/test_data_migrations.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
9+
from datetime import datetime
910

1011
from django.apps import apps
12+
from django.db import IntegrityError
1113
from django.db import connection
1214
from django.db.migrations.executor import MigrationExecutor
1315
from django.test import TestCase
@@ -951,3 +953,82 @@ def test_fix_alpine_purl(self):
951953

952954
assert package.filter(type="alpine").count() == 0
953955
assert package.filter(type="apk").count() == 1
956+
957+
958+
class TestCodeCommitMigration(TestMigrations):
959+
"""
960+
Tests the migration that introduces the CodeCommit model
961+
and adds new ManyToMany fields to ImpactedPackage ( affecting_commits, fixed_by_commits ).
962+
"""
963+
964+
app_name = "vulnerabilities"
965+
migrate_from = "0102_alter_impactedpackage_affecting_vers_and_more"
966+
migrate_to = "0103_codecommit_impactedpackage_affecting_commits_and_more"
967+
968+
def setUpBeforeMigration(self, apps):
969+
"""
970+
Prepare old data before migration — this should be destroyed afterward.
971+
"""
972+
ImpactedPackage = apps.get_model("vulnerabilities", "ImpactedPackage")
973+
AdvisoryV2 = apps.get_model("vulnerabilities", "AdvisoryV2")
974+
975+
date = datetime.now()
976+
adv = AdvisoryV2.objects.create(
977+
unique_content_id="old_adv",
978+
url="https://old.example.com",
979+
summary="Old advisory",
980+
date_imported=date,
981+
date_collected=date,
982+
advisory_id="old_adv",
983+
avid="test_pipeline/old_adv",
984+
datasource_id="test_pipeline",
985+
)
986+
ImpactedPackage.objects.create(advisory=adv, base_purl="pkg:pypi/oldpkg")
987+
988+
def test_unique_constraint_on_commit_hash_and_vcs_url(self):
989+
"""Ensure the (commit_hash, vcs_url) uniqueness constraint works."""
990+
CodeCommit = self.apps.get_model("vulnerabilities", "CodeCommit")
991+
992+
CodeCommit.objects.create(
993+
commit_hash="abc123",
994+
vcs_url="https://github.com/example/repo.git",
995+
commit_rank="0",
996+
commit_author="tester",
997+
commit_message="message 1",
998+
commit_date=datetime.now(),
999+
)
1000+
1001+
with self.assertRaises(IntegrityError):
1002+
CodeCommit.objects.create(
1003+
commit_hash="abc123",
1004+
vcs_url="https://github.com/example/repo.git",
1005+
)
1006+
1007+
def test_m2m_relationships_work(self):
1008+
"""Ensure that the new M2M relationships can store data."""
1009+
ImpactedPackage = self.apps.get_model("vulnerabilities", "ImpactedPackage")
1010+
AdvisoryV2 = self.apps.get_model("vulnerabilities", "AdvisoryV2")
1011+
CodeCommit = self.apps.get_model("vulnerabilities", "CodeCommit")
1012+
1013+
adv = AdvisoryV2.objects.get(
1014+
unique_content_id="old_adv",
1015+
advisory_id="old_adv",
1016+
avid="test_pipeline/old_adv",
1017+
datasource_id="test_pipeline",
1018+
)
1019+
1020+
impacted = ImpactedPackage.objects.get(advisory=adv, base_purl="pkg:pypi/oldpkg")
1021+
commit1 = CodeCommit.objects.create(
1022+
commit_hash="def456",
1023+
vcs_url="https://example.com/repo.git",
1024+
)
1025+
commit2 = CodeCommit.objects.create(
1026+
commit_hash="eef456",
1027+
vcs_url="https://example.com/repo.git",
1028+
)
1029+
1030+
impacted.affecting_commits.add(commit1)
1031+
impacted.fixed_by_commits.add(commit2)
1032+
1033+
self.assertIn(commit1, impacted.affecting_commits.all())
1034+
self.assertIn(commit2, impacted.fixed_by_commits.all())

0 commit comments

Comments
 (0)