Skip to content

Commit 8c3493f

Browse files
committed
Add support for affected and fixed commits in AdvisoryData
Add aosp importer and a test Update the aosp importer to correctly parse CodeCommit using packageurl-python Update packageurl-python version Remove invalid purl in test_osv.py vulntotal Add a docs for VCS_URLS_SUPPORTED_TYPES Signed-off-by: ziad hany <[email protected]>
1 parent 98e5160 commit 8c3493f

27 files changed

+1080
-133
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ MarkupSafe==2.1.1
6464
matplotlib-inline==0.1.3
6565
multidict==6.0.2
6666
mypy-extensions==0.4.3
67-
packageurl-python==0.15.6
67+
packageurl-python==0.17.6
6868
packaging==21.3
6969
paramiko==3.4.0
7070
parso==0.8.3

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ install_requires =
7171
drf-spectacular[sidecar]>=0.24.2
7272

7373
#essentials
74-
packageurl-python>=0.15
74+
packageurl-python>=0.17
7575
univers>=30.12.0
7676
license-expression>=30.0.0
7777

vulnerabilities/importer.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from vulnerabilities.severity_systems import ScoringSystem
3838
from vulnerabilities.utils import classproperty
3939
from vulnerabilities.utils import get_reference_id
40+
from vulnerabilities.utils import is_commit
4041
from vulnerabilities.utils import is_cve
4142
from vulnerabilities.utils import nearest_patched_package
4243
from vulnerabilities.utils import purl_to_dict
@@ -194,6 +195,65 @@ def from_url(cls, url):
194195
return cls(url=url)
195196

196197

