@@ -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
440658def 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 )
0 commit comments