99
1010import datetime
1111import logging
12+ from traceback import format_exc as traceback_format_exc
1213from typing import Iterable
1314from typing import List
1415
16+ from django .core .exceptions import ValidationError
17+ from django .db import transaction
18+
1519from vulnerabilities .importer import AdvisoryData
1620from vulnerabilities .importer import Importer
21+ from vulnerabilities .improver import Inference
22+ from vulnerabilities .improvers .default import DefaultImporter
1723from vulnerabilities .models import Advisory
24+ from vulnerabilities .models import Alias
25+ from vulnerabilities .models import Package
26+ from vulnerabilities .models import PackageRelatedVulnerability
27+ from vulnerabilities .models import Vulnerability
28+ from vulnerabilities .models import VulnerabilityReference
29+ from vulnerabilities .models import VulnerabilityRelatedReference
30+ from vulnerabilities .models import VulnerabilitySeverity
31+ from vulnerabilities .models import Weakness
1832
1933logger = logging .getLogger (__name__ )
2034
@@ -41,42 +55,259 @@ def run(self) -> None:
4155 importer_class = self .importer_class
4256 logger .info (f"Starting import for { importer_name } " )
4357 advisory_datas = importer_class ().advisory_data ()
44- count = process_advisories (advisory_datas = advisory_datas , importer_name = importer_name )
58+ count = self . process_advisories (advisory_datas = advisory_datas , importer_name = importer_name )
4559 logger .info (f"Finished import for { importer_name } . Imported { count } advisories." )
4660
61+ def do_import (self , advisories ) -> None :
62+ advisory_importer = DefaultImporter (advisories = advisories )
63+ logger .info (f"Running importer: { advisory_importer .qualified_name } " )
64+ importer_name = advisory_importer .qualified_name
65+ advisories = []
66+ for advisory in advisory_importer .interesting_advisories :
67+ if advisory .date_imported :
68+ continue
69+ logger .info (f"Processing advisory: { advisory !r} " )
70+ try :
71+ inferences = advisory_importer .get_inferences (
72+ advisory_data = advisory .to_advisory_data ()
73+ )
74+ process_inferences (
75+ inferences = inferences ,
76+ advisory = advisory ,
77+ improver_name = importer_name ,
78+ )
79+ except Exception as e :
80+ logger .info (f"Failed to process advisory: { advisory !r} with error { e !r} " )
81+ logger .info ("Finished importing using %s." , advisory_importer .__class__ .qualified_name )
4782
48- def process_advisories (advisory_datas : Iterable [AdvisoryData ], importer_name : str ) -> List :
49- """
50- Insert advisories into the database
51- Return the number of inserted advisories.
52- """
53- count = 0
54- for data in advisory_datas :
55- # https://nvd.nist.gov/vuln/detail/CVE-2013-4314
56- # https://github.com/cms-dev/cms/issues/888#issuecomment-516977572
83+ def process_advisories (
84+ self , advisory_datas : Iterable [AdvisoryData ], importer_name : str
85+ ) -> List :
86+ """
87+ Insert advisories into the database
88+ Return the number of inserted advisories.
89+ """
90+ count = 0
91+ advisories = []
92+ for data in advisory_datas :
93+ try :
94+ obj , created = Advisory .objects .get_or_create (
95+ aliases = data .aliases ,
96+ summary = data .summary ,
97+ affected_packages = [pkg .to_dict () for pkg in data .affected_packages ],
98+ references = [ref .to_dict () for ref in data .references ],
99+ date_published = data .date_published ,
100+ weaknesses = data .weaknesses ,
101+ defaults = {
102+ "created_by" : importer_name ,
103+ "date_collected" : datetime .datetime .now (tz = datetime .timezone .utc ),
104+ },
105+ )
106+ if not obj .date_imported :
107+ advisories .append (obj )
108+ except Exception as e :
109+ logger .error (
110+ f"Error while processing { data !r} with aliases { data .aliases !r} : { e !r} \n { traceback_format_exc ()} "
111+ )
112+ continue
113+ if created :
114+ logger .info (
115+ f"[*] New Advisory with aliases: { obj .aliases !r} , created_by: { obj .created_by } "
116+ )
117+ count += 1
118+ else :
119+ logger .debug (f"Advisory with aliases: { obj .aliases !r} already exists." )
57120 try :
58- data .summary = data .summary .replace ("\x00 " , "\uFFFD " )
59- obj , created = Advisory .objects .get_or_create (
60- aliases = data .aliases ,
61- summary = data .summary ,
62- affected_packages = [pkg .to_dict () for pkg in data .affected_packages ],
63- references = [ref .to_dict () for ref in data .references ],
64- date_published = data .date_published ,
65- weaknesses = data .weaknesses ,
66- defaults = {
67- "created_by" : importer_name ,
68- "date_collected" : datetime .datetime .now (tz = datetime .timezone .utc ),
69- },
70- )
121+ self .do_import (advisories )
71122 except Exception as e :
72- logger .error (f"Error while processing { data !r} with aliases { data .aliases !r} : { e !r} " )
123+ logger .error (
124+ f"Error while processing advisories from { importer_name !r} : { e !r} \n { traceback_format_exc ()} "
125+ )
126+ return count
127+
128+
129+ @transaction .atomic
130+ def process_inferences (inferences : List [Inference ], advisory : Advisory , improver_name : str ):
131+ """
132+ Return number of inferences processed.
133+ An atomic transaction that updates both the Advisory (e.g. date_imported)
134+ and processes the given inferences to create or update corresponding
135+ database fields.
136+
137+ This avoids failing the entire improver when only a single inference is
138+ erroneous. Also, the atomic transaction for every advisory and its
139+ inferences makes sure that date_imported of advisory is consistent.
140+ """
141+ inferences_processed_count = 0
142+
143+ if not inferences :
144+ logger .warning (f"Nothing to improve. Source: { improver_name } Advisory id: { advisory .id } " )
145+ return inferences_processed_count
146+
147+ logger .info (f"Improving advisory id: { advisory .id } " )
148+
149+ for inference in inferences :
150+ vulnerability = get_or_create_vulnerability_and_aliases (
151+ vulnerability_id = inference .vulnerability_id ,
152+ alias_names = inference .aliases ,
153+ summary = inference .summary ,
154+ )
155+
156+ if not vulnerability :
157+ logger .warning (f"Unable to get vulnerability for inference: { inference !r} " )
73158 continue
74- if created :
75- logger .info (
76- f"[*] New Advisory with aliases: { obj .aliases !r} , created_by: { obj .created_by } "
159+
160+ for ref in inference .references :
161+
162+ reference = VulnerabilityReference .objects .get_or_none (
163+ reference_id = ref .reference_id ,
164+ url = ref .url ,
77165 )
78- count += 1
79- else :
80- logger .debug (f"Advisory with aliases: { obj .aliases !r} already exists. Skipped." )
81166
82- return count
167+ if not reference :
168+ reference = create_valid_vulnerability_reference (
169+ reference_id = ref .reference_id ,
170+ url = ref .url ,
171+ )
172+ if not reference :
173+ continue
174+
175+ VulnerabilityRelatedReference .objects .update_or_create (
176+ reference = reference ,
177+ vulnerability = vulnerability ,
178+ )
179+
180+ for severity in ref .severities :
181+ _vs , updated = VulnerabilitySeverity .objects .update_or_create (
182+ scoring_system = severity .system .identifier ,
183+ reference = reference ,
184+ defaults = {
185+ "value" : str (severity .value ),
186+ "scoring_elements" : str (severity .scoring_elements ),
187+ },
188+ )
189+ if updated :
190+ logger .info (
191+ f"Severity updated for reference { ref !r} to value: { severity .value !r} "
192+ f"and scoring_elements: { severity .scoring_elements !r} "
193+ )
194+
195+ for affected_purl in inference .affected_purls or []:
196+ vulnerable_package = Package .objects .get_or_create_from_purl (purl = affected_purl )
197+ PackageRelatedVulnerability (
198+ vulnerability = vulnerability ,
199+ package = vulnerable_package ,
200+ created_by = improver_name ,
201+ confidence = inference .confidence ,
202+ fix = False ,
203+ ).update_or_create ()
204+
205+ if inference .fixed_purl :
206+ fixed_package = Package .objects .get_or_create_from_purl (purl = inference .fixed_purl )
207+ PackageRelatedVulnerability (
208+ vulnerability = vulnerability ,
209+ package = fixed_package ,
210+ created_by = improver_name ,
211+ confidence = inference .confidence ,
212+ fix = True ,
213+ ).update_or_create ()
214+
215+ if inference .weaknesses and vulnerability :
216+ for cwe_id in inference .weaknesses :
217+ cwe_obj , created = Weakness .objects .get_or_create (cwe_id = cwe_id )
218+ cwe_obj .vulnerabilities .add (vulnerability )
219+ cwe_obj .save ()
220+ inferences_processed_count += 1
221+
222+ advisory .date_imported = datetime .datetime .now (tz = datetime .timezone .utc )
223+ advisory .save ()
224+ return inferences_processed_count
225+
226+
227+ def create_valid_vulnerability_reference (url , reference_id = None ):
228+ """
229+ Create and return a new validated VulnerabilityReference from a
230+ ``url`` and ``reference_id``.
231+ Return None and log a warning if this is not a valid reference.
232+ """
233+ reference = VulnerabilityReference (
234+ reference_id = reference_id ,
235+ url = url ,
236+ )
237+
238+ try :
239+ reference .full_clean ()
240+ except ValidationError as e :
241+ logger .warning (f"Invalid vulnerability reference: { reference !r} : { e } " )
242+ return
243+
244+ reference .save ()
245+ return reference
246+
247+
248+ def get_or_create_vulnerability_and_aliases (alias_names , vulnerability_id = None , summary = None ):
249+ """
250+ Get or create vulnerabilitiy and aliases such that all existing and new
251+ aliases point to the same vulnerability
252+ """
253+ existing_vulns = set ()
254+ alias_names = set (alias_names )
255+ new_alias_names = set ()
256+ for alias_name in alias_names :
257+ try :
258+ alias = Alias .objects .get (alias = alias_name )
259+ existing_vulns .add (alias .vulnerability )
260+ except Alias .DoesNotExist :
261+ new_alias_names .add (alias_name )
262+
263+ # If given set of aliases point to different vulnerabilities in the
264+ # database, request is malformed
265+ # TODO: It is possible that all those vulnerabilities are actually
266+ # the same at data level, figure out a way to merge them
267+ if len (existing_vulns ) > 1 :
268+ logger .warning (
269+ f"Given aliases { alias_names } already exist and do not point "
270+ f"to a single vulnerability. Cannot improve. Skipped."
271+ )
272+ return
273+
274+ existing_alias_vuln = existing_vulns .pop () if existing_vulns else None
275+
276+ if (
277+ existing_alias_vuln
278+ and vulnerability_id
279+ and existing_alias_vuln .vulnerability_id != vulnerability_id
280+ ):
281+ logger .warning (
282+ f"Given aliases { alias_names !r} already exist and point to existing"
283+ f"vulnerability { existing_alias_vuln } . Unable to create Vulnerability "
284+ f"with vulnerability_id { vulnerability_id } . Skipped"
285+ )
286+ return
287+
288+ if existing_alias_vuln :
289+ vulnerability = existing_alias_vuln
290+ elif vulnerability_id :
291+ try :
292+ vulnerability = Vulnerability .objects .get (vulnerability_id = vulnerability_id )
293+ except Vulnerability .DoesNotExist :
294+ logger .warning (
295+ f"Given vulnerability_id: { vulnerability_id } does not exist in the database"
296+ )
297+ return
298+ else :
299+ vulnerability = Vulnerability (summary = summary )
300+ vulnerability .save ()
301+
302+ if summary and summary != vulnerability .summary :
303+ logger .warning (
304+ f"Inconsistent summary for { vulnerability !r} . "
305+ f"Existing: { vulnerability .summary } , provided: { summary } "
306+ )
307+
308+ for alias_name in new_alias_names :
309+ alias = Alias (alias = alias_name , vulnerability = vulnerability )
310+ alias .save ()
311+ logger .info (f"New alias for { vulnerability !r} : { alias_name } " )
312+
313+ return vulnerability
0 commit comments