root/branch/ggpolo/umitDB/InventoryChanges.py @ 1431

Revision 1431, 23.5 kB (checked in by ggpolo, 6 years ago)

Huge speed gain on XML insertion into databaseng, including insertion on Inventory and calculating changes.

Line 
1# Copyright (C) 2007 Insecure.Com LLC.
2#
3# Author:  Guilherme Polo <ggpolo@gmail.com>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18# USA
19
20from umitCore.I18N import _
21
22from umitDB.Connection import ConnectDB
23from umitDB.Store import RawStore
24from umitDB.Retrieve import InventoryRetrieve
25
26class BadParams(Exception):
27    pass
28
29class ConnectToUpdate(ConnectDB, InventoryRetrieve, RawStore):
30    """
31    You should use this when doing inventory changes update exclusively
32    in database.
33    """
34
35    def __init__(self, database):
36        ConnectDB.__init__(self, database)
37        InventoryRetrieve.__init__(self, self.conn, self.cursor)
38        RawStore.__init__(self, self.conn, self.cursor)
39
40
41class UpdateChanges:
42    """
43    Updates/creates list of changes for Inventory.
44    """
45   
46    def __init__(self, invdb, fk_inventory=None):
47       
48        self.invdb = invdb
49        self.fk_inventory = fk_inventory
50       
51        if fk_inventory:
52            self.do_update(fk_inventory)
53
54       
55    def do_update(self, inv_id):
56        """
57        Build changes found in scans related to an Inventory id and
58        updates everything necessary.
59        """
60        addresses = [ ]
61       
62        # get scan ids and finish time for especified inventory
63        finish_data = self.invdb.get_finish_data_for_inventory_from_db(inv_id)
64         
65        # retrieve host addresses for especified inventory id
66        for scan in finish_data:
67            # retrieve host ids for each scan
68            for host in self.invdb.get_hosts_id_for_scan_from_db(scan[0]):
69                # retrieve ipv4 address for host
70                addr = self.invdb.get_ipv4_for_host_from_db(host[0])
71               
72                if addr in addresses:
73                    continue
74               
75                addresses.append(addr)
76                       
77        # generate changes list for each address in especified inventory
78        for addr in addresses:
79           
80            # get all host pks for especified inventory and for each address
81            # in addresses
82            base_data = self.invdb.get_hosts_base_data_for_inventory_from_db(addr, 
83                                                                       inv_id)
84           
85            if not base_data:
86                print "Inventory id %d has no data for host \
87address '%s' yet." % (inv_id, addr)
88                continue
89           
90            # dict where all data will be stored
91            data_dict = { }
92       
93            # get host and scan ids
94            scan_ids = [item[0] for item in finish_data]
95            host_ids = [item[0] for item in base_data]
96           
97            indexes = [ ]
98            count = 0
99            # get indexes where there is host data
100            for indx, item in enumerate(scan_ids):
101                try:
102                    host_ids[count]
103                except IndexError:
104                    continue
105                else:
106                    indexes.append(scan_ids.index(host_ids[count]))
107                    count += 1
108                   
109            down_time = self._check_for_downtime(finish_data, base_data)
110            for item in down_time[1]:
111                data_dict[item] = (_("Availability"), _("Host down"), 
112                                   down_time[0][item], -1, -1)
113               
114            # load initial data for doing comparison
115            date = finish_data[indexes[0]][1]
116            hostA = base_data[0]
117            self.invdb.use_dict_cursor()
118            pdata1 = self.invdb.get_portid_and_state_for_host_from_db(hostA[1])
119            self.invdb.use_standard_cursor()
120            fpinfo1 = self.invdb.get_fingerprint_info_for_host_from_db(hostA[1])
121            osmatch1 = self.invdb.get_osmatch_for_host_from_db(hostA[1])
122            osclasses1 = self.invdb.get_osclasses_for_host_from_db(hostA[1])
123            pcdata1 = self.invdb.get_portid_and_fks_for_host_from_db(hostA[1])
124           
125            host_count = 1
126           
127            # now, load following data and compare against hostA
128            for indx, item in enumerate(base_data[host_count:]):
129                hostB = item
130               
131                # load data to compare against hostA
132                self.invdb.use_dict_cursor()
133                pdata2 = self.invdb.get_portid_and_state_for_host_from_db(hostB[1])
134                self.invdb.use_standard_cursor()
135                fpinfo2 = self.invdb.get_fingerprint_info_for_host_from_db(hostB[1])
136                osmatch2 = self.invdb.get_osmatch_for_host_from_db(hostB[1])
137                osclasses2 = self.invdb.get_osclasses_for_host_from_db(hostB[1])
138                pcdata2 = self.invdb.get_portid_and_fks_for_host_from_db(hostB[1])
139               
140                # compare old data against new data
141                category, diff_text = self._compare_data(pdata2, fpinfo2, 
142                                                         osmatch2, osclasses2,
143                                                         pcdata2,
144                                                         pdata1, fpinfo1, 
145                                                         osmatch1, osclasses1,
146                                                         pcdata1)
147
148                # store in data_dict current result
149                data_dict[hostA[0]] = (category, diff_text, date, hostA[1], 
150                                       hostB[1])
151               
152                # swap data
153                pdata1 = pdata2
154                pcdata1 = pcdata2
155                fpinfo1 = fpinfo2
156                osmatch1 = osmatch2
157                osclasses1 = osclasses2
158                hostA = hostB
159               
160                # get next date
161                date = finish_data[indexes[indx+1]][1]
162           
163            # now load the first entry
164            data_dict[hostA[0]] = (_("Inventory"), 
165                                   _("Host added to the Inventory."), date,
166                                   hostA[1], hostA[1])
167           
168            self._insert_changes(data_dict, addr, inv_id)
169           
170           
171    def _insert_changes(self, data_dict, addr_id, inventory_id):
172        """
173        Insert changes in database, this was collected at do_update method.
174        """
175        # sort dict keys in descendent order
176        dict_keys = data_dict.keys()
177        dict_keys.sort()
178        dict_keys.reverse()
179           
180        # insert data into database
181        fk_address = self.invdb.get_address_id_for_address_from_db(addr_id)
182        for key in dict_keys:
183            affected = data_dict[key][0]
184            text = data_dict[key][1]
185            date = data_dict[key][2]
186            new_hostid = data_dict[key][3]
187            old_hostid = data_dict[key][4]
188           
189            # check if category 'affected' already exists on database
190            fk_category = self.invdb.get_inventory_change_category_id(affected)
191            if not fk_category:
192                # didn't exist, create it now
193                self.invdb.insert_inventory_change_category_db(affected)
194                fk_category = self.invdb.get_id_for("inventory_change_category")
195                   
196            # check if comparison is already in database
197            # (this should have been done at earlier stage, but for now
198            #  it is being done here)
199            ret = self.invdb.get_inventory_comparison(old_hostid, new_hostid, 
200                                                      date, inventory_id)
201                   
202            if not ret:
203                # need to insert new comparison
204                self.invdb.insert_inventory_comparison_db(old_hostid, new_hostid, 
205                                                    date, text, inventory_id, 
206                                                    fk_category, fk_address)
207
208           
209    def _compare_data(self, pdata2, fpinfo2, osmatch2, osclasses2, pcdata2,
210                      pdata1, fpinfo1, osmatch1, osclasses1, pcdata1):
211        """
212        Compare two sets of data, checking how it changes.
213        """
214        host_diff = ''
215        common_text = ''
216        ports_only = None # ports diff only
217        fp_only = None # fingerprint diff only
218
219        # compare pdataNs
220        if pdata1 != pdata2:
221            host_diff = self._ports_diff(pdata2, pdata1)
222            fp_only = False
223            ports_only = True
224
225        # compare pcdataNs (pc here stands for port complete, wich means
226        # we will be dealing with other port infos beyond id and states,
227        # like we did right above.
228        old_ports = { }
229        new_ports = { }
230
231        self.invdb.use_dict_cursor()
232        for pd in pcdata2:
233            fpd = self.invdb.get_port_data_for_pdata_from_db(pd[2], pd[3], 
234                                                             pd[1])
235            old_ports[pd[0]] = fpd
236
237        for pd in pcdata1:
238            fpd = self.invdb.get_port_data_for_pdata_from_db(pd[2], pd[3], 
239                                                             pd[1])
240            new_ports[pd[0]] = fpd
241        self.invdb.use_standard_cursor()
242       
243        info_changes = [ ]
244        for key, value in old_ports.items():
245            if key in new_ports:
246                if value != new_ports[key]:
247                    info_changes.append(key)
248       
249        if info_changes:
250            if not ports_only:
251                ports_only = True
252
253            verb, plural = self.conjugate(info_changes, False)
254            ports_str = ', '.join([str(p) for p in info_changes])
255            host_diff = ''.join([host_diff,
256                                 _("Port%s %s %s changed info!" % (plural, 
257                                                          ports_str, verb))])
258
259        # compare fpinfoNs
260        # dont consider uptime and lastboot in fingerprint (will probably
261        # not consider others too)
262        if fpinfo1 and (fpinfo1[2:] != fpinfo2[2:]):
263            space = host_diff and ' ' or ''
264            common_text = _('%sFingerprint, ') % space
265            ports_only = False
266            if fp_only is None:
267                fp_only = True
268
269        # compare osmatchNs
270        if osmatch1 != osmatch2:
271            space = host_diff and ' ' or ''
272            common_text = ''.join([common_text, _("OS Match, ")])
273            fp_only = False
274            ports_only = False
275
276        # compare osclassesNs
277        if osclasses1 != osclasses2:
278            space = host_diff and ' ' or ''
279            if len(common_text) == len("Fingerprint, "):
280                common_text = ''.join([common_text[:-2], " "])
281            if common_text:
282                common_text = ''.join([common_text, _("and OS Classes")])
283            else:
284                common_text = ''.join([common_text, _("OS Classes")])
285            fp_only = False
286            ports_only = False
287               
288        if common_text:
289            if fp_only or len(common_text) == len(" Fingerprint, "):
290                common_text = common_text[:-2]
291
292            host_diff = ''.join([host_diff, common_text, _(" changed.")])
293
294        # check diff
295        if host_diff:
296            if ports_only:
297                affected = _("Ports")
298            elif fp_only:
299                affected = _("Fingerprint")
300            else:
301                affected = _("Several")
302               
303        else:
304            # Nothing here means "Almost nothing", there could be
305            # changes in extraports for example.
306            affected = _("Nothing")
307            host_diff = _("No noticeables changes since last sucessfull scan.")
308           
309           
310        return (affected, host_diff)
311 
312
313    def conjugate(self, alist, toBe=True):
314        """
315        Do conjugation based on alist size.
316        It expects that alist is not empty.
317        """
318        if toBe:
319            verbs = ('are', 'is')
320        else:
321            verbs = ('have', 'has')
322
323        if len(alist) > 1:
324            verb = verbs[0]
325            plural = _('s')
326        else:
327            verb = verbs[1]
328            plural = _('')
329   
330        return verb, plural
331
332
333    def _ports_diff(self, old, new):
334        """
335        Return a prettier difference between pdata.
336        """
337        closed_text = _("closed")
338        open_text = _("open")
339        now_text = _("now")
340        port_text = _("Port")
341        and_text = _("and")
342       
343        # first build dict where portid is the key
344        old_dict = { }
345        new_dict = { }
346
347        for d in old:
348            old_dict[d['portid']] = d['state']
349       
350        for d in new:
351            new_dict[d['portid']] = d['state']
352
353        # check for port changes now
354        closed_ports = [ ]
355        open_ports = [ ]
356
357        for key, value in old_dict.items():
358            if key in new_dict:
359                new_value = new_dict[key]
360
361                if value != new_value:
362                    print value, new_value, 'differs but Im not doing nothing'
363
364            else:
365                closed_ports.append(key)
366
367        for key, value in new_dict.items():
368            if key in old_dict:
369                old_value = old_dict[key]
370
371                if value != old_value:
372                    print value, old_value, 'differs but Im not doing nothing'
373
374            else:
375                open_ports.append(key)
376
377        text = ''
378        if closed_ports:
379            verb, plural = self.conjugate(closed_ports)
380            closed_ports = ', '.join([str(p) for p in closed_ports])
381            closed_ports = "%s%s %s %s %s %s." % (port_text, plural, 
382                                                  closed_ports, verb, 
383                                                  closed_text, now_text)
384
385        if open_ports:
386            verb, plural = self.conjugate(open_ports)
387            open_ports = ', '.join([str(p) for p in open_ports])
388            open_ports = "%s%s %s %s %s %s." % (port_text, plural, 
389                                                open_ports, verb, 
390                                                open_text, now_text)
391
392        if open_ports and closed_ports:
393            text = ' '.join([open_ports[:-1 -len(now_text) -1], and_text,
394                             closed_ports])
395        elif open_ports:
396            text = open_ports
397        elif closed_ports:
398            text = closed_ports
399
400        return text
401   
402 
403    def _check_for_downtime(self, all_scans, host_scans):
404        """
405        Return scan id associated to a date showing in what scans a host
406        was down.
407        """
408        down_d = { }
409        down_order = [ ]
410        d = 0
411        for item in all_scans:
412            try:
413                host_scans[d][0]
414            except IndexError:
415                down_order.append(item[0])
416                down_d[item[0]] = item[1]
417            else:
418                if item[0] == host_scans[d][0]:
419                    d += 1
420                else:
421                    down_order.append(item[0])
422                    down_d[item[0]] = item[1]
423       
424        return (down_d, down_order)
425
426
427class ChangesRetrieve(InventoryRetrieve):
428    """
429    Retrieves changes from database in many ways.
430    """
431   
432    def __init__(self, conn, cursor):
433        InventoryRetrieve.__init__(self, conn, cursor)
434       
435
436    def get_categories_id_name(self):
437        """
438        Return all category_id, category_name from database.
439        """
440        self.cursor.execute("SELECT * FROM inventory_change_category")
441        data = self.cursor.fetchall()
442       
443        return data
444   
445       
446    def get_categories_name(self):
447        """
448        Return all categories name from database.
449        """
450        self.cursor.execute("SELECT name FROM inventory_change_category")
451        ctg = self.cursor.fetchall()
452       
453        return ctg
454   
455   
456    def get_category_name_by_id(self, cid):
457        """
458        Return category name with especified id.
459        """
460        name = self.cursor.execute("SELECT name FROM inventory_change_category\
461                                    WHERE pk=?", (cid, )).fetchone()
462
463        if name:
464            return name[0]
465   
466   
467    def get_category_id_by_name(self, name):
468        """
469        Return category id with especified name.
470        """
471        cid = self.cursor.execute("SELECT pk FROM inventory_change_category \
472                                   WHERE name=?", (name, )).fetchone()[0]
473       
474        return cid
475       
476
477    def timerange_changes_data_generic(self, start, end, category, inventory,
478                                       hostaddr):
479        """
480        Selects what method to use to grab changes data.
481        """
482        # start and end should always be present
483        if not start or not end:
484            raise BadParams("You should especify range start and range end")
485
486        # check for missing args for inventory and hostaddr
487        if inventory and not hostaddr or not inventory and hostaddr:
488            raise BadParams("You should especify a hostaddr and inventory")
489
490
491        if category:
492            return self.timerange_changes_categoryid_data(category, start, 
493                                                          end, inventory, 
494                                                          hostaddr)
495        else:
496            return self.timerange_changes_data(start, end, inventory, 
497                                                hostaddr)
498
499       
500    def timerange_changes_data(self, start, end, fk_inventory=None, 
501                               fk_address=None):
502        """
503        Retrieve changes data in a timerange.
504        """
505        if fk_inventory:
506            data = self.cursor.execute("SELECT fk_category, short_descritpion,\
507            entry_date, fk_inventory, fk_address, old_hostid, new_hostid \
508            FROM _inventory_changes WHERE fk_inventory=? AND fk_address=? \
509            AND entry_date >= ? AND entry_date < ? ORDER BY entry_date DESC",
510            (fk_inventory, fk_address, start, end)).fetchall()
511        else:
512            data = self.cursor.execute("SELECT fk_category, short_description,\
513            entry_date, fk_inventory, fk_address, old_hostid, new_hostid \
514            FROM _inventory_changes WHERE entry_date >= ? AND \
515            entry_date < ? ORDER BY entry_date DESC", (start, end)).fetchall()
516       
517        return data
518       
519    def timerange_changes_categoryid_data(self, fk_category, start, end,
520                                          fk_inventory=None, fk_address=None):
521        """
522        Retrieve changes data in a timerange for an especific category id.
523        """
524        if fk_inventory:
525            data = self.cursor.execute("SELECT fk_category, short_description,\
526            entry_date, fk_inventory, fk_address, old_hostid, new_hostid \
527            FROM _inventory_changes WHERE fk_inventory=? AND fk_address=? AND \
528            fk_category=? AND entry_date >= ? AND entry_date < ? \
529            ORDER BY entry_date DESC", (fk_inventory, fk_address, fk_category,
530                                        start, end)).fetchall()
531        else:
532            data = self.cursor.execute("SELECT fk_category, short_description,\
533            entry_date, fk_inventory, fk_address, old_hostid, new_hostid \
534            FROM _inventory_changes WHERE fk_category=? AND entry_date >= ? \
535            AND entry_date < ? ORDER BY entry_date DESC", (fk_category, start, 
536                                                           end)).fetchall()
537       
538        return data
539   
540   
541    def timerange_changes_categoryname_data(self, category, start, end):
542        """
543        Retrieve changes data in a timerange for an especific category name.
544        """
545        data = self.cursor.execute("SELECT fk_category, short_description, \
546        entry_date, fk_inventory, fk_address, old_hostid, new_hostid \
547        FROM _inventory_changes JOIN inventory_change_category as icc ON \
548        (_inventory_changes.fk_category = icc.pk) WHERE icc.name=? AND \
549        entry_date >= ? AND entry_date < ? \
550        ORDER BY entry_date DESC", (category, start, end)).fetchall()
551       
552        return data
553       
554
555    def timerange_changes_count_generic(self, start, end, category, inventory,
556                                        hostaddr):
557        """
558        Selects what method to use to grab changes count.
559        """
560        # start and end should always be present
561        if not start or not end:
562            raise BadParams("You should especify range start and range end")
563
564        # check for missing args for inventory and hostaddr
565        if inventory and not hostaddr or not inventory and hostaddr:
566            raise BadParams("You should especify a hostaddr and inventory")
567
568
569        if category:
570            return self.timerange_changes_categoryid_count(category, start, 
571                                                           end, inventory, 
572                                                           hostaddr)
573        else:
574            return self.timerange_changes_count(start, end, inventory, 
575                                                hostaddr)
576
577
578    def timerange_changes_count(self, start, end, fk_inventory=None, 
579                                fk_hostaddr=None):
580        """
581        Get number of changes in a timerange.
582        """
583        if fk_inventory:
584            count = self.cursor.execute("SELECT pk FROM _inventory_changes \
585                               WHERE fk_inventory=? AND fk_address=? AND \
586                               entry_date >= ? AND entry_date < ?", 
587                               (fk_inventory, fk_hostaddr, start, 
588                                end)).fetchall()
589        else:
590            count = self.cursor.execute("SELECT pk FROM _inventory_changes \
591                               WHERE entry_date >= ? AND entry_date < ?", 
592                               (start, end)).fetchall()
593       
594        return len(count)
595   
596   
597    def timerange_changes_categoryid_count(self, fk_category, start, end,
598                                           fk_inventory=None, 
599                                           fk_hostaddr=None):
600        """
601        Get number of changes in a timerange for an especific category id.
602        """
603        if fk_inventory:
604            self.cursor.execute("SELECT pk FROM _inventory_changes WHERE \
605                                 fk_category=? AND fk_inventory=? AND \
606                                 fk_address=? AND entry_date >= ? AND \
607                                 entry_date < ?", (fk_category, 
608                                 fk_inventory, fk_hostaddr, start, end))
609        else:
610            self.cursor.execute("SELECT pk FROM _inventory_changes WHERE \
611                                 fk_category=? AND entry_date >= ? AND \
612                                 entry_date < ?", (fk_category, start, end))
613
614        count = self.cursor.fetchall()
615
616        return len(count)
617       
618       
619    def timerange_changes_categoryname_count(self, category, start, end):
620        """
621        Get number of changes in a timerange for an especific category name.
622        """
623        # not being used
624        count = self.cursor.execute("SELECT pk FROM _inventory_changes JOIN \
625                           inventory_change_category as icc ON \
626                           (_inventory_changes.fk_category = icc.pk) WHERE \
627                           icc.name=? AND entry_date > ? AND entry_date < ?", 
628                           (category, start, end)).fetchall()
629       
630        return len(count)
631
632   
Note: See TracBrowser for help on using the browser.