|
10 | 10 | import logging |
11 | 11 | from datetime import datetime |
12 | 12 | from datetime import timezone |
| 13 | +from traceback import format_exc as traceback_format_exc |
13 | 14 | from typing import List |
14 | 15 |
|
15 | 16 | from django.core.exceptions import ValidationError |
@@ -79,7 +80,7 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver |
79 | 80 | for inference in inferences: |
80 | 81 | vulnerability = get_or_create_vulnerability_and_aliases( |
81 | 82 | vulnerability_id=inference.vulnerability_id, |
82 | | - alias_names=inference.aliases, |
| 83 | + aliases=inference.aliases, |
83 | 84 | summary=inference.summary, |
84 | 85 | ) |
85 | 86 |
|
@@ -173,69 +174,97 @@ def create_valid_vulnerability_reference(url, reference_id=None): |
173 | 174 | return reference |
174 | 175 |
|
175 | 176 |
|
176 | | -def get_or_create_vulnerability_and_aliases(alias_names, vulnerability_id=None, summary=None): |
| 177 | +def get_or_create_vulnerability_and_aliases( |
| 178 | + aliases: List[str], vulnerability_id=None, summary=None |
| 179 | +): |
177 | 180 | """ |
178 | 181 | Get or create vulnerabilitiy and aliases such that all existing and new |
179 | 182 | aliases point to the same vulnerability |
180 | 183 | """ |
181 | | - existing_vulns = set() |
182 | | - alias_names = set(alias_names) |
183 | | - new_alias_names = set() |
184 | | - for alias_name in alias_names: |
185 | | - try: |
186 | | - alias = Alias.objects.get(alias=alias_name) |
187 | | - existing_vulns.add(alias.vulnerability) |
188 | | - except Alias.DoesNotExist: |
189 | | - new_alias_names.add(alias_name) |
190 | | - |
191 | | - # If given set of aliases point to different vulnerabilities in the |
192 | | - # database, request is malformed |
193 | | - # TODO: It is possible that all those vulnerabilities are actually |
194 | | - # the same at data level, figure out a way to merge them |
195 | | - if len(existing_vulns) > 1: |
196 | | - logger.warning( |
197 | | - f"Given aliases {alias_names} already exist and do not point " |
198 | | - f"to a single vulnerability. Cannot improve. Skipped." |
199 | | - ) |
200 | | - return |
201 | | - |
202 | | - existing_alias_vuln = existing_vulns.pop() if existing_vulns else None |
203 | | - |
204 | | - if ( |
205 | | - existing_alias_vuln |
206 | | - and vulnerability_id |
207 | | - and existing_alias_vuln.vulnerability_id != vulnerability_id |
208 | | - ): |
209 | | - logger.warning( |
210 | | - f"Given aliases {alias_names!r} already exist and point to existing" |
211 | | - f"vulnerability {existing_alias_vuln}. Unable to create Vulnerability " |
212 | | - f"with vulnerability_id {vulnerability_id}. Skipped" |
213 | | - ) |
214 | | - return |
| 184 | + aliases = set(alias.strip() for alias in aliases if alias and alias.strip()) |
| 185 | + new_alias_names, existing_vulns = get_vulns_for_aliases_and_get_new_aliases(aliases) |
| 186 | + |
| 187 | + # All aliases must point to the same vulnerability |
| 188 | + vulnerability = None |
| 189 | + if existing_vulns: |
| 190 | + if len(existing_vulns) != 1: |
| 191 | + vcids = ", ".join(v.vulnerability_id for v in existing_vulns) |
| 192 | + logger.error( |
| 193 | + f"Cannot create vulnerability. " |
| 194 | + f"Aliases {aliases} already exist and point " |
| 195 | + f"to multiple vulnerabilities {vcids}." |
| 196 | + ) |
| 197 | + return |
| 198 | + else: |
| 199 | + vulnerability = existing_vulns.pop() |
| 200 | + |
| 201 | + if vulnerability_id and vulnerability.vulnerability_id != vulnerability_id: |
| 202 | + logger.error( |
| 203 | + f"Cannot create vulnerability. " |
| 204 | + f"Aliases {aliases} already exist and point to a different " |
| 205 | + f"vulnerability {vulnerability} than the requested " |
| 206 | + f"vulnerability {vulnerability_id}." |
| 207 | + ) |
| 208 | + return |
215 | 209 |
|
216 | | - if existing_alias_vuln: |
217 | | - vulnerability = existing_alias_vuln |
218 | | - elif vulnerability_id: |
| 210 | + if vulnerability_id and not vulnerability: |
219 | 211 | try: |
220 | 212 | vulnerability = Vulnerability.objects.get(vulnerability_id=vulnerability_id) |
221 | 213 | except Vulnerability.DoesNotExist: |
222 | | - logger.warning( |
223 | | - f"Given vulnerability_id: {vulnerability_id} does not exist in the database" |
224 | | - ) |
| 214 | + logger.error(f"Cannot get requested vulnerability {vulnerability_id}.") |
225 | 215 | return |
| 216 | + if vulnerability: |
| 217 | + # TODO: We should keep multiple summaries, one for each advisory |
| 218 | + # if summary and summary != vulnerability.summary: |
| 219 | + # logger.warning( |
| 220 | + # f"Inconsistent summary for {vulnerability.vulnerability_id}. " |
| 221 | + # f"Existing: {vulnerability.summary!r}, provided: {summary!r}" |
| 222 | + # ) |
| 223 | + associate_vulnerability_with_aliases(vulnerability=vulnerability, aliases=new_alias_names) |
226 | 224 | else: |
227 | | - vulnerability = Vulnerability(summary=summary) |
228 | | - vulnerability.save() |
| 225 | + try: |
| 226 | + vulnerability = create_vulnerability_and_add_aliases( |
| 227 | + aliases=new_alias_names, summary=summary |
| 228 | + ) |
| 229 | + except Exception as e: |
| 230 | + logger.error( |
| 231 | + f"Cannot create vulnerability with summary {summary!r} and {new_alias_names!r} {e!r}.\n{traceback_format_exc()}." |
| 232 | + ) |
| 233 | + return |
229 | 234 |
|
230 | | - if summary and summary != vulnerability.summary: |
231 | | - logger.warning( |
232 | | - f"Inconsistent summary for {vulnerability!r}. " |
233 | | - f"Existing: {vulnerability.summary}, provided: {summary}" |
234 | | - ) |
| 235 | + return vulnerability |
| 236 | + |
| 237 | + |
| 238 | +def get_vulns_for_aliases_and_get_new_aliases(aliases): |
| 239 | + """ |
| 240 | + Return ``new_aliases`` that are not in the database and |
| 241 | + ``existing_vulns`` that point to the given ``aliases``. |
| 242 | + """ |
| 243 | + new_aliases = set(aliases) |
| 244 | + existing_vulns = set() |
| 245 | + for alias in Alias.objects.filter(alias__in=aliases): |
| 246 | + existing_vulns.add(alias.vulnerability) |
| 247 | + new_aliases.remove(alias.alias) |
| 248 | + return new_aliases, existing_vulns |
235 | 249 |
|
236 | | - for alias_name in new_alias_names: |
| 250 | + |
| 251 | +@transaction.atomic |
| 252 | +def create_vulnerability_and_add_aliases(aliases, summary): |
| 253 | + """ |
| 254 | + Return a new ``vulnerability`` created with ``summary`` |
| 255 | + and associate the ``vulnerability`` with ``aliases``. |
| 256 | + Raise exception if no alias is associated with the ``vulnerability``. |
| 257 | + """ |
| 258 | + vulnerability = Vulnerability(summary=summary) |
| 259 | + vulnerability.save() |
| 260 | + associate_vulnerability_with_aliases(aliases, vulnerability) |
| 261 | + if not vulnerability.aliases.count(): |
| 262 | + raise Exception(f"Vulnerability {vulnerability.vcid} must have one or more aliases") |
| 263 | + return vulnerability |
| 264 | + |
| 265 | + |
| 266 | +def associate_vulnerability_with_aliases(aliases, vulnerability): |
| 267 | + for alias_name in aliases: |
237 | 268 | alias = Alias(alias=alias_name, vulnerability=vulnerability) |
238 | 269 | alias.save() |
239 | 270 | logger.info(f"New alias for {vulnerability!r}: {alias_name}") |
240 | | - |
241 | | - return vulnerability |
|
0 commit comments