Skip to content

Commit 1858eb5

Browse files
authored
Merge pull request #2380 from aboutcode-org/weakness-curation
Add support for CWE weakness curation
2 parents 7cdf304 + c95f8aa commit 1858eb5

9 files changed

Lines changed: 924 additions & 13 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Generated by Django 5.2.11 on 2026-07-16 10:13
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0139_cleanup_none_string_in_severity"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="advisorytodo",
15+
name="issue_type",
16+
field=models.CharField(
17+
choices=[
18+
("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"),
19+
("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"),
20+
(
21+
"MISSING_AFFECTED_AND_FIXED_BY_PACKAGES",
22+
"Advisory is missing both affected and fixed-by packages",
23+
),
24+
("MISSING_SUMMARY", "Advisory is missing summary"),
25+
(
26+
"CONFLICTING_FIXED_BY_PACKAGES",
27+
"Advisories have conflicting fixed-by packages",
28+
),
29+
(
30+
"CONFLICTING_AFFECTED_PACKAGES",
31+
"Advisories have conflicting affected packages",
32+
),
33+
(
34+
"CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES",
35+
"Advisories have conflicting affected and fixed-by packages",
36+
),
37+
("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"),
38+
("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"),
39+
],
40+
db_index=True,
41+
help_text="Select the issue that needs to be addressed from the available options.",
42+
max_length=50,
43+
),
44+
),
45+
migrations.AlterField(
46+
model_name="advisorytodov2",
47+
name="issue_type",
48+
field=models.CharField(
49+
choices=[
50+
("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"),
51+
("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"),
52+
(
53+
"MISSING_AFFECTED_AND_FIXED_BY_PACKAGES",
54+
"Advisory is missing both affected and fixed-by packages",
55+
),
56+
("MISSING_SUMMARY", "Advisory is missing summary"),
57+
(
58+
"CONFLICTING_FIXED_BY_PACKAGES",
59+
"Advisories have conflicting fixed-by packages",
60+
),
61+
(
62+
"CONFLICTING_AFFECTED_PACKAGES",
63+
"Advisories have conflicting affected packages",
64+
),
65+
(
66+
"CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES",
67+
"Advisories have conflicting affected and fixed-by packages",
68+
),
69+
("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"),
70+
("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"),
71+
],
72+
db_index=True,
73+
help_text="Select the issue that needs to be addressed from the available options.",
74+
max_length=50,
75+
),
76+
),
77+
]

vulnerabilities/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from cwe2.mappings import xml_database_path
3535
from cwe2.weakness import Weakness as DBWeakness
3636
from django.contrib.auth import get_user_model
37-
from django.contrib.auth.models import Group
3837
from django.contrib.auth.models import UserManager
3938
from django.core import exceptions
4039
from django.core.exceptions import ValidationError
@@ -2463,6 +2462,7 @@ def create_new_job(self, execute_now=False):
24632462
"Advisories have conflicting affected and fixed-by packages",
24642463
),
24652464
("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"),
2465+
("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"),
24662466
]
24672467

24682468

vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def steps(cls):
4545
cls.compute_individual_advisory_todo,
4646
cls.detect_conflicting_package_versions,
4747
cls.detect_conflicting_cvss_scores,
48+
cls.detect_conflicting_weakness,
4849
)
4950

5051
def compute_individual_advisory_todo(self):
@@ -436,6 +437,223 @@ def detect_conflicting_cvss_scores(self):
436437
f"conflicting CVSS scores related to {total_count_conflicting_advisory} advisories."
437438
)
438439

