import sys
import os
+import tdb
sys.path.insert(0, "bin/python")
import samba.gpo as gpo
import optparse
from StringIO import StringIO
from abc import ABCMeta, abstractmethod
+class Backlog:
+ def __init__(self, sysvol_log):
+ if os.path.isfile(sysvol_log):
+ self.backlog = tdb.open(sysvol_log)
+ else:
+ self.backlog = tdb.Tdb(sysvol_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
+ self.backlog.transaction_start()
+
+ def version(self, guid):
+ try:
+ old_version = int(self.backlog.get(guid))
+ except TypeError:
+ old_version = -1
+ return old_version
+
+ def store(self, guid, version):
+ self.backlog.store(guid, '%i' % version)
+
+ def commit(self):
+ self.backlog.transaction_commit()
+
+ def __del__(self):
+ self.backlog.close()
+
class gp_ext(object):
__metaclass__ = ABCMeta
pass
@abstractmethod
- def parse(self, afile, ldb, conn, attr_log, lp):
+ def parse(self, afile, ldb, conn, lp):
pass
@abstractmethod
class inf_to():
__metaclass__ = ABCMeta
- def __init__(self, logger, ldb, dn, lp, attribute, val):
+ def __init__(self, logger, ldb, lp, attribute, val):
self.logger = logger
self.ldb = ldb
- self.dn = dn
self.attribute = attribute
self.val = val
self.lp = lp
return "Security GPO extension"
def list(self, rootpath):
- path = "%s%s" % (rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
- return path
+ return os.path.join(rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
def listmachpol(self, rootpath):
- path = "%s%s" % (rootpath, "Machine/Registry.pol")
- return path
+ return os.path.join(rootpath, "Machine/Registry.pol")
def listuserpol(self, rootpath):
- path = "%s%s" % (rootpath, "User/Registry.pol")
- return path
+ return os.path.join(rootpath, "User/Registry.pol")
def populate_inf(self):
return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
}
}
- def read_inf(self, path, conn, attr_log):
+ def read_inf(self, path, conn):
ret = False
inftable = self.populate_inf()
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.
(att, setter) = current_section.get(key)
value = value.encode('ascii', 'ignore')
ret = True
- setter(self.logger, self.ldb, self.dn, self.lp, att, value).update_samba()
+ setter(self.logger, self.ldb, self.lp, att, value).update_samba()
return ret
- def parse(self, afile, ldb, conn, attr_log, lp):
+ def parse(self, afile, ldb, conn, lp):
self.ldb = ldb
self.lp = lp
- self.dn = ldb.get_default_basedn()
# Fixing the bug where only some Linux Boxes capitalize MACHINE
if afile.endswith('inf'):
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)
+ return self.read_inf(bfile, conn)
except NTSTATUSError:
continue
except ValueError:
try:
- return self.read_inf(afile, conn, attr_log)
+ return self.read_inf(afile, conn)
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
-
-
-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.
-
- 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
# Co-Edited by Matthieu Pattou July 2013 from original August 2013
# Edited by Garming Sam Feb. 2014
# Edited by Luke Morrison April 2014
+# Edited by David Mulder May 2017
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
applied, have changed, or is in the right container'''
import os
-import fcntl
import sys
-import tempfile
-import subprocess
-import tdb
sys.path.insert(0, "bin/python")
-import samba
import optparse
from samba import getopt as options
from samba.gpclass import *
from samba import smb
import logging
-
-# Finds all GPO Files ending in inf
-def gp_path_list(path):
-
- GPO_LIST = []
- for ext in gp_extensions:
- GPO_LIST.append((ext, ext.list(path)))
- return GPO_LIST
-
-
-def gpo_parser(GPO_LIST, ldb, conn, attr_log, lp):
- '''The API method to parse the GPO
- :param GPO_LIST:
- :param ldb: Live instance of an LDB object AKA Samba
- :param conn: Live instance of a CIFS connection
- :param attr_log: backlog path for GPO and attribute to be written
- no return except a newly updated Samba
- '''
-
- ret = False
- for entry in GPO_LIST:
- (ext, thefile) = entry
- if ret == False:
- ret = ext.parse(thefile, ldb, conn, attr_log, lp)
- else:
- temp = ext.parse(thefile, ldb, conn, attr_log, lp)
- return ret
-
-
-class GPOServiceSetup:
- def __init__(self):
- """Initialize all components necessary to return instances of
- a Samba lp context (smb.conf) and Samba LDB context
- """
-
- self.parser = optparse.OptionParser("samba_gpoupdate [options]")
- self.sambaopts = options.SambaOptions(self.parser)
- self.credopts = None
- self.opts = None
- self.args = None
- self.lp = None
- self.smbconf = None
- self.creds = None
- self.url = None
-
- # Setters or Initializers
- def init_parser(self):
- '''Get the command line options'''
- self.parser.add_option_group(self.sambaopts)
- self.parser.add_option_group(options.VersionOptions(self.parser))
- self.init_credopts()
- self.parser.add_option("-H", dest="url", help="URL for the samdb")
- self.parser.add_option_group(self.credopts)
-
- def init_argsopts(self):
- '''Set the options and the arguments'''
- (opts, args) = self.parser.parse_args()
-
- self.opts = opts
- self.args = args
-
- def init_credopts(self):
- '''Set Credential operations'''
- self.credopts = options.CredentialsOptions(self.parser)
-
- def init_lp(self):
- '''Set the loadparm context'''
- self.lp = self.sambaopts.get_loadparm()
- self.smbconf = self.lp.configfile
- if (not self.opts.url):
- self.url = self.lp.samdb_url()
- else:
- self.url = self.opts.url
-
- def init_session(self):
- '''Initialize the session'''
- self.creds = self.credopts.get_credentials(self.lp,
- fallback_machine=True)
- self.session = system_session()
-
- def InitializeService(self):
- '''Inializer for the thread'''
- self.init_parser()
- self.init_argsopts()
- self.init_lp()
- self.init_session()
-
- # Getters
- def Get_LDB(self):
- '''Return a live instance of Samba'''
- SambaDB = SamDB(self.url, session_info=self.session,
- credentials=self.creds, lp=self.lp)
- return SambaDB
-
- def Get_lp_Content(self):
- '''Return an instance of a local lp context'''
- return self.lp
-
- def Get_Creds(self):
- '''Return an instance of a local creds'''
- return self.creds
-
-
-# Set up the GPO service
-GPOService = GPOServiceSetup()
-GPOService.InitializeService()
-
-# Get the Samba Instance
-test_ldb = GPOService.Get_LDB()
-
-# Get The lp context
-lp = GPOService.Get_lp_Content()
-
-# Set up logging
-logger = logging.getLogger('samba_gpoupdate')
-logger.addHandler(logging.StreamHandler(sys.stdout))
-logger.setLevel(logging.CRITICAL)
-log_level = lp.log_level()
-if log_level == 1:
- logger.setLevel(logging.ERROR)
-elif log_level == 2:
- logger.setLevel(logging.WARNING)
-elif log_level == 3:
- logger.setLevel(logging.INFO)
-elif log_level >= 4:
- logger.setLevel(logging.DEBUG)
-
-# Get the CREDS
-creds = GPOService.Get_Creds()
-
-# Read the readable backLog into a hashmap
-# then open writable backLog in same location
-BackLoggedGPO = None
-sys_log = '%s/%s' % (lp.get("path", "sysvol"), 'gpo.tdb')
-attr_log = '%s/%s' % (lp.get("path", "sysvol"), 'attrlog.txt')
-
-
-if os.path.isfile(sys_log):
- BackLog = tdb.open(sys_log)
-else:
- BackLog = tdb.Tdb(sys_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
-BackLoggedGPO = scan_log(BackLog)
-
-
-# We need to know writable DC to setup SMB connection
-net = Net(creds=creds, lp=lp)
-cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
- nbt.NBT_SERVER_DS))
-dc_hostname = cldap_ret.pdc_dns_name
-
-try:
- conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
-except Exception, e:
- raise Exception("Error connecting to '%s' using SMB" % dc_hostname, e)
-
-# Get the dn of the domain, and the dn of readable/writable DC
-global_dn = test_ldb.domain_dn()
-DC_OU = "OU=Domain Controllers" + ',' + global_dn
-
-# Set up a List of the GUID for all GPO's
-guid_list = [x['name'] for x in conn.list('%s/Policies' % lp.get("realm").lower())]
-SYSV_PATH = '%s/%s/%s' % (lp.get("path", "sysvol"), lp.get("realm"), 'Policies')
-
-hierarchy_gpos = establish_hierarchy(test_ldb, guid_list, DC_OU, global_dn)
-change_backlog = False
-
-# Take a local list of all current GPO list and run it against previous GPO's
-# to see if something has changed. If so reset default and re-apply GPO.
-Applicable_GPO = []
-for i in hierarchy_gpos:
- Applicable_GPO += i
-
-# Flag gets set when
-GPO_Changed = False
-GPO_Deleted = check_deleted(Applicable_GPO, BackLoggedGPO)
-if (GPO_Deleted):
- # Null the backlog
- BackLoggedGPO = {}
- # Reset defaults then overwrite them
- Reset_Defaults(test_ldb)
- GPO_Changed = False
-
-BackLog.transaction_start()
-for guid_eval in hierarchy_gpos:
- guid = guid_eval[0]
- gp_extensions = [gp_sec_ext(logger)]
- local_path = '%s/Policies' % lp.get("realm").lower() + '/' + guid + '/'
- version = int(gpo.gpo_get_sysvol_gpt_version(lp.get("path", "sysvol") + '/' + local_path)[1])
+''' Fetch the hostname of a writable DC '''
+def get_dc_hostname():
+ 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
+
+if __name__ == "__main__":
+ parser = optparse.OptionParser('samba_gpoupdate [options]')
+ sambaopts = options.SambaOptions(parser)
+
+ # Get the command line options
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(options.VersionOptions(parser))
+ credopts = options.CredentialsOptions(parser)
+ parser.add_option('-H', '--url', dest='url', help='URL for the samdb')
+ parser.add_option_group(credopts)
+
+ # Set the options and the arguments
+ (opts, args) = parser.parse_args()
+
+ # Set the loadparm context
+ lp = sambaopts.get_loadparm()
+ if not opts.url:
+ url = lp.samdb_url()
+ else:
+ url = opts.url
+
+ # Initialize the session
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ session = system_session()
+
+ # Set up logging
+ logger = logging.getLogger('samba_gpoupdate')
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+ logger.setLevel(logging.CRITICAL)
+ log_level = lp.log_level()
+ if log_level == 1:
+ logger.setLevel(logging.ERROR)
+ elif log_level == 2:
+ logger.setLevel(logging.WARNING)
+ elif log_level == 3:
+ logger.setLevel(logging.INFO)
+ elif log_level >= 4:
+ logger.setLevel(logging.DEBUG)
+
+ '''Return a live instance of Samba'''
+ test_ldb = SamDB(url, session_info=session, credentials=creds, lp=lp)
+
+ # Read the readable backLog into a hashmap
+ # then open writable backLog in same location
+ sysvol_log = os.path.join(lp.get('cache directory'), 'gpo.tdb')
+
+ backlog = Backlog(sysvol_log)
+
+ dc_hostname = get_dc_hostname()
try:
- old_version = int(BackLoggedGPO.get(guid))
+ conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
except:
- old_version = -1
- gpolist = gp_path_list(local_path)
- if version != old_version:
- GPO_Changed = True
- # If the GPO has a dn that is applicable to Samba
- if guid_eval[1]:
- # If it has a GPO file that could apply to Samba
- if gpolist[0][1]:
- # If it we have not read it before and is not empty
- # Rewrite entire logfile here
- if (version != 0) and GPO_Changed == True:
- logger.info('GPO %s has changed' % guid)
- try:
- change_backlog = gpo_parser(gpolist, test_ldb, conn, attr_log, lp)
- except:
- logger.error('Failed to parse gpo %s' % guid)
- continue
- BackLog.store(guid, '%i' % version)
-BackLog.transaction_commit()
-BackLog.close()
+ 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
+ gp_extensions = [gp_sec_ext(logger)]
+ local_path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
+ version = int(gpo.gpo_get_sysvol_gpt_version(os.path.join(lp.get("path", "sysvol"), local_path))[1])
+ if version != backlog.version(guid):
+ logger.info('GPO %s has changed' % guid)
+ try:
+ for ext in gp_extensions:
+ ext.parse(ext.list(local_path), test_ldb, conn, lp)
+ except:
+ logger.error('Failed to parse gpo %s' % guid)
+ continue
+ backlog.store(guid, version)
+ backlog.commit()