Skip to content
Open
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
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ MarkupSafe==2.1.1
matplotlib-inline==0.1.3
multidict==6.0.2
mypy-extensions==0.4.3
packageurl-python==0.15.6
packageurl-python==0.17.6
packaging==21.3
paramiko==3.4.0
parso==0.8.3
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ install_requires =
drf-spectacular[sidecar]>=0.24.2

#essentials
packageurl-python>=0.15
packageurl-python>=0.17
univers>=30.12.0
license-expression>=30.0.0

Expand Down
85 changes: 81 additions & 4 deletions vulnerabilities/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from vulnerabilities.severity_systems import ScoringSystem
from vulnerabilities.utils import classproperty
from vulnerabilities.utils import get_reference_id
from vulnerabilities.utils import is_commit
from vulnerabilities.utils import is_cve
from vulnerabilities.utils import nearest_patched_package
from vulnerabilities.utils import purl_to_dict
Expand Down Expand Up @@ -194,6 +195,65 @@ def from_url(cls, url):
return cls(url=url)


"""
For VCS URLs that can currently be formed into PURLs (github, bitbucket, and gitlab),
we support full code commit collection.

For any VCS URL types not included in this set, CodeCommit objects will not be
created at this time. Instead, unsupported VCS URLs will be stored only as
references, serving as a fallback until we support them.
"""
VCS_URLS_SUPPORTED_TYPES = {"github", "bitbucket", "gitlab"}


@dataclasses.dataclass(eq=True)
@functools.total_ordering
class CodePatchData:
commit_hash: str
vcs_url: str
commit_patch: Optional[str] = None

def __post_init__(self):
if not self.commit_hash:
raise ValueError("Commit must have a non-empty commit_hash.")

if not is_commit(self.commit_hash):
raise ValueError(f"Commit must be a valid a commit_hash: {self.commit_hash}.")

if not self.vcs_url:
raise ValueError("Commit must have a non-empty vcs_url.")

def __lt__(self, other):
if not isinstance(other, CodePatchData):
return NotImplemented
return self._cmp_key() < other._cmp_key()

# TODO: Add cache
def _cmp_key(self):
return (
self.commit_hash,
self.vcs_url,
self.commit_patch,
)

def to_dict(self) -> dict:
"""Return a normalized dictionary representation of the commit."""
return {
"commit_hash": self.commit_hash,
"vcs_url": self.vcs_url,
"commit_patch": self.commit_patch,
}

@classmethod
def from_dict(cls, data: dict):
"""Create a Commit instance from a dictionary."""
return cls(
commit_hash=data.get("commit_hash"),
vcs_url=data.get("vcs_url"),
commit_patch=data.get("commit_patch"),
)