198+
"""
199+
For VCS URLs that can currently be formed into PURLs (github, bitbucket, and gitlab),
200+
we support full code commit collection.
201+
202+
For any VCS URL types not included in this set, CodeCommit objects will not be
203+
created at this time. Instead, unsupported VCS URLs will be stored only as
204+
references, serving as a fallback until we support them.
205+
"""
206+
VCS_URLS_SUPPORTED_TYPES = {"github", "bitbucket", "gitlab"}
207+
208+
209+
@dataclasses.dataclass(eq=True)
210+
@functools.total_ordering
211+
class CodePatchData:
212+
commit_hash: str
213+
vcs_url: str
214+
commit_patch: Optional[str] = None
215+
216+
def __post_init__(self):
217+
if not self.commit_hash:
218+
raise ValueError("Commit must have a non-empty commit_hash.")
219+
220+
if not is_commit(self.commit_hash):
221+
raise ValueError(f"Commit must be a valid a commit_hash: {self.commit_hash}.")
222+
223+
if not self.vcs_url:
224+
raise ValueError("Commit must have a non-empty vcs_url.")
225+
226+
def __lt__(self, other):
227+
if not isinstance(other, CodePatchData):
228+
return NotImplemented
229+
return self._cmp_key() < other._cmp_key()
230+
231+
# TODO: Add cache
232+
def _cmp_key(self):
233+
return (
234+
self.commit_hash,
235+
self.vcs_url,
236+
self.commit_patch,
237+
)
238+
239+
def to_dict(self) -> dict:
240+
"""Return a normalized dictionary representation of the commit."""
241+
return {
242+
"commit_hash": self.commit_hash,
243+
"vcs_url": self.vcs_url,
244+
"commit_patch": self.commit_patch,
245+
}
246+
247+
@classmethod
248+
def from_dict(cls, data: dict):
249+
"""Create a Commit instance from a dictionary."""
250+
return cls(
251+
commit_hash=data.get("commit_hash"),
252+
vcs_url=data.get("vcs_url"),
253+
commit_patch=data.get("commit_patch"),
254+
)
255+
256+
197257
class UnMergeablePackageError(Exception):
198258
"""
199259
Raised when a package cannot be merged with another one.
@@ -344,21 +404,28 @@ class AffectedPackageV2:
344404
"""
345405
Relate a Package URL with a range of affected versions and fixed versions.
346406
The Package URL must *not* have a version.
347-
AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range``.
407+
AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range`` or ``introduced_by_commits`` or ``fixed_by_commits``.
348408
"""
349409

350410
package: PackageURL
351411
affected_version_range: Optional[VersionRange] = None
352412
fixed_version_range: Optional[VersionRange] = None
413+
introduced_by_commits: List[CodePatchData] = dataclasses.field(default_factory=list)
414+
fixed_by_commits: List[CodePatchData] = dataclasses.field(default_factory=list)
353415

354416
def __post_init__(self):
355417
if self.package.version:
356418
raise ValueError(f"Affected Package URL {self.package!r} cannot have a version.")
357419

358-
if not (self.affected_version_range or self.fixed_version_range):
420+
if not (
421+
self.affected_version_range
422+
or self.fixed_version_range
423+
or self.introduced_by_commits
424+
or self.fixed_by_commits
425+
):
359426
raise ValueError(
360-
f"Affected Package {self.package!r} should have either fixed version range or an "
361-
"affected version range."
427+
f"Affected package {self.package!r} must have either a fixed version range, "
428+
"an affected version range, introduced commits, or fixed commits."
362429
)
363430

364431
def __lt__(self, other):
@@ -372,6 +439,8 @@ def _cmp_key(self):
372439
str(self.package),
373440
str(self.affected_version_range or ""),
374441
str(self.fixed_version_range or ""),
442+
str(self.introduced_by_commits or []),
443+
str(self.fixed_by_commits or []),
375444
)
376445

377446
def to_dict(self):
@@ -385,6 +454,8 @@ def to_dict(self):
385454
"package": purl_to_dict(self.package),
386455
"affected_version_range": affected_version_range,
387456
"fixed_version_range": fixed_version_range,
457+
"introduced_by_commits": [commit.to_dict() for commit in self.introduced_by_commits],
458+
"fixed_by_commits": [commit.to_dict() for commit in self.fixed_by_commits],
388459
}
389460

390461
@classmethod
@@ -396,6 +467,8 @@ def from_dict(cls, affected_pkg: dict):
396467
fixed_version_range = None
397468
affected_range = affected_pkg["affected_version_range"]
398469
fixed_range = affected_pkg["fixed_version_range"]
470+
introduced_by_commits = affected_pkg.get("introduced_by_commits") or []
471+
fixed_by_commits = affected_pkg.get("fixed_by_commits") or []
399472

400473
try:
401474
affected_version_range = VersionRange.from_string(affected_range)
@@ -417,6 +490,10 @@ def from_dict(cls, affected_pkg: dict):
417490
package=package,
418491
affected_version_range=affected_version_range,
419492
fixed_version_range=fixed_version_range,
493+
introduced_by_commits=[
494+
CodePatchData.from_dict(commit) for commit in introduced_by_commits
495+
],
496+
fixed_by_commits=[CodePatchData.from_dict(commit) for commit in fixed_by_commits],
420497
)
421498

422499

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from vulnerabilities.pipelines import nvd_importer
4242
from vulnerabilities.pipelines import pypa_importer
4343
from vulnerabilities.pipelines import pysec_importer
44+
from vulnerabilities.pipelines.v2_importers import aosp_importer as aosp_importer_v2
4445
from vulnerabilities.pipelines.v2_importers import apache_httpd_importer as apache_httpd_v2
4546
from vulnerabilities.pipelines.v2_importers import archlinux_importer as archlinux_importer_v2
4647
from vulnerabilities.pipelines.v2_importers import curl_importer as curl_importer_v2
@@ -81,6 +82,7 @@
8182
mozilla_importer_v2.MozillaImporterPipeline,
8283
github_osv_importer_v2.GithubOSVImporterPipeline,
8384
redhat_importer_v2.RedHatImporterPipeline,
85+
aosp_importer_v2.AospImporterPipeline,
8486
nvd_importer.NVDImporterPipeline,
8587
github_importer.GitHubAPIImporterPipeline,
8688
gitlab_importer.GitLabImporterPipeline,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2.22 on 2025-11-18 20:45
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0103_codecommit_impactedpackage_affecting_commits_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterUniqueTogether(
14+
name="codecommit",
15+
unique_together={("commit_hash", "vcs_url", "commit_rank")},
16+
),
17+
migrations.AddField(
18+
model_name="codecommit",
19+
name="commit_patch",
20+
field=models.TextField(blank=True, help_text="patch content of the commit.", null=True),
21+
),
22+
migrations.RemoveField(
23+
model_name="codecommit",
24+
name="commit_author",
25+
),
26+
migrations.RemoveField(
27+
model_name="codecommit",
28+
name="commit_date",
29+
),
30+
migrations.RemoveField(
31+
model_name="codecommit",
32+
name="commit_message",
33+
),
34+
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.22 on 2025-11-18 20:46
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0104_alter_codecommit_unique_together_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.RenameModel(
14+
old_name="CodeCommit",
15+
new_name="CodePatch",
16+
),
17+
migrations.RemoveField(
18+
model_name="impactedpackage",
19+
name="affecting_commits",
20+
),
21+
migrations.AddField(
22+
model_name="impactedpackage",
23+
name="introduced_by_commits",
24+
field=models.ManyToManyField(
25+
help_text="Commits introducing this impact.",
26+
related_name="introducing_commits_in_impacts",
27+
to="vulnerabilities.codepatch",
28+
),
29+
),
30+
]

vulnerabilities/models.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,14 +2957,14 @@ 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",
2960+
introduced_by_commits = models.ManyToManyField(
2961+
"CodePatch",
2962+
related_name="introducing_commits_in_impacts",
29632963
help_text="Commits introducing this impact.",
29642964
)
29652965

29662966
fixed_by_commits = models.ManyToManyField(
2967-
"CodeCommit",
2967+
"CodePatch",
29682968
related_name="fixing_commits_in_impacts",
29692969
help_text="Commits fixing this impact.",
29702970
)
@@ -3387,9 +3387,9 @@ def get_known_ransomware_campaign_use_type(self):
33873387
return "Known" if self.known_ransomware_campaign_use else "Unknown"
33883388

33893389

3390-
class CodeCommit(models.Model):
3390+
class CodePatch(models.Model):
33913391
"""
3392-
A CodeCommit Represents a single VCS commit (e.g., Git) related to a ImpactedPackage.
3392+
A CodePatch Represents a single VCS commit (e.g., Git) related to a ImpactedPackage.
33933393
"""
33943394

33953395
commit_hash = models.CharField(max_length=64, help_text="Unique commit identifier (e.g., SHA).")
@@ -3402,15 +3402,7 @@ class CodeCommit(models.Model):
34023402
help_text="Rank of the commit to support ordering by commit. Rank "
34033403
"zero means the rank has not been defined yet",
34043404
)
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-
)
3405+
commit_patch = models.TextField(null=True, blank=True, help_text="patch content of the commit.")
34143406

34153407
class Meta:
3416-
unique_together = ("commit_hash", "vcs_url")
3408+
unique_together = ("commit_hash", "vcs_url", "commit_rank")

0 commit comments

Comments
 (0)