440+
def detect_conflicting_weakness(self):
441+
"""
442+
Create ToDos for advisories with conflicting opinions on weaknesses for a vulnerability.
443+
"""
444+
advisory_relation_to_create = {}
445+
todo_to_create = []
446+
new_todos_count = 0
447+
batch_size = 1
448+
total_count_conflicting_advisory = 0
449+
total_weakness_conflict_count = 0
450+
total_successfully_compared_advisory_count = 0
451+
existing_todo_ids = set(
452+
AdvisoryToDoV2.objects.values_list("related_advisories_id", flat=True)
453+
)
454+
455+
advisory_qs = (
456+
AdvisoryV2.objects.exclude(
457+
advisory_todos__issue_type="MISSING_AFFECTED_AND_FIXED_BY_PACKAGES"
458+
)
459+
.filter(weaknesses__isnull=False)
460+
.todo_excluded()
461+
.latest_per_avid()
462+
.distinct()
463+
.prefetch_related("weaknesses")
464+
)
465+
466+
cve_aliases = AdvisoryAlias.objects.filter(alias__istartswith="cve").prefetch_related(
467+
Prefetch("advisories", queryset=advisory_qs, to_attr="filtered_advisories")
468+
)
469+
non_cve_aliases = AdvisoryAlias.objects.exclude(alias__istartswith="cve").prefetch_related(
470+
Prefetch("advisories", queryset=advisory_qs, to_attr="filtered_advisories")
471+
)
472+
advisory_count = advisory_qs.count()
473+
aliases_count = cve_aliases.count() + non_cve_aliases.count()
474+
progress = LoopProgress(
475+
total_iterations=aliases_count,
476+
logger=self.log,
477+
progress_step=5,
478+
)
479+
self.log(f"Detect conflicting weaknesses in {advisory_count} advisory.")
480+
aliases = chain(
481+
cve_aliases.iterator(chunk_size=50),
482+
non_cve_aliases.iterator(chunk_size=50),
483+
)
484+
for alias in progress.iter(aliases):
485+
486+
advisory_avid_map = {}
487+
cwe_avid_map = defaultdict(
488+
lambda: {
489+
"cwes": (),
490+
"avid_precedence": [],
491+
"primary": "",
492+
"secondaries": [],
493+
}
494+
)
495+
496+
advisories_with_common_alias = alias.filtered_advisories or []
497+
known_advisory_ids = [a.id for a in advisories_with_common_alias]
498+
adv_with_alias_in_adv_id = advisory_qs.filter(advisory_id=alias.alias).exclude(
499+
id__in=known_advisory_ids
500+
)
501+
if not advisories_with_common_alias and not adv_with_alias_in_adv_id.exists():
502+
continue
503+
504+
advisories_with_common_alias.extend(adv_with_alias_in_adv_id)
505+
initial_advisory_group_size = len(advisories_with_common_alias)
506+
507+
if initial_advisory_group_size < 2:
508+
continue
509+
510+
cwe_details = {}
511+
for advisory in advisories_with_common_alias:
512+
cwes = set()
513+
for w in advisory.weaknesses.all():
514+
if not w.weakness.weakness_abstraction:
515+
continue
516+
517+
cwe_details[w.cwe_id] = w.to_dict()
518+
cwes.add(w.cwe_id)
519+
520+
canonical_cwes = canonical_value(cwes)
521+
cwe_checksum = sha256_digest(canonical_cwes)
522+
cwe_avid_map[cwe_checksum]["cwes"] = canonical_cwes
523+
cwe_avid_map[cwe_checksum]["avid_precedence"].append(
524+
(advisory.avid, advisory.precedence)
525+
)
526+
advisory_avid_map[advisory.avid] = advisory
527+
528+
if len(cwe_avid_map) < 2:
529+
continue
530+
531+
for map in cwe_avid_map.values():
532+
avid_precedence = map["avid_precedence"]
533+
sorted_avids = [
534+
x[0] for x in sorted(avid_precedence, key=lambda x: x[1], reverse=True)
535+
]
536+
map["primary"] = {"advisory_uid": sorted_avids[0]}
537+
map["secondaries"] = [{"advisory_uid": a} for a in sorted_avids[1:]]
538+
del map["avid_precedence"]
539+
540+
weakness_conflict_count, count_conflicting_advisory = (
541+
check_conflicting_weaknesses_for_alias(
542+
alias=alias,
543+
comparable_cwe_map=cwe_avid_map,
544+
advisories=advisory_avid_map,
545+
cwe_details=cwe_details,
546+
todo_to_create=todo_to_create,
547+
advisory_relation_to_create=advisory_relation_to_create,
548+
existing_todo_ids=existing_todo_ids,
549+
)
550+
)
551+
552+
total_weakness_conflict_count += weakness_conflict_count
553+
total_count_conflicting_advisory += count_conflicting_advisory
554+
total_successfully_compared_advisory_count += initial_advisory_group_size
555+
556+
if len(todo_to_create) > batch_size:
557+
new_todos_count += bulk_create_with_m2m(
558+
todos=todo_to_create,
559+
advisories=advisory_relation_to_create,
560+
logger=self.log,
561+
)
562+
advisory_relation_to_create.clear()
563+
todo_to_create.clear()
564+
565+
new_todos_count += bulk_create_with_m2m(
566+
todos=todo_to_create,
567+
advisories=advisory_relation_to_create,
568+
logger=self.log,
569+
)
570+
571+
self.log(
572+
f"Successfully compared {total_successfully_compared_advisory_count} advisories, created {new_todos_count} new ToDos for {total_weakness_conflict_count} "
573+
f"conflicting weaknesses related to {total_count_conflicting_advisory} advisories."
574+
)
575+
576+
577+
def compute_cwe_disagreement(cwe_groups):
578+
"""Compute differences in cwe across given cwe groups."""
579+
580+
cwe_union = set().union(*cwe_groups)
581+
cwe_intersection = set.intersection(*cwe_groups)
582+
583+
return {
584+
"cwe_union": list(sorted(cwe_union)),
585+
"cwe_intersection": list(cwe_intersection),
586+
"cwe_disagreement": list(cwe_union - cwe_intersection),
587+
}
588+
589+
590+
def check_conflicting_weaknesses_for_alias(
591+
alias,
592+
advisories,
593+
comparable_cwe_map,
594+
cwe_details,
595+
todo_to_create,
596+
advisory_relation_to_create,
597+
existing_todo_ids,
598+
):
599+
"""
600+
Add appropriate AdvisoryToDo for conflicting weaknesses for given advisories..
601+
"""
602+
603+
curation_item = {}
604+
cwe_groups = [set(value["cwes"]) for value in comparable_cwe_map.values()]
605+
disagreement = compute_cwe_disagreement(cwe_groups)
606+
607+
cwe_disagreement_count = len(disagreement["cwe_disagreement"])
608+
if cwe_disagreement_count < 1:
609+
return 0, 0
610+
611+
noun = "weaknesses" if cwe_disagreement_count > 1 else "weakness"
612+
curation_item["all_cwes"] = disagreement["cwe_union"]
613+
curation_item["cwe_details"] = cwe_details
614+
curation_item["partial_curation"] = {"cwes": disagreement["cwe_intersection"]}
615+
curation_item["conflict_reason"] = f"Advisories report different {noun} for {alias}"
616+
curation_item["advisories"] = list(comparable_cwe_map.values())
617+
618+
issue_type = "CONFLICTING_WEAKNESSES"
619+
conflicting_advisories = list(advisories.values())
620+
621+
conflict_checksum = sha256_digest(canonical_value([curation_item]))
622+
issue_detail = {
623+
"alias": alias.alias,
624+
"conflict_checksum": conflict_checksum,
625+
"curation_items": [curation_item],
626+
}
627+
628+
todo_id = advisories_checksum(conflicting_advisories)
629+
630+
if todo_id in existing_todo_ids:
631+
return 0, 0
632+
633+
existing_todo_ids.add(todo_id)
634+
conflicting_advisories_count = len(conflicting_advisories)
635+
636+
date_published = min(
637+
(a.date_published for a in conflicting_advisories if a.date_published),
638+
default=None,
639+
)
640+
date_collected = min(
641+
(a.date_collected for a in conflicting_advisories if a.date_collected),
642+
default=None,
643+
)
644+
todo = AdvisoryToDoV2(
645+
related_advisories_id=todo_id,
646+
issue_type=issue_type,
647+
issue_detail=issue_detail,
648+
alias=alias,
649+
advisories_count=conflicting_advisories_count,
650+
oldest_advisory_date=date_published or date_collected,
651+
)
652+
todo_to_create.append(todo)
653+
advisory_relation_to_create[todo_id] = conflicting_advisories
654+
655+
return cwe_disagreement_count, conflicting_advisories_count
656+
439657

