|
| 1 | +# |
| 2 | +# Copyright (c) nexB Inc. and others. All rights reserved. |
| 3 | +# VulnerableCode is a trademark of nexB Inc. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. |
| 6 | +# See https://github.com/aboutcode-org/vulnerablecode for support or download. |
| 7 | +# See https://aboutcode.org for more information about nexB OSS projects. |
| 8 | +# |
| 9 | + |
| 10 | +from collections import defaultdict |
| 11 | + |
| 12 | +from django.db import transaction |
| 13 | + |
| 14 | +from vulnerabilities.models import AdvisorySet |
| 15 | +from vulnerabilities.models import AdvisorySetMember |
| 16 | +from vulnerabilities.models import AdvisoryV2 |
| 17 | +from vulnerabilities.models import PackageV2 |
| 18 | +from vulnerabilities.pipelines import VulnerableCodePipeline |
| 19 | +from vulnerabilities.utils import compute_advisory_content |
| 20 | + |
| 21 | + |
| 22 | +class GroupAdvisoriesForPackages(VulnerableCodePipeline): |
| 23 | + """Detect and flag packages that do not exist upstream.""" |
| 24 | + |
| 25 | + pipeline_id = "group_advisories_for_packages" |
| 26 | + |
| 27 | + @classmethod |
| 28 | + def steps(cls): |
| 29 | + return (cls.group_advisories_for_packages,) |
| 30 | + |
| 31 | + def group_advisories_for_packages(self): |
| 32 | + group_advisoris_for_packages(logger=self.log) |
| 33 | + |
| 34 | + |
| 35 | +def merge_advisories(advisories): |
| 36 | + |
| 37 | + advisories = list(advisories) |
| 38 | + |
| 39 | + content_hash_map = defaultdict(list) |
| 40 | + result_groups = [] |
| 41 | + |
| 42 | + for adv in advisories: |
| 43 | + |
| 44 | + if adv.advisory_content_hash: |
| 45 | + content_hash_map[adv.advisory_content_hash].append(adv) |
| 46 | + else: |
| 47 | + content_hash = compute_advisory_content(advisory_data=adv) |
| 48 | + if content_hash: |
| 49 | + content_hash_map[content_hash].append(adv) |
| 50 | + else: |
| 51 | + result_groups.append([adv]) |
| 52 | + |
| 53 | + final_groups = [] |
| 54 | + |
| 55 | + for group in content_hash_map.values(): |
| 56 | + groups = get_merged_identifier_groups(group) |
| 57 | + final_groups.extend(groups) |
| 58 | + |
| 59 | + return final_groups |
| 60 | + |
| 61 | + |
| 62 | +def get_merged_identifier_groups(advisories): |
| 63 | + |
| 64 | + identifier_groups = defaultdict(set) |
| 65 | + advisory_to_identifiers = defaultdict(set) |
| 66 | + |
| 67 | + advisories = list(advisories) |
| 68 | + |
| 69 | + for adv in advisories: |
| 70 | + |
| 71 | + identifier_groups[adv.advisory_id].add(adv) |
| 72 | + advisory_to_identifiers[adv].add(adv.advisory_id) |
| 73 | + |
| 74 | + for alias in adv.aliases.all(): |
| 75 | + identifier_groups[alias.alias].add(adv) |
| 76 | + advisory_to_identifiers[adv].add(alias.alias) |
| 77 | + |
| 78 | + groups = [set(advs) for advs in identifier_groups.values() if len(advs) > 1] |
| 79 | + |
| 80 | + merged = [] |
| 81 | + |
| 82 | + for group in groups: |
| 83 | + group = set(group) |
| 84 | + |
| 85 | + i = 0 |
| 86 | + while i < len(merged): |
| 87 | + if group & merged[i]: |
| 88 | + group |= merged[i] |
| 89 | + merged.pop(i) |
| 90 | + else: |
| 91 | + i += 1 |
| 92 | + |
| 93 | + merged.append(group) |
| 94 | + |
| 95 | + all_grouped = set() |
| 96 | + for g in merged: |
| 97 | + all_grouped |= g |
| 98 | + |
| 99 | + for adv in advisories: |
| 100 | + if adv not in all_grouped: |
| 101 | + merged.append({adv}) |
| 102 | + |
| 103 | + final_groups = [] |
| 104 | + |
| 105 | + for group in merged: |
| 106 | + identifiers = set() |
| 107 | + for adv in group: |
| 108 | + for alias in adv.aliases.values_list("alias", flat=True): |
| 109 | + identifiers.add(alias) |
| 110 | + |
| 111 | + primary = max(group, key=lambda a: a.precedence if a.precedence is not None else -1) |
| 112 | + |
| 113 | + secondary = [a for a in group if a != primary] |
| 114 | + |
| 115 | + final_groups.append((identifiers, primary, secondary)) |
| 116 | + |
| 117 | + return final_groups |
| 118 | + |
| 119 | + |
| 120 | +def group_advisoris_for_packages(logger=None): |
| 121 | + for package in PackageV2.objects.iterator(): |
| 122 | + affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( |
| 123 | + purl=package.purl |
| 124 | + ).prefetch_related("aliases") |
| 125 | + |
| 126 | + fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl( |
| 127 | + purl=package.purl |
| 128 | + ).prefetch_related("aliases") |
| 129 | + |
| 130 | + try: |
| 131 | + delete_and_save_advisory_set(package, affecting_advisories, relation="affecting") |
| 132 | + delete_and_save_advisory_set(package, fixed_by_advisories, relation="fixing") |
| 133 | + except Exception as e: |
| 134 | + print(f"Failed rebuilding advisory sets for package {package.purl}: {e!r}") |
| 135 | + continue |
| 136 | + |
| 137 | + |
| 138 | +@transaction.atomic |
| 139 | +def delete_and_save_advisory_set(package, advisories, relation=None): |
| 140 | + AdvisorySet.objects.filter(package=package, relation_type=relation).delete() |
| 141 | + |
| 142 | + groups = merge_advisories(advisories) |
| 143 | + |
| 144 | + membership_to_create = [] |
| 145 | + |
| 146 | + for identifiers, primary, secondary in groups: |
| 147 | + |
| 148 | + advisory_set = AdvisorySet.objects.create( |
| 149 | + package=package, |
| 150 | + relation_type=relation, |
| 151 | + identifiers=list(identifiers), |
| 152 | + primary_advisory=primary, |
| 153 | + ) |
| 154 | + |
| 155 | + membership_to_create.append( |
| 156 | + AdvisorySetMember( |
| 157 | + advisory_set=advisory_set, |
| 158 | + advisory=primary, |
| 159 | + is_primary=True, |
| 160 | + ) |
| 161 | + ) |
| 162 | + |
| 163 | + for adv in secondary: |
| 164 | + membership_to_create.append( |
| 165 | + AdvisorySetMember( |
| 166 | + advisory_set=advisory_set, |
| 167 | + advisory=adv, |
| 168 | + is_primary=False, |
| 169 | + ) |
| 170 | + ) |
| 171 | + |
| 172 | + AdvisorySetMember.objects.bulk_create(membership_to_create) |
0 commit comments