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
65 changes: 52 additions & 13 deletions vulnerabilities/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from vulnerabilities.models import VulnerabilitySeverity
from vulnerabilities.models import Weakness
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
from vulnerabilities.utils import group_advisories_by_content


class CharInFilter(filters.BaseInFilter, filters.CharFilter):
Expand Down Expand Up @@ -361,19 +362,39 @@ def get_affected_by_vulnerabilities(self, package):

latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
impact_by_avid = {}

result = {}

advisories = []
for impact in impacts:
avid = impact.advisory.avid
advisory = advisory_by_avid.get(avid)
if not advisory:
continue
fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()]
result[advisory.avid] = {
"advisory_id": advisory.avid,
"fixed_by_packages": fixed_by_packages,
}
advisories.append(advisory)
impact_by_avid[avid] = impact

grouped_advisories = group_advisories_by_content(advisories=advisories)

advs = []

for hash in grouped_advisories:
advs.append(grouped_advisories[hash])

result = []

for advisory in advs:
primary_advisory = advisory["primary"]
avid = primary_advisory.avid
impact = impact_by_avid.get(avid)
if not impact:
continue
result.append(
{
"advisory_id": primary_advisory.avid,
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
"duplicate_advisory_ids": [adv.avid for adv in advisory["secondary"]],
}
)

return result

Expand All @@ -384,7 +405,25 @@ def get_fixing_vulnerabilities(self, package):

latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)

return [adv.avid for adv in latest_advisories]
grouped_advisories = group_advisories_by_content(advisories=latest_advisories)

advs = []

for hash in grouped_advisories:
advs.append(grouped_advisories[hash])

result = []

for advisory in advs:
primary_advisory = advisory["primary"]
result.append(
{
"advisory_id": primary_advisory.avid,
"duplicate_advisory_ids": [adv.avid for adv in advisory["secondary"]],
}
)

return result

def get_next_non_vulnerable_version(self, package):
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
Expand Down Expand Up @@ -1078,14 +1117,14 @@ def list(self, request, *args, **kwargs):
return self.get_paginated_response(
{
"packages": serializer.data,
"advisories": advisory_data,
"advisories_by_id": advisory_data,
}
)

return Response(
{
"packages": serializer.data,
"advisories": advisory_data,
"advisories_by_id": advisory_data,
}
)

Expand Down Expand Up @@ -1160,7 +1199,7 @@ def bulk_lookup(self, request):
return Response(
{
"packages": package_data,
"advisories": advisory_data,
"advisories_by_id": advisory_data,
}
)

Expand Down Expand Up @@ -1254,7 +1293,7 @@ def bulk_search(self, request):
return Response(
{
"packages": package_data,
"advisories": advisory_data,
"advisories_by_id": advisory_data,
}
)

Expand Down Expand Up @@ -1308,7 +1347,7 @@ def bulk_search(self, request):
return Response(
{
"packages": package_data,
"advisories": advisory_data,
"advisories_by_id": advisory_data,
}
)

Expand Down
22 changes: 22 additions & 0 deletions vulnerabilities/migrations/0113_advisoryv2_precedence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.25 on 2026-02-10 12:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0112_alter_advisoryseverity_scoring_system_and_more"),
]

operations = [
migrations.AddField(
model_name="advisoryv2",
name="precedence",
field=models.IntegerField(
blank=True,
help_text="Precedence indicates the priority of advisory from different datasources. It is determined based on the reliability of the datasource and how close it is to the source.",
null=True,
),
),
]
62 changes: 49 additions & 13 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import csv
import datetime
import hashlib
import json
import logging
import uuid
import xml.etree.ElementTree as ET
Expand Down Expand Up @@ -69,7 +70,9 @@
from vulnerabilities.severity_systems import EPSS
from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import compute_patch_checksum
from vulnerabilities.utils import normalize_list
from vulnerabilities.utils import normalize_purl
from vulnerabilities.utils import normalize_text
from vulnerabilities.utils import purl_to_dict
from vulnerablecode import __version__ as VULNERABLECODE_VERSION
from vulnerablecode.settings import VULNERABLECODE_PIPELINE_TIMEOUT
Expand Down Expand Up @@ -2988,11 +2991,11 @@ class AdvisoryV2(models.Model):
help_text="Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.",
)

# precedence = models.IntegerField(
# null=True,
# blank=True,
# help_text="Precedence indicates the priority level of addressing a vulnerability based on its overall risk",
# )
precedence = models.IntegerField(
null=True,
blank=True,
help_text="Precedence indicates the priority of advisory from different datasources. It is determined based on the reliability of the datasource and how close it is to the source.",
)

@property
def risk_score(self):
Expand Down Expand Up @@ -3038,16 +3041,20 @@ def to_advisory_data(self) -> "AdvisoryDataV2":

return AdvisoryDataV2(
advisory_id=self.advisory_id,
aliases=[item.alias for item in self.aliases.all()],
aliases=normalize_list([item.alias for item in self.aliases.all()]),
summary=self.summary,
affected_packages=[
impacted.to_affected_package_data() for impacted in self.impacted_packages.all()
],
references=[ref.to_reference_v2_data() for ref in self.references.all()],
patches=[patch.to_patch_data() for patch in self.patches.all()],
affected_packages=normalize_list(
[impacted.to_affected_package_data() for impacted in self.impacted_packages.all()]
),
references=normalize_list(
[ref.to_reference_v2_data() for ref in self.references.all()]
),
patches=normalize_list([patch.to_patch_data() for patch in self.patches.all()]),
date_published=self.date_published,
weaknesses=[weak.cwe_id for weak in self.weaknesses.all()],
severities=[sev.to_vulnerability_severity_data() for sev in self.severities.all()],
weaknesses=normalize_list([weak.cwe_id for weak in self.weaknesses.all()]),
severities=normalize_list(
[sev.to_vulnerability_severity_data() for sev in self.severities.all()]
),
url=self.url,
)

