1818
1919from component_catalog .models import PACKAGE_URL_FIELDS
2020from component_catalog .models import Package
21+ from component_catalog .models import PackageAffectedByVulnerability
2122from dejacode_toolkit .vulnerablecode import VulnerableCode
2223from dje .models import DejacodeUser
2324from dje .utils import chunked_queryset
@@ -145,6 +146,33 @@ def fetch_for_packages(
145146 return results
146147
147148
149+ def batch_add_affected (affected_packages , vulnerabilities ):
150+ """
151+ Link all ``vulnerabilities`` to all ``affected_packages`` using two queries:
152+ one SELECT to find existing relationships, one bulk INSERT for the missing ones.
153+
154+ Replaces N*M individual ``get_or_create`` calls from ``add_affected_by``.
155+ """
156+ existing_pairs = set (
157+ PackageAffectedByVulnerability .objects .filter (
158+ package__in = affected_packages ,
159+ vulnerability__in = vulnerabilities ,
160+ ).values_list ("package_id" , "vulnerability_id" )
161+ )
162+ to_create = [
163+ PackageAffectedByVulnerability (
164+ package = package ,
165+ vulnerability = vulnerability ,
166+ dataspace_id = package .dataspace_id ,
167+ )
168+ for package in affected_packages
169+ for vulnerability in vulnerabilities
170+ if (package .pk , vulnerability .pk ) not in existing_pairs
171+ ]
172+ if to_create :
173+ PackageAffectedByVulnerability .objects .bulk_create (to_create , ignore_conflicts = True )
174+
175+
148176def process_vc_entry (
149177 vc_entry , queryset , dataspace , update , results , vulnerability_cache , log_func = None , verbosity = 1
150178):
@@ -156,9 +184,14 @@ def process_vc_entry(
156184 pre-fetched by the caller in a single batch query. Newly created vulnerabilities are
157185 added to the cache so subsequent entries in the same batch reuse them without a DB hit.
158186
159- Risk score updates on packages are deferred: ``update_risk_score`` is called once per
160- affected package after all vulnerabilities for this entry are processed, then the
161- API-provided purl-level ``risk_score`` overwrites the computed value if present.
187+ M2M links between packages and vulnerabilities are created in batch via
188+ ``batch_add_affected`` (1 SELECT + 1 bulk INSERT) instead of one ``get_or_create``
189+ per pair.
190+
191+ Risk score is applied in a single UPDATE query, bypassing ``Package.save()`` and the
192+ ``handle_assigned_licenses`` overhead it carries. The API-provided purl-level
193+ ``risk_score`` is used directly when present; otherwise the MAX of the linked
194+ vulnerability risk scores is computed in the same query.
162195
163196 Returns the affected packages as a list (already evaluated), or an empty list if the
164197 entry has no vulnerabilities. The ``results`` dict is updated in-place.
@@ -185,39 +218,38 @@ def process_vc_entry(
185218 label = "advisory" if advisory_count == 1 else "advisories"
186219 log_func (f" { purl } : { advisory_count } { label } " )
187220
221+ vulnerabilities = []
188222 for vulnerability_data in affected_by_vulnerabilities :
189223 advisory_uid = vulnerability_data ["advisory_uid" ]
190224 vulnerability = create_or_update_vulnerability (
191225 vulnerability_data ,
192226 dataspace ,
193- affected_packages ,
194227 update ,
195228 results ,
196229 vulnerability = vulnerability_cache .get (advisory_uid ),
197230 )
198231 vulnerability_cache [advisory_uid ] = vulnerability
232+ vulnerabilities .append (vulnerability )
199233
200- # Call update_risk_score once per package after all vulnerabilities are linked,
201- # then let the API-provided purl-level risk_score overwrite the computed value.
202- for package in affected_packages :
203- package .update_risk_score ()
234+ # Link packages to vulnerabilities: 1 SELECT + 1 bulk INSERT instead of N*M get_or_create.
235+ batch_add_affected (affected_packages , vulnerabilities )
204236
237+ # Update risk_score without triggering Package.save() (which carries handle_assigned_licenses).
205238 if package_risk_score := vc_entry .get ("risk_score" ):
206239 packages_qs .update (risk_score = package_risk_score )
207240
208241 return affected_packages
209242
210243
211244def create_or_update_vulnerability (
212- vulnerability_data , dataspace , affected_packages , update , results , vulnerability = None
245+ vulnerability_data , dataspace , update , results , vulnerability = None
213246):
214247 """
215- Create or update a Vulnerability from ``vulnerability_data`` and link it to
216- ``affected_packages``.
248+ Create or update a Vulnerability from ``vulnerability_data``.
217249
218250 ``vulnerability`` is the already-resolved instance (looked up from the caller's
219- ``vulnerability_cache``), or ``None`` if not yet created. Risk score updates on
220- ``affected_packages`` are deferred to the caller via ``update_score=False ``.
251+ ``vulnerability_cache``), or ``None`` if not yet created. M2M linking is handled
252+ by the caller via ``batch_add_affected ``.
221253 """
222254 if not vulnerability :
223255 vulnerability = Vulnerability .create_from_data (
@@ -234,7 +266,6 @@ def create_or_update_vulnerability(
234266 if updated_fields :
235267 results ["updated" ] += 1
236268
237- vulnerability .add_affected (affected_packages , update_score = False )
238269 return vulnerability
239270
240271
0 commit comments