@@ -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
440655def 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