class UnMergeablePackageError(Exception):
"""
Raised when a package cannot be merged with another one.
Expand Down Expand Up @@ -344,21 +404,28 @@ class AffectedPackageV2:
"""
Relate a Package URL with a range of affected versions and fixed versions.
The Package URL must *not* have a version.
AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range``.
AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range`` or ``introduced_by_commits`` or ``fixed_by_commits``.
"""

package: PackageURL
affected_version_range: Optional[VersionRange] = None
fixed_version_range: Optional[VersionRange] = None
introduced_by_commits: List[CodePatchData] = dataclasses.field(default_factory=list)
fixed_by_commits: List[CodePatchData] = dataclasses.field(default_factory=list)

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

if not (self.affected_version_range or self.fixed_version_range):
if not (
self.affected_version_range
or self.fixed_version_range
or self.introduced_by_commits
or self.fixed_by_commits
):
raise ValueError(
f"Affected Package {self.package!r} should have either fixed version range or an "
"affected version range."
f"Affected package {self.package!r} must have either a fixed version range, "
"an affected version range, introduced commits, or fixed commits."
)

def __lt__(self, other):
Expand All @@ -372,6 +439,8 @@ def _cmp_key(self):
str(self.package),
str(self.affected_version_range or ""),
str(self.fixed_version_range or ""),
str(self.introduced_by_commits or []),
str(self.fixed_by_commits or []),
)

def to_dict(self):
Expand All @@ -385,6 +454,8 @@ def to_dict(self):
"package": purl_to_dict(self.package),
"affected_version_range": affected_version_range,
"fixed_version_range": fixed_version_range,
"introduced_by_commits": [commit.to_dict() for commit in self.introduced_by_commits],
"fixed_by_commits": [commit.to_dict() for commit in self.fixed_by_commits],
}

@classmethod
Expand All @@ -396,6 +467,8 @@ def from_dict(cls, affected_pkg: dict):
fixed_version_range = None
affected_range = affected_pkg["affected_version_range"]
fixed_range = affected_pkg["fixed_version_range"]
introduced_by_commits = affected_pkg.get("introduced_by_commits") or []
fixed_by_commits = affected_pkg.get("fixed_by_commits") or []

try:
affected_version_range = VersionRange.from_string(affected_range)
Expand All @@ -417,6 +490,10 @@ def from_dict(cls, affected_pkg: dict):
package=package,
affected_version_range=affected_version_range,
fixed_version_range=fixed_version_range,
introduced_by_commits=[
CodePatchData.from_dict(commit) for commit in introduced_by_commits
],
fixed_by_commits=[CodePatchData.from_dict(commit) for commit in fixed_by_commits],
)


Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from vulnerabilities.pipelines import nvd_importer
from vulnerabilities.pipelines import pypa_importer
from vulnerabilities.pipelines import pysec_importer
from vulnerabilities.pipelines.v2_importers import aosp_importer as aosp_importer_v2
from vulnerabilities.pipelines.v2_importers import apache_httpd_importer as apache_httpd_v2
from vulnerabilities.pipelines.v2_importers import archlinux_importer as archlinux_importer_v2
from vulnerabilities.pipelines.v2_importers import curl_importer as curl_importer_v2
Expand Down Expand Up @@ -81,6 +82,7 @@
mozilla_importer_v2.MozillaImporterPipeline,
github_osv_importer_v2.GithubOSVImporterPipeline,
redhat_importer_v2.RedHatImporterPipeline,
aosp_importer_v2.AospImporterPipeline,
nvd_importer.NVDImporterPipeline,
github_importer.GitHubAPIImporterPipeline,
gitlab_importer.GitLabImporterPipeline,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.22 on 2025-11-18 20:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0103_codecommit_impactedpackage_affecting_commits_and_more"),
]

operations = [
migrations.AlterUniqueTogether(
name="codecommit",
unique_together={("commit_hash", "vcs_url", "commit_rank")},
),
migrations.AddField(
model_name="codecommit",
name="commit_patch",
field=models.TextField(blank=True, help_text="patch content of the commit.", null=True),
),
migrations.RemoveField(
model_name="codecommit",
name="commit_author",
),
migrations.RemoveField(
model_name="codecommit",
name="commit_date",
),
migrations.RemoveField(
model_name="codecommit",
name="commit_message",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.22 on 2025-11-18 20:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0104_alter_codecommit_unique_together_and_more"),
]

operations = [
migrations.RenameModel(
old_name="CodeCommit",
new_name="CodePatch",
),
migrations.RemoveField(
model_name="impactedpackage",
name="affecting_commits",
),
migrations.AddField(
model_name="impactedpackage",
name="introduced_by_commits",
field=models.ManyToManyField(
help_text="Commits introducing this impact.",
related_name="introducing_commits_in_impacts",
to="vulnerabilities.codepatch",
),
),
]
24 changes: 8 additions & 16 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2957,14 +2957,14 @@ class ImpactedPackage(models.Model):
help_text="Packages vulnerable to this impact.",
)

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

fixed_by_commits = models.ManyToManyField(
"CodeCommit",
"CodePatch",
related_name="fixing_commits_in_impacts",
help_text="Commits fixing this impact.",
)
Expand Down Expand Up @@ -3387,9 +3387,9 @@ def get_known_ransomware_campaign_use_type(self):
return "Known" if self.known_ransomware_campaign_use else "Unknown"


class CodeCommit(models.Model):
class CodePatch(models.Model):
"""
A CodeCommit Represents a single VCS commit (e.g., Git) related to a ImpactedPackage.
A CodePatch 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).")
Expand All @@ -3402,15 +3402,7 @@ class CodeCommit(models.Model):
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."
)
commit_patch = models.TextField(null=True, blank=True, help_text="patch content of the commit.")

class Meta:
unique_together = ("commit_hash", "vcs_url")
unique_together = ("commit_hash", "vcs_url", "commit_rank")
Loading