Expand All @@ -3058,6 +3065,35 @@ def get_aliases(self):
"""
return self.aliases.all()

def compute_advisory_content(self):
"""
Compute a unique content hash for an advisory by normalizing its data and hashing it.

:param advisory: An Advisory object
:return: SHA-256 hash digest as content hash
"""
normalized_data = {
"summary": normalize_text(self.summary),
"impacted_packages": sorted(
[impact.to_dict() for impact in self.impacted_packages.all()],
key=lambda x: json.dumps(x, sort_keys=True),
),
"patches": sorted(
[patch.to_patch_data().to_dict() for patch in self.patches.all()],
key=lambda x: json.dumps(x, sort_keys=True),
),
"severities": sorted(
[sev.to_vulnerability_severity_data().to_dict() for sev in self.severities.all()],
key=lambda x: (x.get("system"), x.get("value")),
),
"weaknesses": normalize_list([weakness.cwe_id for weakness in self.weaknesses.all()]),
}

normalized_json = json.dumps(normalized_data, separators=(",", ":"), sort_keys=True)
content_hash = hashlib.sha256(normalized_json.encode("utf-8")).hexdigest()

return content_hash

alias = get_aliases


Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ class VulnerableCodeBaseImporterPipelineV2(VulnerableCodePipeline):
spdx_license_expression = None
repo_url = None
ignorable_versions = []
precedence = 0

# Control how often progress log is shown (range: 1–100, higher value = less frequent log)
progress_step = 10
Expand Down Expand Up @@ -318,6 +319,7 @@ def collect_and_store_advisories(self):
advisory=advisory,
pipeline_id=self.pipeline_id,
logger=self.log,
precedence=self.precedence,
):
collected_advisory_count += 1
except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class AlpineLinuxImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://secdb.alpinelinux.org/license.txt"
repo_url = "git+https://github.com/aboutcode-org/aboutcode-mirror-alpine-secdb/"

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/aosp_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class AospImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
spdx_license_expression = "Apache-2.0"
license_url = "https://github.com/quarkslab/aosp_dataset/blob/master/LICENSE"

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ class ApacheHTTPDImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://www.apache.org/licenses/LICENSE-2.0"
base_url = "https://httpd.apache.org/security/json/"

precedence = 200

links = []

ignorable_versions = frozenset(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class ApacheKafkaImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
"CVE-2021-4104",
]

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class ApacheTomcatImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://www.apache.org/licenses/LICENSE-2.0"
base_url = "https://tomcat.apache.org/security"

precedence = 200

def fetch_advisory_links(self):
"""
Yield the URLs of each Tomcat version security-related page.
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/archlinux_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class ArchLinuxImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
spdx_license_expression = "MIT"
license_url = "https://github.com/archlinux/arch-security-tracker/blob/master/LICENSE"

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/curl_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class CurlImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
repo_url = "https://github.com/curl/curl-www/"
url = "https://curl.se/docs/vuln.json"

precedence = 200

@classmethod
def steps(cls):
return (cls.collect_and_store_advisories,)
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/debian_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class DebianImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
Moritz
"""

precedence = 200

api_url = "https://security-tracker.debian.org/tracker/data/json"
response = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class ElixirSecurityImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://github.com/dependabot/elixir-security-advisories/blob/master/LICENSE.txt"
repo_url = "git+https://github.com/dependabot/elixir-security-advisories"

precedence = 200

@classmethod
def steps(cls):
return (cls.clone, cls.collect_and_store_advisories, cls.clean_downloads)
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/epss_importer_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class EPSSImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
spdx_license_expression = "unknown"
importer_name = "EPSS Importer"

precedence = 200

def advisories_count(self):
return len(self.lines)

Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/fireeye_importer_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class FireeyeImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
repo_url = "git+https://github.com/mandiant/Vulnerability-Disclosures"
pipeline_id = "fireeye_importer_v2"

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/github_osv_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class GithubOSVImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://github.com/github/advisory-database/blob/main/LICENSE.md"
repo_url = "git+https://github.com/github/advisory-database/"

precedence = 100

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/gitlab_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class GitLabImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://gitlab.com/gitlab-org/advisories-community/-/blob/main/LICENSE"
repo_url = "git+https://gitlab.com/gitlab-org/advisories-community/"

precedence = 100

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/istio_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class IstioImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
license_url = "https://github.com/istio/istio.io/blob/master/LICENSE"
repo_url = "git+https://github.com/istio/istio.io"

precedence = 200

@classmethod
def steps(cls):
return (
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/pipelines/v2_importers/mattermost_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class MattermostImporterPipeline(VulnerableCodeBaseImporterPipelineV2):

_cached_data = None # Class-level cache

precedence = 200

@classmethod
def steps(cls):
return (cls.collect_and_store_advisories,)
Expand Down
Loading
Loading