Skip to content

Commit 5c50941

Browse files
committed
feat: extend todo pipeline to compute weakness conflicts
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 93cc6ae commit 5c50941

1 file changed

Lines changed: 217 additions & 2 deletions

File tree

vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py

Lines changed: 217 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,220 @@ 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+
cwe_details[w.cwe_id] = w.to_dict()
515+
cwes.add(w.cwe_id)
516+
517+
canonical_cwes = canonical_value(cwes)
518+
cwe_checksum = sha256_digest(canonical_cwes)
519+
cwe_avid_map[cwe_checksum]["cwes"] = canonical_cwes
520+
cwe_avid_map[cwe_checksum]["avid_precedence"].append(
521+
(advisory.avid, advisory.precedence)
522+
)
523+
advisory_avid_map[advisory.avid] = advisory
524+
525+
if len(cwe_avid_map) < 2:
526+
continue
527+
528+
for map in cwe_avid_map.values():
529+
avid_precedence = map["avid_precedence"]
530+
sorted_avids = [
531+
x[0] for x in sorted(avid_precedence, key=lambda x: x[1], reverse=True)
532+
]
533+
map["primary"] = {"advisory_uid": sorted_avids[0]}
534+
map["secondaries"] = [{"advisory_uid": a} for a in sorted_avids[1:]]
535+
del map["avid_precedence"]
536+
537+
weakness_conflict_count, count_conflicting_advisory = (
538+
check_conflicting_weaknesses_for_alias(
539+
alias=alias,
540+
comparable_cwe_map=cwe_avid_map,
541+
advisories=advisory_avid_map,
542+
cwe_details=cwe_details,
543+
todo_to_create=todo_to_create,
544+
advisory_relation_to_create=advisory_relation_to_create,
545+
existing_todo_ids=existing_todo_ids,
546+
)
547+
)
548+
549+
total_weakness_conflict_count += weakness_conflict_count
550+
total_count_conflicting_advisory += count_conflicting_advisory
551+
total_successfully_compared_advisory_count += initial_advisory_group_size
552+
553+
if len(todo_to_create) > batch_size:
554+
new_todos_count += bulk_create_with_m2m(
555+
todos=todo_to_create,
556+
advisories=advisory_relation_to_create,
557+
logger=self.log,
558+
)
559+
advisory_relation_to_create.clear()
560+
todo_to_create.clear()
561+
562+
new_todos_count += bulk_create_with_m2m(
563+
todos=todo_to_create,
564+
advisories=advisory_relation_to_create,
565+
logger=self.log,
566+
)
567+
568+
self.log(
569+
f"Successfully compared {total_successfully_compared_advisory_count} advisories, created {new_todos_count} new ToDos for {total_weakness_conflict_count} "
570+
f"conflicting weaknesses related to {total_count_conflicting_advisory} advisories."
571+
)
572+
573+
574+
def compute_cwe_disagreement(cwe_groups):
575+
"""Compute differences in cwe across given cwe groups."""
576+
577+
cwe_union = set().union(*cwe_groups)
578+
cwe_intersection = set.intersection(*cwe_groups)
579+
580+
return {
581+
"cwe_union": list(sorted(cwe_union)),
582+
"cwe_intersection": list(cwe_intersection),
583+
"cwe_disagreement": list(cwe_union - cwe_intersection),
584+
}
585+
586+
587+
def check_conflicting_weaknesses_for_alias(
588+
alias,
589+
advisories,
590+
comparable_cwe_map,
591+
cwe_details,
592+
todo_to_create,
593+
advisory_relation_to_create,
594+
existing_todo_ids,
595+
):
596+
"""
597+
Add appropriate AdvisoryToDo for conflicting weaknesses for given advisories..
598+
"""
599+
600+
curation_item = {}
601+
cwe_groups = [set(value["cwes"]) for value in comparable_cwe_map.values()]
602+
disagreement = compute_cwe_disagreement(cwe_groups)
603+
604+
cwe_disagreement_count = len(disagreement["cwe_disagreement"])
605+
if cwe_disagreement_count < 1:
606+
return 0, 0
607+
608+
noun = "weaknesses" if cwe_disagreement_count > 1 else "weakness"
609+
curation_item["all_cwes"] = disagreement["cwe_union"]
610+
curation_item["cwe_details"] = cwe_details
611+
curation_item["partial_curation"] = {"cwes": disagreement["cwe_intersection"]}
612+
curation_item["conflict_reason"] = f"Advisories report different {noun} for {alias}"
613+
curation_item["advisories"] = list(comparable_cwe_map.values())
614+
615+
issue_type = "CONFLICTING_WEAKNESSES"
616+
conflicting_advisories = list(advisories.values())
617+
618+
conflict_checksum = sha256_digest(canonical_value([curation_item]))
619+
issue_detail = {
620+
"alias": alias.alias,
621+
"conflict_checksum": conflict_checksum,
622+
"curation_items": [curation_item],
623+
}
624+
625+
todo_id = advisories_checksum(conflicting_advisories)
626+
627+
if todo_id in existing_todo_ids:
628+
return 0, 0
629+
630+
existing_todo_ids.add(todo_id)
631+
conflicting_advisories_count = len(conflicting_advisories)
632+
633+
date_published = min(
634+
(a.date_published for a in conflicting_advisories if a.date_published),
635+
default=None,
636+
)
637+
date_collected = min(
638+
(a.date_collected for a in conflicting_advisories if a.date_collected),
639+
default=None,
640+
)
641+
todo = AdvisoryToDoV2(
642+
related_advisories_id=todo_id,
643+
issue_type=issue_type,
644+
issue_detail=issue_detail,
645+
alias=alias,
646+
advisories_count=conflicting_advisories_count,
647+
oldest_advisory_date=date_published or date_collected,
648+
)
649+
todo_to_create.append(todo)
650+
advisory_relation_to_create[todo_id] = conflicting_advisories
651+
652+
return cwe_disagreement_count, conflicting_advisories_count
653+
439654

440655
def check_conflicting_cvss_for_alias(
441656
alias,
@@ -495,7 +710,7 @@ def check_conflicting_cvss_for_alias(
495710
"cvss": cvss_version,
496711
"conflict_reason": conflict_message,
497712
"partial_cvss_curation": consensus_metrics,
498-
"advisories": get_grouped_advisory_curation(
713+
"advisories": get_grouped_cvss_advisory_curation(
499714
advisory_curation_item_map, cvss_type, advisories, item.keys()
500715
),
501716
}
@@ -539,7 +754,7 @@ def check_conflicting_cvss_for_alias(
539754
return len(curation_items), conflicting_advisories_count
540755

541756

542-
def get_grouped_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids):
757+
def get_grouped_cvss_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids):
543758
"""Group curation advisory based on CVSS vector similarity."""
544759
curation_items = []
545760
vector_group = defaultdict(list)

0 commit comments

Comments
 (0)