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

Revision 1382, 22.8 kB (checked in by ggpolo, 6 years ago)

some doc improvement, cleaner code for _update_date in TLBase, fixed a bug related to changing filters of same lenght for TLGraph

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