440658
def check_conflicting_cvss_for_alias(
441659
alias,
@@ -495,7 +713,7 @@ def check_conflicting_cvss_for_alias(
495713
"cvss": cvss_version,
496714
"conflict_reason": conflict_message,
497715
"partial_cvss_curation": consensus_metrics,
498-
"advisories": get_grouped_advisory_curation(
716+
"advisories": get_grouped_cvss_advisory_curation(
499717
advisory_curation_item_map, cvss_type, advisories, item.keys()
500718
),
501719
}
@@ -539,7 +757,7 @@ def check_conflicting_cvss_for_alias(
539757
return len(curation_items), conflicting_advisories_count
540758

541759

542-
def get_grouped_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids):
760+
def get_grouped_cvss_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids):
543761
"""Group curation advisory based on CVSS vector similarity."""
544762
curation_items = []
545763
vector_group = defaultdict(list)

vulnerabilities/templates/advisory_todos.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ <h1>Advisory To-Dos</h1>
127127
<a href="{% url 'todo-detail' todo_id=todo.todo_id %}" class="has-text-info">
128128
{% elif todo.issue_type == "CONFLICTING_SEVERITY_SCORES" %}
129129
<a href="{% url 'todo-severity-detail' todo_id=todo.todo_id %}" class="has-text-info">
130+
{% elif todo.issue_type == "CONFLICTING_WEAKNESSES" %}
131+
<a href="{% url 'todo-weakness-detail' todo_id=todo.todo_id %}" class="has-text-info">
130132
{% endif %}
131133
<div class="columns px-1 is-vcentered">
132134
<div class="column has-text-left" style="flex: 0 0 20%;">

0 commit comments

Comments
 (0)