diff --git a/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py b/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py new file mode 100644 index 000000000..cd13721e9 --- /dev/null +++ b/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.25 on 2025-12-19 08:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0105_packagecommitpatch_patch_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="advisoryreference", + name="url", + field=models.URLField(help_text="URL to the vulnerability reference", max_length=1024), + ), + migrations.AlterField( + model_name="advisoryseverity", + name="value", + field=models.CharField( + help_text="Example: 9.0, Important, High", max_length=50, null=True + ), + ), + migrations.AlterField( + model_name="advisoryweakness", + name="cwe_id", + field=models.IntegerField(help_text="CWE id", unique=True), + ), + migrations.AlterUniqueTogether( + name="advisoryreference", + unique_together={("url", "reference_type")}, + ), + migrations.AlterUniqueTogether( + name="advisoryseverity", + unique_together={ + ("url", "scoring_system", "value", "scoring_elements", "published_at") + }, + ), + migrations.AddConstraint( + model_name="advisoryseverity", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("value__isnull", False), models.Q(("value", ""), _negated=True)), + models.Q( + ("scoring_elements__isnull", False), + models.Q(("scoring_elements", ""), _negated=True), + ), + _connector="OR", + ), + name="scoring_elements_or_value_must_be_set", + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 4c2cf5499..f4b21eb49 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -2573,7 +2573,8 @@ class AdvisorySeverity(models.Model): ), ) - value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High") + # A severity value might be missing and it may just contain scoring_elements only + value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High", null=True) scoring_elements = models.CharField( max_length=150, @@ -2591,6 +2592,16 @@ class AdvisorySeverity(models.Model): class Meta: verbose_name_plural = "Advisory severities" ordering = ["url", "scoring_system", "value"] + unique_together = ("url", "scoring_system", "value", "scoring_elements", "published_at") + constraints = [ + models.CheckConstraint( + check=( + Q(value__isnull=False) & ~Q(value="") + | Q(scoring_elements__isnull=False) & ~Q(scoring_elements="") + ), + name="scoring_elements_or_value_must_be_set", + ) + ] def to_dict(self): return { @@ -2612,7 +2623,7 @@ class AdvisoryWeakness(models.Model): A weakness is a software weakness that is associated with a vulnerability. """ - cwe_id = models.IntegerField(help_text="CWE id") + cwe_id = models.IntegerField(help_text="CWE id", unique=True) cwe_by_id = {} @@ -2659,7 +2670,6 @@ class AdvisoryReference(models.Model): url = models.URLField( max_length=1024, help_text="URL to the vulnerability reference", - unique=True, ) ADVISORY = "advisory" @@ -2689,6 +2699,7 @@ class AdvisoryReference(models.Model): class Meta: ordering = ["reference_id", "url", "reference_type"] + unique_together = ("url", "reference_type") def __str__(self): reference_id = f" {self.reference_id}" if self.reference_id else "" diff --git a/vulnerabilities/pipelines/v2_importers/gitlab_importer.py b/vulnerabilities/pipelines/v2_importers/gitlab_importer.py index e28ab7520..a32b6a6c3 100644 --- a/vulnerabilities/pipelines/v2_importers/gitlab_importer.py +++ b/vulnerabilities/pipelines/v2_importers/gitlab_importer.py @@ -25,7 +25,9 @@ from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackageV2 from vulnerabilities.importer import ReferenceV2 +from vulnerabilities.importer import VulnerabilitySeverity from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 +from vulnerabilities.severity_systems import SCORING_SYSTEMS from vulnerabilities.utils import build_description from vulnerabilities.utils import get_advisory_url from vulnerabilities.utils import get_cwe_id @@ -291,6 +293,31 @@ def parse_gitlab_advisory( fixed_version_range=fixed_version_range, ) + cvss_v2 = gitlab_advisory.get("cvss_v2") + cvss_v3 = gitlab_advisory.get("cvss_v3") + severities = [] + if cvss_v2: + severities.append( + VulnerabilitySeverity( + system=SCORING_SYSTEMS["cvssv2"], + scoring_elements=cvss_v2, + value=None, + url=advisory_url, + ) + ) + if cvss_v3: + scoring_system = SCORING_SYSTEMS["cvssv3"] + if cvss_v3.startswith("CVSS:3.1/"): + scoring_system = SCORING_SYSTEMS["cvssv3.1"] + severities.append( + VulnerabilitySeverity( + system=scoring_system, + scoring_elements=cvss_v3, + value=None, + url=advisory_url, + ) + ) + return AdvisoryData( advisory_id=advisory_id, aliases=aliases, @@ -299,6 +326,7 @@ def parse_gitlab_advisory( date_published=date_published, affected_packages=[affected_package], weaknesses=cwe_list, + severities=severities, url=advisory_url, original_advisory_text=json.dumps(gitlab_advisory, indent=2, ensure_ascii=False), ) diff --git a/vulnerabilities/pipelines/v2_importers/npm_importer.py b/vulnerabilities/pipelines/v2_importers/npm_importer.py index bf9de86d1..0e3ff7f13 100644 --- a/vulnerabilities/pipelines/v2_importers/npm_importer.py +++ b/vulnerabilities/pipelines/v2_importers/npm_importer.py @@ -88,6 +88,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]: severities.append( VulnerabilitySeverity( system=CVSSV3, + scoring_elements=cvss_vector, value=cvss_score, url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json", ) @@ -97,6 +98,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]: VulnerabilitySeverity( system=CVSSV2, value=cvss_score, + scoring_elements=cvss_vector, url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json", ) ) diff --git a/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py b/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py index 4afe8b2a1..2b82b667c 100644 --- a/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py +++ b/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py @@ -16,7 +16,6 @@ from vulnerabilities.models import AdvisorySeverity from vulnerabilities.models import AdvisoryV2 from vulnerabilities.pipelines import VulnerableCodePipeline -from vulnerabilities.pipelines.v2_importers.vulnrichment_importer import VulnrichImporterPipeline from vulnerabilities.severity_systems import SCORING_SYSTEMS logger = logging.getLogger(__name__) @@ -38,7 +37,6 @@ def steps(cls): def collect_ssvc_data(self): vulnrichment_advisories = ( AdvisoryV2.objects.filter( - datasource_id=VulnrichImporterPipeline.pipeline_id, severities__scoring_system=SCORING_SYSTEMS["ssvc"], ) .distinct() @@ -59,6 +57,7 @@ def collect_ssvc_data(self): self.log(f"Processing advisory: {advisory.advisory_id}") for severity in advisory.severities.all(): ssvc_vector = severity.scoring_elements + self.log(f"SSVC Vector found: {ssvc_vector}") try: ssvc_tree, decision = convert_vector_to_tree_and_decision(ssvc_vector) self.log( @@ -78,7 +77,7 @@ def collect_ssvc_data(self): ).distinct() related_advisories = related_advisories.exclude(id=advisory.id) ssvc_obj.related_advisories.set(related_advisories) - except ValueError as e: + except Exception as e: logger.error( f"Failed to parse SSVC vector '{ssvc_vector}' for advisory '{advisory}': {e}" ) diff --git a/vulnerabilities/pipes/advisory.py b/vulnerabilities/pipes/advisory.py index 8f7b33353..b5405ecbf 100644 --- a/vulnerabilities/pipes/advisory.py +++ b/vulnerabilities/pipes/advisory.py @@ -85,16 +85,17 @@ def get_or_create_advisory_severities(severities: List) -> QuerySet: severity_objs = [] for severity in severities: published_at = str(severity.published_at) if severity.published_at else None - sev, _ = AdvisorySeverity.objects.get_or_create( - scoring_system=severity.system.identifier, - value=severity.value, - scoring_elements=severity.scoring_elements, - defaults={ - "published_at": published_at, - }, - url=severity.url, - ) - severity_objs.append(sev) + if severity.scoring_elements or severity.value: + sev, _ = AdvisorySeverity.objects.get_or_create( + scoring_system=severity.system.identifier, + value=severity.value, + scoring_elements=severity.scoring_elements, + defaults={ + "published_at": published_at, + }, + url=severity.url, + ) + severity_objs.append(sev) return AdvisorySeverity.objects.filter(id__in=[severity.id for severity in severity_objs]) diff --git a/vulnerabilities/risk.py b/vulnerabilities/risk.py index 04af140f0..0628422bb 100644 --- a/vulnerabilities/risk.py +++ b/vulnerabilities/risk.py @@ -43,6 +43,8 @@ def get_weighted_severity(severities): weight = WEIGHT_CONFIG.get(severity_source, DEFAULT_WEIGHT) max_weight = float(weight) / 10 vul_score = severity.value + if not vul_score: + continue try: vul_score = float(vul_score) vul_score_value = vul_score * max_weight diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index 976ae80de..24a4b0d2c 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -63,6 +63,14 @@ +
  • + + + Severity details ({{ severity_vectors|length }}) + + +
  • + {% if ssvcs %}
  • @@ -450,6 +458,102 @@ {% endif %} +
    + {% for severity_vector in severity_vectors %} + {% if severity_vector.vector.version == '2.0' %} + Vector: {{ severity_vector.vector.vectorString }} Found at {{ severity_vector.origin }} + + + + + + + + + + + + + + + + + + + +
    Exploitability (E)Access Vector (AV)Access Complexity (AC)Authentication (Au)Confidentiality Impact (C)Integrity Impact (I)Availability Impact (A)
    {{ severity_vector.vector.exploitability|cvss_printer:"high,functional,unproven,proof_of_concept,not_defined" }}{{ severity_vector.vector.accessVector|cvss_printer:"local,adjacent_network,network" }}{{ severity_vector.vector.accessComplexity|cvss_printer:"high,medium,low" }}{{ severity_vector.vector.authentication|cvss_printer:"multiple,single,none" }}{{ severity_vector.vector.confidentialityImpact|cvss_printer:"none,partial,complete" }}{{ severity_vector.vector.integrityImpact|cvss_printer:"none,partial,complete" }}{{ severity_vector.vector.availabilityImpact|cvss_printer:"none,partial,complete" }}
    + {% elif severity_vector.vector.version == '3.1' or severity_vector.vector.version == '3.0'%} + Vector: {{ severity_vector.vector.vectorString }} Found at {{ severity_vector.origin }} + + + + + + + + + + + + + + + + + + + + + +
    Attack Vector (AV)Attack Complexity (AC)Privileges Required (PR)User Interaction (UI)Scope (S)Confidentiality Impact (C)Integrity Impact (I)Availability Impact (A)
    {{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent_network,local,physical"}}{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}{{ severity_vector.vector.userInteraction|cvss_printer:"none,required"}}{{ severity_vector.vector.scope|cvss_printer:"unchanged,changed" }}{{ severity_vector.vector.confidentialityImpact|cvss_printer:"high,low,none" }}{{ severity_vector.vector.integrityImpact|cvss_printer:"high,low,none" }}{{ severity_vector.vector.availabilityImpact|cvss_printer:"high,low,none" }}
    + {% elif severity_vector.vector.version == '4' %} + Vector: {{ severity_vector.vector.vectorString }} Found at {{ severity_vector.origin }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Attack Vector (AV)Attack Complexity (AC)Attack Requirements (AT)Privileges Required (PR)User Interaction (UI)Vulnerable System Impact Confidentiality (VC)Vulnerable System Impact Integrity (VI)Vulnerable System Impact Availability (VA)Subsequent System Impact Confidentiality (SC)Subsequent System Impact Integrity (SI)Subsequent System Impact Availability (SA)
    {{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent,local,physical"}}{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}{{ severity_vector.vector.attackRequirement|cvss_printer:"none,present" }}{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}{{ severity_vector.vector.userInteraction|cvss_printer:"none,passive,active"}}{{ severity_vector.vector.vulnerableSystemImpactConfidentiality|cvss_printer:"high,low,none" }}{{ severity_vector.vector.vulnerableSystemImpactIntegrity|cvss_printer:"high,low,none" }}{{ severity_vector.vector.vulnerableSystemImpactAvailability|cvss_printer:"high,low,none" }}{{ severity_vector.vector.subsequentSystemImpactConfidentiality|cvss_printer:"high,low,none" }}{{ severity_vector.vector.subsequentSystemImpactIntegrity|cvss_printer:"high,low,none" }}{{ severity_vector.vector.subsequentSystemImpactAvailability|cvss_printer:"high,low,none" }}
    + {% elif severity_vector.vector.version == 'ssvc' %} +
    + Vector: {{ severity_vector.vector.vectorString }} Found at {{ severity_vector.origin }} +
    + {% endif %} + {% empty %} + + + There are no known vectors. + + + {% endfor %} +
    +
    {% if ssvcs %} {% for ssvc in ssvcs %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 7cdc0cd4f..8a77df84b 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -429,8 +429,10 @@ def get_context_data(self, **kwargs): if weakness_object.weakness ] - valid_severities = self.object.severities.exclude(scoring_system=EPSS.identifier).filter( - scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.keys() + valid_severities = ( + self.object.severities.exclude(scoring_system=EPSS.identifier) + .filter(scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.values()) + .exclude(scoring_elements="") ) epss_severity = advisory.severities.filter(scoring_system="epss").first() @@ -445,6 +447,27 @@ def get_context_data(self, **kwargs): ssvc_entries = [] seen = set() + severity_vectors = [] + + for severity in valid_severities: + try: + vector_values_system = SCORING_SYSTEMS.get(severity.scoring_system) + if not vector_values_system: + logging.error(f"Unknown scoring system: {severity.scoring_system}") + continue + if vector_values_system.identifier in ["cvssv3.1_qr"]: + continue + vector_values = vector_values_system.get(severity.scoring_elements) + if vector_values: + severity_vectors.append({"vector": vector_values, "origin": severity.url}) + except ( + CVSS2MalformedError, + CVSS3MalformedError, + CVSS4MalformedError, + NotImplementedError, + ): + logging.error(f"CVSSMalformedError for {severity.scoring_elements}") + def add_ssvc(ssvc): key = (ssvc.vector, ssvc.source_advisory_id) if key in seen: @@ -473,9 +496,9 @@ def add_ssvc(ssvc): "severities": list(advisory.severities.all()), "references": list(advisory.references.all()), "aliases": list(advisory.aliases.all()), + "severity_vectors": severity_vectors, "weaknesses": weaknesses_present_in_db, "status": advisory.get_status_label, - # "history": advisory.history, "epss_data": epss_data, } )