diff --git a/component_catalog/api.py b/component_catalog/api.py
index abd8705f..5c3d8764 100644
--- a/component_catalog/api.py
+++ b/component_catalog/api.py
@@ -630,7 +630,7 @@ class PackageSerializer(
read_only=True,
many=True,
fields=[
- "vulnerability_id",
+ "advisory_uid",
"api_url",
"uuid",
],
diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html
index 9fa439bd..1613caa8 100644
--- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html
+++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html
@@ -51,11 +51,11 @@
{% if vulnerability.resource_url %}
- {{ vulnerability.vulnerability_id }}
+ {{ vulnerability.advisory_id }}
{% else %}
- {{ vulnerability.vulnerability_id }}
+ {{ vulnerability.advisory_id }}
{% endif %}
diff --git a/component_catalog/tests/test_api.py b/component_catalog/tests/test_api.py
index afd60a0a..cfa8cb20 100644
--- a/component_catalog/tests/test_api.py
+++ b/component_catalog/tests/test_api.py
@@ -1351,7 +1351,7 @@ def test_api_package_endpoint_vulnerabilities_features(self):
self.assertEqual("9.0", results[0]["risk_score"])
self.assertEqual(
vulnerability1.vulnerability_id,
- results[0]["affected_by_vulnerabilities"][0]["vulnerability_id"],
+ results[0]["affected_by_vulnerabilities"][0]["advisory_uid"],
)
data = {"affected_by": vulnerability1.vulnerability_id}
diff --git a/component_catalog/tests/test_views.py b/component_catalog/tests/test_views.py
index 5efafcb1..9bfbdf55 100644
--- a/component_catalog/tests/test_views.py
+++ b/component_catalog/tests/test_views.py
@@ -1054,7 +1054,7 @@ def test_component_details_view_tab_vulnerabilities(self):
)
self.assertContains(response, expected)
self.assertContains(response, 'id="tab_vulnerabilities"')
- self.assertContains(response, vulnerability1.vcid)
+ self.assertContains(response, vulnerability1.vulnerability_id)
def test_component_catalog_component_create_ajax_view(self):
component_create_ajax_url = reverse("component_catalog:component_add_ajax")
@@ -3020,7 +3020,7 @@ def test_package_details_view_tab_vulnerabilities(self):
)
self.assertContains(response, expected)
self.assertContains(response, 'id="tab_vulnerabilities"')
- self.assertContains(response, self.vulnerability1.vcid)
+ self.assertContains(response, self.vulnerability1.vulnerability_id)
def test_vulnerablecode_get_plain_purls(self):
purls = get_plain_purls(packages=[])
@@ -3064,37 +3064,6 @@ def test_vulnerablecode_get_vulnerable_purls(self):
vulnerable_purls = vulnerablecode.get_vulnerable_purls(packages=[self.package1])
self.assertEqual(["pkg:pypi/django@2.1"], vulnerable_purls)
- def test_vulnerablecode_get_vulnerable_cpes(self):
- vulnerablecode = VulnerableCode(self.dataspace)
- vulnerable_cpes = vulnerablecode.get_vulnerable_cpes(components=[])
- self.assertEqual([], vulnerable_cpes)
-
- components = [self.component1, self.component2]
- vulnerable_cpes = vulnerablecode.get_vulnerable_cpes(components=components)
- self.assertEqual([], vulnerable_cpes)
-
- self.component1.cpe = "cpe:2.3:a:djangoproject:django:0.95:*:*:*:*:*:*:*"
- self.component1.save()
-
- with mock.patch(
- "dejacode_toolkit.vulnerablecode.VulnerableCode.bulk_search_by_cpes"
- ) as bulk_search:
- bulk_search.return_value = [
- {
- "vulnerability_id": "VCID-188m-1bke-aaae",
- "summary": "The administrative interface in django.contrib.admin ",
- "references": [
- {"reference_id": ""},
- ],
- }
- ]
- vulnerable_cpes = vulnerablecode.get_vulnerable_cpes(components=components)
- self.assertEqual([], vulnerable_cpes)
-
- bulk_search.return_value[0]["references"] = [{"reference_id": self.component1.cpe}]
- vulnerable_cpes = vulnerablecode.get_vulnerable_cpes(components=components)
- self.assertEqual([self.component1.cpe], vulnerable_cpes)
-
@mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.request_get")
def test_vulnerablecode_get_vulnerabilities_cache(self, mock_request_get):
vulnerablecode = VulnerableCode(self.dataspace)
diff --git a/dejacode_toolkit/__init__.py b/dejacode_toolkit/__init__.py
index aa246c63..f6f43760 100644
--- a/dejacode_toolkit/__init__.py
+++ b/dejacode_toolkit/__init__.py
@@ -27,7 +27,7 @@ def get_settings(var_name, default=None):
def is_service_available(label, session, url, raise_exceptions):
"""Check if a configured integration service is available."""
try:
- response = session.head(url, timeout=REQUESTS_TIMEOUT)
+ response = session.head(url, allow_redirects=True, timeout=REQUESTS_TIMEOUT)
response.raise_for_status()
except requests.exceptions.RequestException as request_exception:
logger.debug(f"{label} is_available() error: {request_exception}")
@@ -43,6 +43,7 @@ class BaseService:
settings_prefix = None
url_field_name = None
api_key_field_name = None
+ api_version = None
default_timeout = REQUESTS_TIMEOUT
def __init__(self, dataspace):
@@ -71,6 +72,9 @@ def __init__(self, dataspace):
self.api_url = f"{self.service_url.rstrip('/')}/api/"
+ if self.api_version:
+ self.api_url = f"{self.api_url}{self.api_version.rstrip('/')}/"
+
def get_session(self):
session = requests.Session()
diff --git a/dejacode_toolkit/vulnerablecode.py b/dejacode_toolkit/vulnerablecode.py
index 34980dcf..82a2e581 100644
--- a/dejacode_toolkit/vulnerablecode.py
+++ b/dejacode_toolkit/vulnerablecode.py
@@ -19,87 +19,44 @@ class VulnerableCode(BaseService):
settings_prefix = "VULNERABLECODE"
url_field_name = "vulnerablecode_url"
api_key_field_name = "vulnerablecode_api_key"
+ api_version = "v3"
- def get_vulnerabilities(
+ def get_vulnerabilities_by_purl(
self,
- url,
- field_name,
- field_value,
+ purl,
timeout=None,
):
- """Get list of vulnerabilities."""
- cached_results = cache.get(field_value)
+ """Get list of vulnerabilities providing a package `purl`."""
+ plain_purl = get_plain_purl(purl)
+
+ cached_results = cache.get(plain_purl)
if cached_results:
return cached_results
- payload = {field_name: field_value}
-
- response = self.request_get(url=url, params=payload, timeout=timeout)
+ response = self.bulk_search_by_purl(purls=[plain_purl], timeout=timeout)
if response and response.get("count"):
results = response["results"]
- cache.set(field_value, results)
+ cache.set(plain_purl, results)
return results
- def get_vulnerabilities_by_purl(
- self,
- purl,
- timeout=None,
- ):
- """Get list of vulnerabilities providing a package `purl`."""
- return self.get_vulnerabilities(
- url=f"{self.api_url}packages/",
- field_name="purl",
- field_value=get_plain_purl(purl),
- timeout=timeout,
- )
-
- def get_vulnerabilities_by_cpe(
- self,
- cpe,
- timeout=None,
- ):
- """Get list of vulnerabilities providing a package or component `cpe`."""
- return self.get_vulnerabilities(
- url=f"{self.api_url}cpes/",
- field_name="cpe",
- field_value=cpe,
- timeout=timeout,
- )
-
def bulk_search_by_purl(
self,
purls,
- purl_only,
+ details=True,
timeout=None,
):
"""Bulk search of vulnerabilities using the provided list of `purls`."""
- url = f"{self.api_url}packages/bulk_search"
+ url = f"{self.api_url}packages"
data = {
"purls": purls,
- "purl_only": purl_only,
- "plain_purl": True,
+ "details": details,
}
logger.debug(f"VulnerableCode: url={url} purls_count={len(purls)}")
return self.request_post(url=url, json=data, timeout=timeout)
- def bulk_search_by_cpes(
- self,
- cpes,
- timeout=None,
- ):
- """Bulk search of vulnerabilities using the provided list of `cpes`."""
- url = f"{self.api_url}cpes/bulk_search"
-
- data = {
- "cpes": cpes,
- }
-
- logger.debug(f"VulnerableCode: url={url} cpes_count={len(cpes)}")
- return self.request_post(url, json=data, timeout=timeout)
-
- def get_vulnerable_purls(self, packages, purl_only=True, timeout=10):
+ def get_vulnerable_purls(self, packages, details=False, timeout=10):
"""
Return a list of PURLs for which at least one `affected_by_vulnerabilities`
was found in the VulnerableCodeDB for the given list of `packages`.
@@ -110,34 +67,11 @@ def get_vulnerable_purls(self, packages, purl_only=True, timeout=10):
return []
vulnerable_purls = self.bulk_search_by_purl(
- plain_purls,
- purl_only=purl_only,
+ purls=plain_purls,
+ details=details,
timeout=timeout,
)
- return vulnerable_purls or []
-
- def get_vulnerable_cpes(self, components):
- """
- Return a list of vulnerable CPEs found in the VulnerableCodeDB for the given
- list of `components`.
- """
- cpes = [component.cpe for component in components if component.cpe]
-
- if not cpes:
- return []
-
- search_results = self.bulk_search_by_cpes(cpes)
- if not search_results:
- return []
-
- vulnerable_cpes = [
- reference.get("reference_id")
- for entry in search_results
- for reference in entry.get("references")
- if reference.get("reference_id").startswith("cpe")
- ]
-
- return list(set(vulnerable_cpes))
+ return vulnerable_purls.get("results") or []
def get_package_url_available_types(self):
# Replace by fetching the endpoint once available.
diff --git a/product_portfolio/templates/product_portfolio/compliance/compliance_watchlist_card.html b/product_portfolio/templates/product_portfolio/compliance/compliance_watchlist_card.html
index 2e1bdb4d..73ad45c3 100644
--- a/product_portfolio/templates/product_portfolio/compliance/compliance_watchlist_card.html
+++ b/product_portfolio/templates/product_portfolio/compliance/compliance_watchlist_card.html
@@ -44,7 +44,7 @@
diff --git a/product_portfolio/tests/test_api.py b/product_portfolio/tests/test_api.py
index 88b50bcd..c60a0356 100644
--- a/product_portfolio/tests/test_api.py
+++ b/product_portfolio/tests/test_api.py
@@ -1157,7 +1157,7 @@ def test_api_productpackage_endpoint_vulnerabilities_features(self):
response = self.client.get(self.pp1_detail_url)
response_analysis = response.data["vulnerability_analyses"][0]
- self.assertEqual(vulnerability1.vulnerability_id, response_analysis["vulnerability_id"])
+ self.assertEqual(vulnerability1.advisory_uid, response_analysis["advisory_uid"])
self.assertEqual(analysis1.state, response_analysis["state"])
self.assertEqual(analysis1.justification, response_analysis["justification"])
@@ -1189,7 +1189,7 @@ def test_api_product_endpoint_vulnerabilities_features(self):
response = self.client.get(self.product1_detail_url)
response_analysis = response.data["vulnerability_analyses"][0]
- self.assertEqual(vulnerability1.vulnerability_id, response_analysis["vulnerability_id"])
+ self.assertEqual(vulnerability1.advisory_uid, response_analysis["advisory_uid"])
self.assertEqual(analysis1.state, response_analysis["state"])
self.assertEqual(analysis1.justification, response_analysis["justification"])
diff --git a/product_portfolio/tests/test_views.py b/product_portfolio/tests/test_views.py
index c4eb4ba2..8fc46d0e 100644
--- a/product_portfolio/tests/test_views.py
+++ b/product_portfolio/tests/test_views.py
@@ -328,9 +328,9 @@ def test_product_portfolio_tab_vulnerability_view_packages_row_rendering(self):
expected = f"""
@@ -375,15 +375,15 @@ def test_product_portfolio_tab_vulnerability_risk_threshold(self):
url = product1.get_url("tab_vulnerabilities")
response = self.client.get(url)
- self.assertContains(response, vulnerability1.vcid)
- self.assertContains(response, vulnerability2.vcid)
+ self.assertContains(response, vulnerability1.vulnerability_id)
+ self.assertContains(response, vulnerability2.vulnerability_id)
self.assertContains(response, "2 results")
self.assertNotContains(response, "A risk threshold filter at")
product1.update(vulnerabilities_risk_threshold=3.0)
response = self.client.get(url)
- self.assertNotContains(response, vulnerability1.vcid)
- self.assertContains(response, vulnerability2.vcid)
+ self.assertNotContains(response, vulnerability1.vulnerability_id)
+ self.assertContains(response, vulnerability2.vulnerability_id)
self.assertContains(response, "1 results")
self.assertContains(response, 'A risk threshold filter at "3.0" is currently applied.')
@@ -4153,7 +4153,7 @@ def test_product_portfolio_product_security_compliance_export_view_json(self):
data = json.loads(response.content)
self.assertEqual(1, len(data))
- self.assertEqual(vulnerability.vulnerability_id, data[0]["vulnerability_id"])
+ self.assertEqual(vulnerability.advisory_uid, data[0]["advisory_uid"])
# Aliases stay a real list in JSON, not a comma-joined string.
self.assertEqual(
["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"],
@@ -4215,8 +4215,8 @@ def test_product_portfolio_product_security_compliance_export_view_yaml(self):
self.assertIn(".yaml", response["Content-Disposition"])
content = response.content.decode()
- self.assertIn(vulnerability.vulnerability_id, content)
- self.assertIn("vulnerability_id", content)
+ self.assertIn(vulnerability.advisory_uid, content)
+ self.assertIn("advisory_uid", content)
def test_product_portfolio_product_security_compliance_export_view_respects_permissions(self):
self.client.login(username=self.basic_user.username, password="secret")
@@ -4239,5 +4239,5 @@ def test_product_portfolio_product_security_compliance_export_view_respects_perm
response = self.client.get(url + "?export=json")
self.assertEqual(200, response.status_code)
data = json.loads(response.content)
- vulnerability_ids = [entry["vulnerability_id"] for entry in data]
- self.assertIn(vulnerability.vulnerability_id, vulnerability_ids)
+ vulnerability_ids = [entry["advisory_uid"] for entry in data]
+ self.assertIn(vulnerability.advisory_uid, vulnerability_ids)
diff --git a/product_portfolio/views.py b/product_portfolio/views.py
index 5bcd9d09..6c215184 100644
--- a/product_portfolio/views.py
+++ b/product_portfolio/views.py
@@ -1208,7 +1208,7 @@ class ProductTabVulnerabilitiesView(
Header("affected_packages", _("Package"), help_text="Affected product packages"),
Header("weighted_risk_score", _("Risk"), filter="weighted_risk_score"),
Header(
- "vulnerability_id",
+ "advisory_uid",
_("Vulnerabilities"),
help_text="Vulnerabilities affecting the product package",
),
@@ -3180,7 +3180,7 @@ def get_context_data(self, **kwargs):
),
)
.filter(product_count__gt=0)
- .order_by("-risk_score", "-product_count", "vulnerability_id")
+ .order_by("-risk_score", "-product_count", "advisory_id")
)
context["vulnerabilities_qs"] = vulnerabilities[: self.limit]
@@ -3232,7 +3232,7 @@ class ProductSecurityComplianceExportView(
export_filename = "security_compliance"
export_fields = {
- "vulnerability_id": "Vulnerability ID",
+ "advisory_uid": "Vulnerability ID",
"aliases": "Aliases",
"summary": "Summary",
"risk_level": "Risk level",
diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py
index b1c62f9e..1d11928d 100644
--- a/vulnerabilities/api.py
+++ b/vulnerabilities/api.py
@@ -36,14 +36,16 @@ class Meta:
fields = (
"api_url",
"uuid",
- "vulnerability_id",
+ "advisory_uid",
+ "advisory_id",
"resource_url",
"summary",
"aliases",
- "references",
"exploitability",
"weighted_severity",
"risk_score",
+ "risk_level",
+ "fixed_packages",
"affected_packages",
"affected_products",
)
@@ -105,7 +107,7 @@ class VulnerabilityViewSet(ExtraPermissionsViewSetMixin, viewsets.ReadOnlyModelV
lookup_field = "uuid"
filterset_class = VulnerabilityFilterSet
extra_permissions = (TabPermission,)
- search_fields = ("vulnerability_id", "aliases")
+ search_fields = ("advisory_uid", "aliases")
ordering_fields = (
"exploitability",
"weighted_severity",
@@ -130,7 +132,7 @@ def get_queryset(self):
class VulnerabilityAnalysisSerializer(DataspacedSerializer, serializers.ModelSerializer):
- vulnerability_id = serializers.ReadOnlyField(source="vulnerability.vulnerability_id")
+ advisory_uid = serializers.ReadOnlyField(source="vulnerability.advisory_uid")
class Meta:
model = VulnerabilityAnalysis
@@ -139,7 +141,7 @@ class Meta:
"uuid",
"product_package",
"vulnerability",
- "vulnerability_id",
+ "advisory_uid",
"state",
"justification",
"responses",
diff --git a/vulnerabilities/fetch.py b/vulnerabilities/fetch.py
index 7f286ed0..94b45497 100644
--- a/vulnerabilities/fetch.py
+++ b/vulnerabilities/fetch.py
@@ -14,6 +14,8 @@
from django.urls import reverse
from django.utils import timezone
+from packageurl import PackageURL
+
from component_catalog.models import PACKAGE_URL_FIELDS
from component_catalog.models import Package
from dejacode_toolkit.vulnerablecode import VulnerableCode
@@ -84,20 +86,21 @@ def fetch_for_packages(
log_func(f"Progress: {intcomma(progress_count)}/{intcomma(object_count)}")
batch_affected_packages = []
- vc_entries = vulnerablecode.get_vulnerable_purls(batch, purl_only=False, timeout=timeout)
+ vc_entries = vulnerablecode.get_vulnerable_purls(batch, details=True, timeout=timeout)
for vc_entry in vc_entries:
affected_by_vulnerabilities = vc_entry.get("affected_by_vulnerabilities")
if not affected_by_vulnerabilities:
continue
+ purl = PackageURL.from_string(vc_entry.get("purl"))
affected_packages = queryset.filter(
- type=vc_entry.get("type"),
- namespace=vc_entry.get("namespace") or "",
- name=vc_entry.get("name"),
- version=vc_entry.get("version") or "",
+ type=purl.type,
+ namespace=purl.namespace,
+ name=purl.name,
+ version=purl.version,
)
if not affected_packages:
- raise CommandError("Could not find package!")
+ raise CommandError("Could not find packages!")
# Store all packages of that batch to then trigger the update_weighted_risk_score
batch_affected_packages.extend(affected_packages)
@@ -119,9 +122,9 @@ def fetch_for_packages(
def create_or_update_vulnerability(
vulnerability_data, dataspace, affected_packages, update, results
):
- vulnerability_id = vulnerability_data["vulnerability_id"]
+ advisory_uid = vulnerability_data["advisory_uid"]
vulnerability_qs = Vulnerability.objects.scope(dataspace)
- vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id)
+ vulnerability = vulnerability_qs.get_or_none(advisory_uid=advisory_uid)
if not vulnerability:
vulnerability = Vulnerability.create_from_data(
diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py
index e63a20a2..6cc5dd6b 100644
--- a/vulnerabilities/filters.py
+++ b/vulnerabilities/filters.py
@@ -75,7 +75,7 @@ class VulnerabilityFilterSet(DataspacedFilterSet):
]
q = SearchFilter(
label=_("Search"),
- search_fields=["vulnerability_id", "aliases"],
+ search_fields=["advisory_uid", "aliases"],
)
sort = NullsLastOrderingFilter(
label=_("Sort"),
diff --git a/vulnerabilities/migrations/0008_purge_and_add_advisory_fields.py b/vulnerabilities/migrations/0008_purge_and_add_advisory_fields.py
new file mode 100644
index 00000000..86f8259d
--- /dev/null
+++ b/vulnerabilities/migrations/0008_purge_and_add_advisory_fields.py
@@ -0,0 +1,64 @@
+# Generated by Django 6.0.4 on 2026-06-10
+
+from django.db import migrations, models
+
+
+def purge_vulnerabilities(apps, schema_editor):
+ """Purge all Vulnerability rows before adding non-nullable advisory fields."""
+ Vulnerability = apps.get_model("vulnerabilities", "Vulnerability")
+ Vulnerability.objects.all().delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("vulnerabilities", "0007_vulnerability_vulnerabili_exploit_f83324_idx_and_more"),
+ ]
+
+ operations = [
+ migrations.RunPython(purge_vulnerabilities, migrations.RunPython.noop),
+ # Remove old unique_together referencing vulnerability_id before dropping the field.
+ migrations.AlterUniqueTogether(
+ name="vulnerability",
+ unique_together={("dataspace", "uuid")},
+ ),
+ migrations.RemoveIndex(
+ model_name="vulnerability",
+ name="vulnerabili_vulnera_92f044_idx",
+ ),
+ migrations.RemoveField(
+ model_name="vulnerability",
+ name="vulnerability_id",
+ ),
+ migrations.RemoveField(
+ model_name="vulnerability",
+ name="references",
+ ),
+ migrations.AddField(
+ model_name="vulnerability",
+ name="advisory_uid",
+ field=models.CharField(
+ help_text="Unique ID for the datasource used for this advisory ."
+ "e.g.: pysec_importer_v2/PYSEC-2020-2233",
+ max_length=250,
+ ),
+ ),
+ migrations.AddField(
+ model_name="vulnerability",
+ name="advisory_id",
+ field=models.CharField(
+ help_text="An advisory is a vulnerability identifier in some database, "
+ "such as PYSEC-2020-2233",
+ max_length=200,
+ ),
+ ),
+ # Restore unique_together with the new advisory_uid field.
+ migrations.AlterUniqueTogether(
+ name="vulnerability",
+ unique_together={("dataspace", "uuid"), ("dataspace", "advisory_uid")},
+ ),
+ migrations.AddIndex(
+ model_name="vulnerability",
+ index=models.Index(fields=["advisory_id"], name="vulnerabili_advisor_7de61b_idx"),
+ ),
+ ]
diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py
index a1b54aa0..bcb04d84 100644
--- a/vulnerabilities/models.py
+++ b/vulnerabilities/models.py
@@ -6,7 +6,6 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#
-import decimal
import logging
from django.contrib.postgres.fields import ArrayField
@@ -78,6 +77,8 @@ def added_or_updated_today(self):
return self.filter(last_modified_date__gte=today)
+# AdvisoryV2
+# https://github.com/aboutcode-org/vulnerablecode/blob/api-integration-dejacode/vulnerabilities/models.py#L3126
class Vulnerability(HistoryDateFieldsMixin, DataspacedModel):
"""
A software vulnerability with a unique identifier and alternate aliases.
@@ -90,12 +91,17 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel):
automatically on object addition or during schedule tasks.
"""
- # The first set of fields are storing data as fetched from VulnerableCode
- vulnerability_id = models.CharField(
- max_length=20,
+ advisory_uid = models.CharField(
+ max_length=250,
help_text=_(
- "A unique identifier for the vulnerability, prefixed with 'VCID-'. "
- "For example, 'VCID-2024-0001'."
+ "Unique ID for the datasource used for this advisory ."
+ "e.g.: pysec_importer_v2/PYSEC-2020-2233"
+ ),
+ )
+ advisory_id = models.CharField(
+ max_length=200,
+ help_text=_(
+ "An advisory is a vulnerability identifier in some database, such as PYSEC-2020-2233"
),
)
resource_url = models.URLField(
@@ -115,13 +121,6 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel):
"(e.g., 'CVE-2017-1000136')."
),
)
- references = JSONListField(
- blank=True,
- help_text=_(
- "A list of references for this vulnerability. Each reference includes a "
- "URL, an optional reference ID, scores, and the URL for further details. "
- ),
- )
fixed_packages = JSONListField(
blank=True,
help_text=_("A list of packages that are not affected by this vulnerability."),
@@ -188,9 +187,9 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel):
class Meta:
verbose_name_plural = "Vulnerabilities"
- unique_together = (("dataspace", "vulnerability_id"), ("dataspace", "uuid"))
+ unique_together = (("dataspace", "advisory_uid"), ("dataspace", "uuid"))
indexes = [
- models.Index(fields=["vulnerability_id"]),
+ models.Index(fields=["advisory_id"]),
models.Index(fields=["exploitability"]),
models.Index(fields=["weighted_severity"]),
models.Index(fields=["risk_score"]),
@@ -201,8 +200,8 @@ def __str__(self):
return self.vulnerability_id
@property
- def vcid(self):
- return self.vulnerability_id
+ def vulnerability_id(self):
+ return self.advisory_id
@property
def cve(self):
@@ -235,12 +234,11 @@ def get_or_create_from_data(cls, dataspace, data, validate=False):
# Support for CycloneDX data structure
data = data.copy()
- vulnerability_id = data.get("vulnerability_id") or data.pop("id", None)
- if not vulnerability_id:
+ advisory_uid = data.get("advisory_uid") or data.pop("id", None)
+ if not advisory_uid:
return
- data["vulnerability_id"] = vulnerability_id
- vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id)
+ vulnerability = vulnerability_qs.get_or_none(advisory_uid=advisory_uid)
if not vulnerability:
vulnerability = cls.create_from_data(
dataspace=dataspace,
@@ -263,51 +261,51 @@ def as_cyclonedx(self, affected_instances, analysis=None):
url=self.resource_url,
)
- references = []
- ratings = []
- for reference in self.references:
- reference_source = cdx_vulnerability.VulnerabilitySource(
- url=reference.get("reference_url"),
- )
- references.append(
- cdx_vulnerability.VulnerabilityReference(
- id=reference.get("reference_id"),
- source=reference_source,
- )
- )
-
- for score_entry in reference.get("scores", []):
- # CycloneDX only support a float value for the score field,
- # where on the VulnerableCode data it can be either a score float value
- # or a severity string value.
- score_value = score_entry.get("value")
- try:
- score = decimal.Decimal(score_value)
- severity = None
- except decimal.DecimalException:
- score = None
- severity = getattr(
- cdx_vulnerability.VulnerabilitySeverity,
- score_value.upper(),
- None,
- )
-
- ratings.append(
- cdx_vulnerability.VulnerabilityRating(
- source=reference_source,
- score=score,
- severity=severity,
- vector=score_entry.get("scoring_elements"),
- )
- )
+ # references = []
+ # ratings = []
+ # for reference in self.references:
+ # reference_source = cdx_vulnerability.VulnerabilitySource(
+ # url=reference.get("reference_url"),
+ # )
+ # references.append(
+ # cdx_vulnerability.VulnerabilityReference(
+ # id=reference.get("reference_id"),
+ # source=reference_source,
+ # )
+ # )
+
+ # for score_entry in reference.get("scores", []):
+ # # CycloneDX only support a float value for the score field,
+ # # where on the VulnerableCode data it can be either a score float value
+ # # or a severity string value.
+ # score_value = score_entry.get("value")
+ # try:
+ # score = decimal.Decimal(score_value)
+ # severity = None
+ # except decimal.DecimalException:
+ # score = None
+ # severity = getattr(
+ # cdx_vulnerability.VulnerabilitySeverity,
+ # score_value.upper(),
+ # None,
+ # )
+
+ # ratings.append(
+ # cdx_vulnerability.VulnerabilityRating(
+ # source=reference_source,
+ # score=score,
+ # severity=severity,
+ # vector=score_entry.get("scoring_elements"),
+ # )
+ # )
return cdx_vulnerability.Vulnerability(
id=self.vulnerability_id,
source=source,
description=self.summary,
affects=affects,
- references=sorted(references),
- ratings=ratings,
+ # references=sorted(references),
+ # ratings=ratings,
analysis=analysis,
)
@@ -491,16 +489,7 @@ def get_entry_for_package(self, vulnerablecode):
affected_by_vulnerabilities = vulnerable_packages[0].get("affected_by_vulnerabilities")
return affected_by_vulnerabilities
- def get_entry_for_component(self, vulnerablecode):
- if not self.cpe:
- return
-
- # Support for Component is paused as the CPES endpoint do not work properly.
- # https://github.com/aboutcode-org/vulnerablecode/issues/1557
- # vulnerabilities = vulnerablecode.get_vulnerabilities_by_cpe(self.cpe, timeout=10)
-
def get_entry_from_vulnerablecode(self):
- from component_catalog.models import Component
from component_catalog.models import Package
from dejacode_toolkit.vulnerablecode import VulnerableCode
@@ -516,8 +505,6 @@ def get_entry_from_vulnerablecode(self):
if not is_vulnerablecode_enabled:
return
- if isinstance(self, Component):
- return self.get_entry_for_component(vulnerablecode)
elif isinstance(self, Package):
return self.get_entry_for_package(vulnerablecode)
diff --git a/vulnerabilities/tests/__init__.py b/vulnerabilities/tests/__init__.py
index 15888df9..076e8ce8 100644
--- a/vulnerabilities/tests/__init__.py
+++ b/vulnerabilities/tests/__init__.py
@@ -13,8 +13,8 @@
def make_vulnerability(dataspace, affecting=None, **data):
"""Create a vulnerability for test purposes."""
- if "vulnerability_id" not in data:
- data["vulnerability_id"] = f"VCID-0000-{make_string(4)}"
+ if "advisory_uid" not in data:
+ data["advisory_uid"] = f"VCID-0000-{make_string(4)}"
vulnerability = Vulnerability.objects.create(
dataspace=dataspace,
diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py
index 82171c1f..fb491b10 100644
--- a/vulnerabilities/tests/test_api.py
+++ b/vulnerabilities/tests/test_api.py
@@ -63,11 +63,11 @@ def test_api_vulnerabilities_list_endpoint_results(self):
# Ordered by risk_score
expected = [
- self.vulnerability3.vulnerability_id,
- self.vulnerability2.vulnerability_id,
- self.vulnerability1.vulnerability_id,
+ self.vulnerability3.advisory_uid,
+ self.vulnerability2.advisory_uid,
+ self.vulnerability1.advisory_uid,
]
- self.assertEqual(expected, [entry["vulnerability_id"] for entry in results])
+ self.assertEqual(expected, [entry["advisory_uid"] for entry in results])
self.assertEqual(str(self.package1), results[2]["affected_packages"][0]["display_name"])
self.assertEqual(str(self.product1), results[2]["affected_products"][0]["display_name"])
@@ -75,12 +75,12 @@ def test_api_vulnerabilities_list_endpoint_results(self):
def test_api_vulnerabilities_list_endpoint_search(self):
self.client.login(username="super_user", password="secret")
- data = {"search": self.vulnerability1.vulnerability_id}
+ data = {"search": self.vulnerability1.advisory_uid}
response = self.client.get(self.vulnerabilities_list_url, data)
self.assertEqual(1, response.data["count"])
- self.assertContains(response, self.vulnerability1.vulnerability_id)
- self.assertNotContains(response, self.vulnerability2.vulnerability_id)
- self.assertNotContains(response, self.vulnerability3.vulnerability_id)
+ self.assertContains(response, self.vulnerability1.advisory_uid)
+ self.assertNotContains(response, self.vulnerability2.advisory_uid)
+ self.assertNotContains(response, self.vulnerability3.advisory_uid)
def test_api_vulnerabilities_list_endpoint_filters(self):
self.client.login(username="super_user", password="secret")
@@ -101,7 +101,7 @@ def test_api_vulnerabilities_detail_endpoint(self):
self.assertContains(response, detail_url)
self.assertIn(detail_url, response.data["api_url"])
- self.assertEqual(self.vulnerability1.vulnerability_id, response.data["vulnerability_id"])
+ self.assertEqual(self.vulnerability1.advisory_uid, response.data["advisory_uid"])
self.assertEqual(str(self.vulnerability1.uuid), response.data["uuid"])
self.assertEqual("0.0", response.data["risk_score"])
self.assertEqual(1, len(response.data["affected_packages"]))
@@ -150,7 +150,7 @@ def test_api_vulnerability_analysis_detail_endpoint(self):
self.assertContains(response, detail_url)
self.assertIn(detail_url, response.data["api_url"])
- self.assertEqual(self.vulnerability1.vulnerability_id, response.data["vulnerability_id"])
+ self.assertEqual(self.vulnerability1.advisory_uid, response.data["advisory_uid"])
self.assertEqual(str(analysis1.uuid), response.data["uuid"])
self.assertTrue(response.data["is_reachable"])
@@ -185,7 +185,7 @@ def test_api_vulnerability_analysis_endpoint_create(self):
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.assertIn(product_package1_detail_url, response.data["product_package"])
self.assertIn(vulnerability1_detail_url, response.data["vulnerability"])
- self.assertEqual(self.vulnerability1.vulnerability_id, response.data["vulnerability_id"])
+ self.assertEqual(self.vulnerability1.advisory_uid, response.data["advisory_uid"])
self.assertEqual("resolved", response.data["state"])
self.assertEqual("code_not_present", response.data["justification"])
self.assertEqual("detail", response.data["detail"])
diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py
index e5e31159..c960a6d1 100644
--- a/vulnerabilities/tests/test_models.py
+++ b/vulnerabilities/tests/test_models.py
@@ -45,7 +45,7 @@ def test_vulnerability_mixin_get_entry_for_package(self, mock_request_get):
affected_by_vulnerabilities = package1.get_entry_for_package(vulnerablecode)
self.assertEqual(1, len(affected_by_vulnerabilities))
- self.assertEqual("VCID-j3au-usaz-aaag", affected_by_vulnerabilities[0]["vulnerability_id"])
+ self.assertEqual("VCID-j3au-usaz-aaag", affected_by_vulnerabilities[0]["advisory_uid"])
@mock.patch("vulnerabilities.models.AffectedByVulnerabilityMixin.get_entry_for_package")
@mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.is_configured")
@@ -83,7 +83,7 @@ def test_vulnerability_mixin_create_vulnerabilities(self):
response_file = self.data / "vulnerabilities" / "idna_3.6_response.json"
response_json = json.loads(response_file.read_text())
vulnerabilities_data = response_json["results"][0]["affected_by_vulnerabilities"]
- vulnerabilities_data.append({"vulnerability_id": "VCID-0002", "risk_score": 5.0})
+ vulnerabilities_data.append({"advisory_uid": "VCID-0002", "risk_score": 5.0})
package1 = make_package(self.dataspace, package_url="pkg:pypi/idna@3.6")
product1 = make_product(self.dataspace, inventory=[package1])
@@ -198,36 +198,52 @@ def test_vulnerability_model_fixed_packages_count_generated_field(self):
def test_vulnerability_model_create_from_data(self):
package1 = make_package(self.dataspace)
- vulnerability_data = {
- "vulnerability_id": "VCID-q4q6-yfng-aaag",
- "summary": "In Django 3.2 before 3.2.25, 4.2 before 4.2.11, and 5.0.",
- "aliases": ["CVE-2024-27351", "GHSA-vm8q-m57g-pff3", "PYSEC-2024-47"],
- "references": [
- {
- "reference_url": "https://access.redhat.com/hydra/rest/"
- "securitydata/cve/CVE-2024-27351.json",
- "reference_id": "",
- "scores": [
- {
- "value": "7.5",
- "scoring_system": "cvssv3",
- "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
- }
- ],
- },
- ],
- "resource_url": "http://public.vulnerablecode.io/vulnerabilities/VCID-q4q6-yfng-aaag",
- }
+ vulnerability_data = (
+ {
+ "purl": "pkg:npm/%40isaacs/brace-expansion@5.0.0",
+ "affected_by_vulnerabilities": [
+ {
+ "advisory_id": "GHSA-7h2j-956f-4vf2",
+ "advisory_uid": "github_osv/GHSA-7h2j-956f-4vf2",
+ "aliases": ["CVE-2026-25547"],
+ "summary": "@isaacs/brace-expansion has Uncontrolled Resource Consumption",
+ "weighted_severity": 7.8,
+ "exploitability": 0.5,
+ "risk_score": 3.9,
+ "fixed_by_packages": ["pkg:npm/%40isaacs/brace-expansion@5.0.1"],
+ "ssvc_trees": [
+ {
+ "vector": "SSVCv2/.../M:M/D:T/2026-02-05T14:24:50Z/",
+ "options": [
+ {"Exploitation": "none"},
+ {"Automatable": "yes"},
+ {"Technical Impact": "partial"},
+ {"Mission Prevalence": "minimal"},
+ {"Public Well-being Impact": "material"},
+ {"Mission & Well-being": "medium"},
+ ],
+ "decision": "Track",
+ "source_url": "https://github.com/.../CVE-2026-25547.json",
+ }
+ ],
+ "resource_url": "http://vulnerablecode.io/.../GHSA-7h2j-956f-4vf2",
+ }
+ ],
+ "fixing_vulnerabilities": [],
+ "next_non_vulnerable_version": "5.0.1",
+ "latest_non_vulnerable_version": "5.0.1",
+ "risk_score": 3.9,
+ },
+ )
vulnerability1 = Vulnerability.create_from_data(
dataspace=self.dataspace,
data=vulnerability_data,
affecting=package1,
)
- self.assertEqual(vulnerability_data["vulnerability_id"], vulnerability1.vulnerability_id)
+ self.assertEqual(vulnerability_data["advisory_uid"], vulnerability1.advisory_uid)
self.assertEqual(vulnerability_data["summary"], vulnerability1.summary)
self.assertEqual(vulnerability_data["aliases"], vulnerability1.aliases)
- self.assertEqual(vulnerability_data["references"], vulnerability1.references)
self.assertEqual(vulnerability_data["resource_url"], vulnerability1.resource_url)
self.assertQuerySetEqual(vulnerability1.affected_packages.all(), [package1])
@@ -244,7 +260,7 @@ def test_vulnerability_model_get_or_create_from_data(self):
self.assertEqual(vulnerability_data["id"], vulnerability1.vulnerability_id)
self.assertEqual(vulnerability_data["summary"], vulnerability1.summary)
- vulnerability_data["vulnerability_id"] = vulnerability_data["id"]
+ vulnerability_data["advisory_uid"] = vulnerability_data["id"]
vulnerability2 = Vulnerability.get_or_create_from_data(
dataspace=self.dataspace,
data=vulnerability_data,
diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py
index 78530268..02bb41a1 100644
--- a/vulnerabilities/views.py
+++ b/vulnerabilities/views.py
@@ -25,7 +25,7 @@ class VulnerabilityListView(
template_name = "vulnerabilities/vulnerability_list.html"
template_list_table = "vulnerabilities/tables/vulnerability_list_table.html"
table_headers = (
- Header("vulnerability_id", _("Vulnerability"), filter="last_modified_date"),
+ Header("advisory_uid", _("Vulnerability"), filter="last_modified_date"),
Header("summary", _("Summary")),
Header("exploitability", _("Exploitability"), filter="exploitability"),
Header("weighted_severity", _("Severity"), filter="weighted_severity"),
@@ -41,7 +41,8 @@ def get_queryset(self):
.get_queryset()
.only(
"uuid",
- "vulnerability_id",
+ "advisory_uid",
+ "advisory_id",
"resource_url",
"aliases",
"summary",