import sys
import os
+import tdb
sys.path.insert(0, "bin/python")
-import samba.gpo as gpo
-import optparse
-import ldb
-from samba.auth import system_session
-import samba.getopt as options
-from samba.samdb import SamDB
-from samba.netcmd import gpo as gpo_user
-import codecs
from samba import NTSTATUSError
from ConfigParser import ConfigParser
from StringIO import StringIO
+from abc import ABCMeta, abstractmethod
+import xml.etree.ElementTree as etree
+import re
+from samba.net import Net
+from samba.dcerpc import nbt
+from samba import smb
+import samba.gpo as gpo
-class gp_ext(object):
- def list(self, rootpath):
+try:
+ from enum import Enum
+ GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
+except ImportError:
+ class GPOSTATE:
+ APPLY = 1
+ ENFORCE = 2
+ UNAPPLY = 3
+
+class gp_log:
+ ''' Log settings overwritten by gpo apply
+ The gp_log is an xml file that stores a history of gpo changes (and the
+ original setting value).
+
+ The log is organized like so:
+
+<gp>
+ <user name="KDC-1$">
+ <applylog>
+ <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
+ </applylog>
+ <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
+ <gp_ext name="System Access">
+ <attribute name="minPwdAge">-864000000000</attribute>
+ <attribute name="maxPwdAge">-36288000000000</attribute>
+ <attribute name="minPwdLength">7</attribute>
+ <attribute name="pwdProperties">1</attribute>
+ </gp_ext>
+ <gp_ext name="Kerberos Policy">
+ <attribute name="ticket_lifetime">1d</attribute>
+ <attribute name="renew_lifetime" />
+ <attribute name="clockskew">300</attribute>
+ </gp_ext>
+ </guid>
+ </user>
+</gp>
+
+ Each guid value contains a list of extensions, which contain a list of
+ attributes. The guid value represents a GPO. The attributes are the values
+ of those settings prior to the application of the GPO.
+ The list of guids is enclosed within a user name, which represents the user
+ the settings were applied to. This user may be the samaccountname of the
+ local computer, which implies that these are machine policies.
+ The applylog keeps track of the order in which the GPOs were applied, so
+ that they can be rolled back in reverse, returning the machine to the state
+ prior to policy application.
+ '''
+ def __init__(self, user, gpostore, db_log=None):
+ ''' Initialize the gp_log
+ param user - the username (or machine name) that policies are
+ being applied to
+ param gpostore - the GPOStorage obj which references the tdb which
+ contains gp_logs
+ param db_log - (optional) a string to initialize the gp_log
+ '''
+ self._state = GPOSTATE.APPLY
+ self.gpostore = gpostore
+ self.username = user
+ if db_log:
+ self.gpdb = etree.fromstring(db_log)
+ else:
+ self.gpdb = etree.Element('gp')
+ self.user = user
+ user_obj = self.gpdb.find('user[@name="%s"]' % user)
+ if user_obj is None:
+ user_obj = etree.SubElement(self.gpdb, 'user')
+ user_obj.attrib['name'] = user
+
+ def state(self, value):
+ ''' Policy application state
+ param value - APPLY, ENFORCE, or UNAPPLY
+
+ The behavior of the gp_log depends on whether we are applying policy,
+ enforcing policy, or unapplying policy. During an apply, old settings
+ are recorded in the log. During an enforce, settings are being applied
+ but the gp_log does not change. During an unapply, additions to the log
+ should be ignored (since function calls to apply settings are actually
+ reverting policy), but removals from the log are allowed.
+ '''
+ # If we're enforcing, but we've unapplied, apply instead
+ if value == GPOSTATE.ENFORCE:
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ apply_log = user_obj.find('applylog')
+ if apply_log is None or len(apply_log) == 0:
+ self._state = GPOSTATE.APPLY
+ else:
+ self._state = value
+ else:
+ self._state = value
+
+ def set_guid(self, guid):
+ ''' Log to a different GPO guid
+ param guid - guid value of the GPO from which we're applying
+ policy
+ '''
+ self.guid = guid
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ obj = user_obj.find('guid[@value="%s"]' % guid)
+ if obj is None:
+ obj = etree.SubElement(user_obj, 'guid')
+ obj.attrib['value'] = guid
+ if self._state == GPOSTATE.APPLY:
+ apply_log = user_obj.find('applylog')
+ if apply_log is None:
+ apply_log = etree.SubElement(user_obj, 'applylog')
+ item = etree.SubElement(apply_log, 'guid')
+ item.attrib['count'] = '%d' % (len(apply_log)-1)
+ item.attrib['value'] = guid
+
+ def apply_log_pop(self):
+ ''' Pop a GPO guid from the applylog
+ return - last applied GPO guid
+
+ Removes the GPO guid last added to the list, which is the most recently
+ applied GPO.
+ '''
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ apply_log = user_obj.find('applylog')
+ if apply_log is not None:
+ ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
+ if ret is not None:
+ apply_log.remove(ret)
+ return ret.attrib['value']
+ if len(apply_log) == 0 and apply_log in user_obj:
+ user_obj.remove(apply_log)
return None
- def __str__(self):
- return "default_gp_ext"
+ def store(self, gp_ext_name, attribute, old_val):
+ ''' Store an attribute in the gp_log
+ param gp_ext_name - Name of the extension applying policy
+ param attribute - The attribute being modified
+ param old_val - The value of the attribute prior to policy
+ application
+ '''
+ if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
+ return None
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+ assert guid_obj is not None, "gpo guid was not set"
+ ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is None:
+ ext = etree.SubElement(guid_obj, 'gp_ext')
+ ext.attrib['name'] = gp_ext_name
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is None:
+ attr = etree.SubElement(ext, 'attribute')
+ attr.attrib['name'] = attribute
+ attr.text = old_val
+
+ def retrieve(self, gp_ext_name, attribute):
+ ''' Retrieve a stored attribute from the gp_log
+ param gp_ext_name - Name of the extension which applied policy
+ param attribute - The attribute being retrieved
+ return - The value of the attribute prior to policy
+ application
+ '''
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+ assert guid_obj is not None, "gpo guid was not set"
+ ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is not None:
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is not None:
+ return attr.text
+ return None
+
+ def list(self, gp_extensions):
+ ''' Return a list of attributes, their previous values, and functions
+ to set them
+ param gp_extensions - list of extension objects, for retrieving attr to
+ func mappings
+ return - list of (attr, value, apply_func) tuples for
+ unapplying policy
+ '''
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+ assert guid_obj is not None, "gpo guid was not set"
+ ret = []
+ data_maps = {}
+ for gp_ext in gp_extensions:
+ data_maps.update(gp_ext.apply_map())
+ exts = guid_obj.findall('gp_ext')
+ if exts is not None:
+ for ext in exts:
+ attrs = ext.findall('attribute')
+ for attr in attrs:
+ func = None
+ if attr.attrib['name'] in data_maps[ext.attrib['name']]:
+ func = data_maps[ext.attrib['name']]\
+ [attr.attrib['name']][-1]
+ else:
+ for dmap in data_maps[ext.attrib['name']].keys():
+ if data_maps[ext.attrib['name']][dmap][0] == \
+ attr.attrib['name']:
+ func = data_maps[ext.attrib['name']][dmap][-1]
+ break
+ ret.append((attr.attrib['name'], attr.text, func))
+ return ret
+ def delete(self, gp_ext_name, attribute):
+ ''' Remove an attribute from the gp_log
+ param gp_ext_name - name of extension from which to remove the
+ attribute
+ param attribute - attribute to remove
+ '''
+ user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+ guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+ assert guid_obj is not None, "gpo guid was not set"
+ ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is not None:
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is not None:
+ ext.remove(attr)
+ if len(ext) == 0:
+ guid_obj.remove(ext)
+
+ def commit(self):
+ ''' Write gp_log changes to disk '''
+ self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
+
+class GPOStorage:
+ def __init__(self, log_file):
+ if os.path.isfile(log_file):
+ self.log = tdb.open(log_file)
+ else:
+ self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
+
+ def start(self):
+ self.log.transaction_start()
+
+ def get_int(self, key):
+ try:
+ return int(self.log.get(key))
+ except TypeError:
+ return None
+
+ def get(self, key):
+ return self.log.get(key)
+
+ def get_gplog(self, user):
+ return gp_log(user, self, self.log.get(user))
+
+ def store(self, key, val):
+ self.log.store(key, val)
+
+ def cancel(self):
+ self.log.transaction_cancel()
+
+ def delete(self, key):
+ self.log.delete(key)
+
+ def commit(self):
+ self.log.transaction_commit()
+
+ def __del__(self):
+ self.log.close()
-class inf_to_ldb(object):
- '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
- hashmaps it to the Samba parameter, which then uses an ldb object to update the
- parameter to Samba4. Not registry oriented whatsoever.
- '''
+class gp_ext(object):
+ __metaclass__ = ABCMeta
- def __init__(self, logger, ldb, dn, attribute, val):
+ def __init__(self, logger):
self.logger = logger
- self.ldb = ldb
- self.dn = dn
- self.attribute = attribute
- self.val = val
- def ch_minPwdAge(self, val):
- self.logger.info('KDC Minimum Password age was changed from %s to %s' % (self.ldb.get_minPwdAge(), val))
- self.ldb.set_minPwdAge(val)
+ @abstractmethod
+ def list(self, rootpath):
+ pass
+
+ @abstractmethod
+ def apply_map(self):
+ pass
- def ch_maxPwdAge(self, val):
- self.logger.info('KDC Maximum Password age was changed from %s to %s' % (self.ldb.get_maxPwdAge(), val))
- self.ldb.set_maxPwdAge(val)
+ @abstractmethod
+ def read(self, policy):
+ pass
- def ch_minPwdLength(self, val):
- self.logger.info('KDC Minimum Password length was changed from %s to %s' % (self.ldb.get_minPwdLength(), val))
- self.ldb.set_minPwdLength(val)
+ def parse(self, afile, ldb, conn, gp_db, lp):
+ self.ldb = ldb
+ self.gp_db = gp_db
+ self.lp = lp
- def ch_pwdProperties(self, val):
- self.logger.info('KDC Password Properties were changed from %s to %s' % (self.ldb.get_pwdProperties(), val))
- self.ldb.set_pwdProperties(val)
+ # Fixing the bug where only some Linux Boxes capitalize MACHINE
+ try:
+ blist = afile.split('/')
+ idx = afile.lower().split('/').index('machine')
+ for case in [
+ blist[idx].upper(),
+ blist[idx].capitalize(),
+ blist[idx].lower()
+ ]:
+ bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
+ '/'.join(blist[idx+1:])
+ try:
+ return self.read(conn.loadfile(bfile.replace('/', '\\')))
+ except NTSTATUSError:
+ continue
+ except ValueError:
+ try:
+ return self.read(conn.loadfile(afile.replace('/', '\\')))
+ except Exception as e:
+ self.logger.error(str(e))
+ return None
- def explicit(self):
- return self.val
+ @abstractmethod
+ def __str__(self):
+ pass
- def nttime2unix(self):
- seconds = 60
- minutes = 60
- hours = 24
- sam_add = 10000000
- val = (self.val)
- val = int(val)
- return str(-(val * seconds * minutes * hours * sam_add))
+class gp_ext_setter():
+ __metaclass__ = ABCMeta
- def mapper(self):
- '''ldap value : samba setter'''
- return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
- "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
- # Could be none, but I like the method assignment in update_samba
- "minPwdLength" : (self.ch_minPwdLength, self.explicit),
- "pwdProperties" : (self.ch_pwdProperties, self.explicit),
+ def __init__(self, logger, ldb, gp_db, lp, attribute, val):
+ self.logger = logger
+ self.ldb = ldb
+ self.attribute = attribute
+ self.val = val
+ self.lp = lp
+ self.gp_db = gp_db
- }
+ def explicit(self):
+ return self.val
def update_samba(self):
(upd_sam, value) = self.mapper().get(self.attribute)
- upd_sam(value()) # or val = value() then update(val)
-
+ upd_sam(value())
-class gp_sec_ext(gp_ext):
- '''This class does the following two things:
- 1) Identifies the GPO if it has a certain kind of filepath,
- 2) Finally parses it.
- '''
-
- count = 0
-
- def __init__(self, logger):
- self.logger = logger
+ @abstractmethod
+ def mapper(self):
+ pass
+ @abstractmethod
def __str__(self):
- return "Security GPO extension"
+ pass
+class gp_inf_ext(gp_ext):
+ @abstractmethod
def list(self, rootpath):
- path = "%s%s" % (rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
- return path
-
- def listmachpol(self, rootpath):
- path = "%s%s" % (rootpath, "Machine/Registry.pol")
- return path
-
- def listuserpol(self, rootpath):
- path = "%s%s" % (rootpath, "User/Registry.pol")
- return path
-
- def populate_inf(self):
- return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
- "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
- "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
- "PasswordComplexity": ("pwdProperties", inf_to_ldb),
- }
- }
-
- def read_inf(self, path, conn, attr_log):
+ pass
+
+ @abstractmethod
+ def apply_map(self):
+ pass
+
+ def read(self, policy):
ret = False
- inftable = self.populate_inf()
+ inftable = self.apply_map()
- policy = conn.loadfile(path.replace('/', '\\')).decode('utf-16')
current_section = None
- LOG = open(attr_log, "a")
- LOG.write(str(path.split('/')[2]) + '\n')
# So here we would declare a boolean,
# that would get changed to TRUE.
inf_conf = ConfigParser()
inf_conf.optionxform=str
- inf_conf.readfp(StringIO(policy))
+ try:
+ inf_conf.readfp(StringIO(policy))
+ except:
+ inf_conf.readfp(StringIO(policy.decode('utf-16')))
for section in inf_conf.sections():
current_section = inftable.get(section)
(att, setter) = current_section.get(key)
value = value.encode('ascii', 'ignore')
ret = True
- setter(self.logger, self.ldb, self.dn, att, value).update_samba()
+ setter(self.logger, self.ldb, self.gp_db, self.lp, att,
+ value).update_samba()
+ self.gp_db.commit()
return ret
- def parse(self, afile, ldb, conn, attr_log):
- self.ldb = ldb
- self.dn = ldb.get_default_basedn()
-
- # Fixing the bug where only some Linux Boxes capitalize MACHINE
- if afile.endswith('inf'):
+ @abstractmethod
+ def __str__(self):
+ pass
+
+''' Fetch the hostname of a writable DC '''
+def get_dc_hostname(creds, lp):
+ net = Net(creds=creds, lp=lp)
+ cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
+ nbt.NBT_SERVER_DS))
+ return cldap_ret.pdc_dns_name
+
+''' Fetch a list of GUIDs for applicable GPOs '''
+def get_gpo_list(dc_hostname, creds, lp):
+ gpos = []
+ ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
+ if ads.connect():
+ gpos = ads.get_gpo_list(creds.get_username())
+ return gpos
+
+def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
+ gp_db = store.get_gplog(creds.get_username())
+ dc_hostname = get_dc_hostname(creds, lp)
+ try:
+ conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
+ except:
+ logger.error('Error connecting to \'%s\' using SMB' % dc_hostname)
+ raise
+ gpos = get_gpo_list(dc_hostname, creds, lp)
+
+ for gpo_obj in gpos:
+ guid = gpo_obj.name
+ if guid == 'Local Policy':
+ continue
+ path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
+ local_path = os.path.join(lp.get("path", "sysvol"), path)
+ version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1])
+ if version != store.get_int(guid):
+ logger.info('GPO %s has changed' % guid)
+ gp_db.state(GPOSTATE.APPLY)
+ else:
+ gp_db.state(GPOSTATE.ENFORCE)
+ gp_db.set_guid(guid)
+ store.start()
+ for ext in gp_extensions:
try:
- blist = afile.split('/')
- idx = afile.lower().split('/').index('machine')
- for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]:
- bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:])
- try:
- return self.read_inf(bfile, conn, attr_log)
- except NTSTATUSError:
- continue
- except ValueError:
- try:
- return self.read_inf(afile, conn, attr_log)
- except:
- return None
-
-
-def scan_log(sysvol_tdb):
- data = {}
- for key in sysvol_tdb.iterkeys():
- data[key] = sysvol_tdb.get(key)
- return data
-
-
-def Reset_Defaults(test_ldb):
- test_ldb.set_minPwdAge(str(-25920000000000))
- test_ldb.set_maxPwdAge(str(-38016000000000))
- test_ldb.set_minPwdLength(str(7))
- test_ldb.set_pwdProperties(str(1))
-
-
-def check_deleted(guid_list, backloggpo):
- if backloggpo is None:
- return False
- for guid in backloggpo:
- if guid not in guid_list:
- return True
- return False
-
-
-# The hierarchy is as per MS http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx
-#
-# It does not care about local GPO, because GPO and snap-ins are not made in Linux yet.
-# It follows the linking order and children GPO are last written format.
-#
-# Also, couple further testing with call scripts entitled informant and informant2.
-# They explicitly show the returned hierarchically sorted list.
-
-
-def container_indexes(GUID_LIST):
- '''So the original list will need to be seperated into containers.
- Returns indexed list of when the container changes after hierarchy
- '''
- count = 0
- container_indexes = []
- while count < (len(GUID_LIST)-1):
- if GUID_LIST[count][2] != GUID_LIST[count+1][2]:
- container_indexes.append(count+1)
- count += 1
- container_indexes.append(len(GUID_LIST))
- return container_indexes
-
+ ext.parse(ext.list(path), test_ldb, conn, gp_db, lp)
+ except Exception as e:
+ logger.error('Failed to parse gpo %s for extension %s' % \
+ (guid, str(ext)))
+ logger.error('Message was: ' + str(e))
+ store.cancel()
+ continue
+ store.store(guid, '%i' % version)
+ store.commit()
+
+def unapply_log(gp_db):
+ while True:
+ item = gp_db.apply_log_pop()
+ if item:
+ yield item
+ else:
+ break
-def sort_linked(SAMDB, guid_list, start, end):
- '''So GPO in same level need to have link level.
- This takes a container and sorts it.
+def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
+ gp_db = store.get_gplog(creds.get_username())
+ gp_db.state(GPOSTATE.UNAPPLY)
+ for gpo_guid in unapply_log(gp_db):
+ gp_db.set_guid(gpo_guid)
+ unapply_attributes = gp_db.list(gp_extensions)
+ for attr in unapply_attributes:
+ attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
+ attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
+ gp_db.delete(str(attr_obj), attr[0])
+ gp_db.commit()
- TODO: Small small problem, it is backwards
- '''
- containers = gpo_user.get_gpo_containers(SAMDB, guid_list[start][0])
- for right_container in containers:
- if right_container.get('dn') == guid_list[start][2]:
- break
- gplink = str(right_container.get('gPLink'))
- gplink_split = gplink.split('[')
- linked_order = []
- ret_list = []
- for ldap_guid in gplink_split:
- linked_order.append(str(ldap_guid[10:48]))
- count = len(linked_order) - 1
- while count > 0:
- ret_list.append([linked_order[count], guid_list[start][1], guid_list[start][2]])
- count -= 1
- return ret_list
-
-
-def establish_hierarchy(SamDB, GUID_LIST, DC_OU, global_dn):
- '''Takes a list of GUID from gpo, and sorts them based on OU, and realm.
- See http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx
- '''
- final_list = []
- count_unapplied_GPO = 0
- for GUID in GUID_LIST:
-
- container_iteration = 0
- # Assume first it is not applied
- applied = False
- # Realm only written on last call, if the GPO is linked to multiple places
- gpo_realm = False
-
- # A very important call. This gets all of the linked information.
- GPO_CONTAINERS = gpo_user.get_gpo_containers(SamDB, GUID)
- for GPO_CONTAINER in GPO_CONTAINERS:
-
- container_iteration += 1
-
- if DC_OU == str(GPO_CONTAINER.get('dn')):
- applied = True
- insert_gpo = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
- final_list.append(insert_gpo)
- break
-
- if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) == 1):
- gpo_realm = True
- applied = True
-
-
- if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) > 1):
- gpo_realm = True
- applied = True
-
-
- if container_iteration == len(GPO_CONTAINERS):
- if gpo_realm == False:
- insert_dud = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
- final_list.insert(0, insert_dud)
- count_unapplied_GPO += 1
- else:
- REALM_GPO = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
- final_list.insert(count_unapplied_GPO, REALM_GPO)
-
- # After GPO are sorted into containers, let's sort the containers themselves.
- # But first we can get the GPO that we don't care about, out of the way.
- indexed_places = container_indexes(final_list)
- count = 0
- unapplied_gpo = []
- # Sorted by container
- sorted_gpo_list = []
-
- # Unapplied GPO live at start of list, append them to final list
- while final_list[0][1] == False:
- unapplied_gpo.append(final_list[count])
- count += 1
- count = 0
- sorted_gpo_list += unapplied_gpo
-
- # A single container call gets the linked order for all GPO in container.
- # So we need one call per container - > index of the Original list
- indexed_places.insert(0, 0)
- while count < (len(indexed_places)-1):
- sorted_gpo_list += (sort_linked(SamDB, final_list, indexed_places[count], indexed_places[count+1]))
- count += 1
- return sorted_gpo_list