[ SEA-GHOST MINI SHELL]
#!/usr/bin/env python
#
# Copyright (c) 2013-2014 Marin Atanasov Nikolov <dnaeon@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer
# in this position and unchanged.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
The zabbix-ldap-sync script is used for syncing LDAP users with Zabbix.
"""
import random
import string
import logging
import ConfigParser
import sys
import ldap
import ldap.filter
from pyzabbix import ZabbixAPI, ZabbixAPIException
from docopt import docopt
class LDAPConn(object):
"""
LDAP connector class
Defines methods for retrieving users and groups from LDAP server.
"""
def __init__(self, uri, base, user, passwd):
self.uri = uri
self.base = base
self.ldap_user = user
self.ldap_pass = passwd
def connect(self):
"""
Establish a connection to the LDAP server.
Raises:
SystemExit
"""
self.conn = ldap.initialize(self.uri)
self.conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
try:
self.conn.simple_bind_s(self.ldap_user, self.ldap_pass)
except ldap.SERVER_DOWN as e:
raise SystemExit('Cannot connect to LDAP server: %s' % e)
def disconnect(self):
"""
Disconnect from the LDAP server.
"""
self.conn.unbind()
def remove_ad_referrals(self, result):
"""
Remove referrals from AD query result
"""
return [i for i in result if i[0] != None]
def get_group_members(self, group):
"""
Retrieves the members of an LDAP group
Args:
group (str): The LDAP group name
Returns:
A list of all users in the LDAP group
"""
attrlist = [group_member_attribute]
filter = group_filter % group
result = self.conn.search_s(base=self.base,
scope=ldap.SCOPE_SUBTREE,
filterstr=filter,
attrlist=attrlist)
if not result:
print '>>> Unable to find group %s, skipping group' % group
return None
# Get DN for each user in the group
if active_directory:
result = self.remove_ad_referrals(result)
final_listing = {}
for members in result:
result_dn = members[0]
result_attrs = members[1]
group_members = []
attrlist = [uid_attribute]
if recursive:
# Get a DN for all users in a group (recursive)
# It's available only on domain controllers with Windows Server 2003 SP2 or later
member_of_filter_dn = memberof_filter % result_dn
if skipdisabled:
filter = "(&%s%s%s)" % (user_filter, member_of_filter_dn, disabled_filter)
else:
filter = "(&%s%s)" % (user_filter, member_of_filter_dn)
uid = self.conn.search_s(base=self.base,
scope=ldap.SCOPE_SUBTREE,
filterstr=filter,
attrlist=attrlist)
for item in self.remove_ad_referrals(uid):
group_members.append(item)
else:
# Otherwise, just get a DN for each user in the group
for member in result_attrs[group_member_attribute]:
if skipdisabled:
filter = "(&%s%s)" % (user_filter, disabled_filter)
else:
filter = "(&%s)" % user_filter
uid = self.conn.search_s(base=member,
scope=ldap.SCOPE_BASE,
filterstr=filter,
attrlist=attrlist)
for item in uid:
group_members.append(item)
# Fill dictionary with usernames and corresponding DNs
for item in group_members:
dn = item[0]
username = item[1][uid_attribute]
if lowercase:
username = str(username)[2: -2].lower()
else:
username = str(username)[2: -2]
final_listing[username] = dn
return final_listing
else:
dn, users = result.pop()
final_listing = {}
group_members = []
# Get info for each user in the group
for memberid in users[group_member_attribute]:
if openldap_type == "groupofnames":
filter = "(objectClass=*)"
# memberid is user dn
base = memberid
else:
# memberid is user attribute, most likely uid
filter = user_filter % memberid
base = self.base
attrlist = [uid_attribute]
# get the actual LDAP object for each group member
uid = self.conn.search_s(base=base,
scope=ldap.SCOPE_SUBTREE,
filterstr=filter,
attrlist=attrlist)
for item in uid:
group_members.append(item)
# Fill dictionary with usernames and corresponding DNs
for item in group_members:
dn = item[0]
username = item[1][uid_attribute]
user = ''.join(username)
final_listing[user] = dn
return final_listing
def get_groups_with_wildcard(self, groups_wildcard):
print(">>> Search group with wildcard: %s"% groups_wildcard)
filter = group_filter % groups_wildcard
result_groups=[]
result = self.conn.search_s(base=self.base,
scope=ldap.SCOPE_SUBTREE,
filterstr=filter,)
for group in result:
# Skip refldap (when Active Directory used)
# [0]==None
if group[0]:
group_name=group[1]['name'][0]
print("Find group %s" % group_name)
result_groups.append(group_name)
if not result_groups:
print('>>> Unable to find group %s, skipping group wildcard' % groups_wildcard)
return result_groups
def get_user_media(self, dn, ldap_media):
"""
Retrieves the 'media' attribute of an LDAP user
Args:
username (str): The LDAP distinguished name to lookup
ldap_media (str): The name of the field containing the media address
Returns:
The user's media attribute value
"""
attrlist = [ldap_media]
result = self.conn.search_s(base=dn,
scope=ldap.SCOPE_BASE,
attrlist=attrlist)
if not result:
return None
dn, data = result.pop()
mail = data.get(ldap_media)
if not mail:
return None
return mail.pop()
def get_user_sn(self, dn):
"""
Retrieves the 'sn' attribute of an LDAP user
Args:
username (str): The LDAP distinguished name to lookup
Returns:
The user's surname attribute
"""
attrlist = ['sn']
result = self.conn.search_s(base=dn,
scope=ldap.SCOPE_BASE,
attrlist=attrlist)
if not result:
return None
dn, data = result.pop()
sn = data.get('sn')
if not sn:
return None
return sn.pop()
def get_user_givenName(self, dn):
"""
Retrieves the 'givenName' attribute of an LDAP user
Args:
username (str): The LDAP distinguished name to lookup
Returns:
The user's given name attribute
"""
attrlist = ['givenName']
result = self.conn.search_s(base=dn,
scope=ldap.SCOPE_BASE,
attrlist=attrlist)
if not result:
return None
dn, data = result.pop()
name = data.get('givenName')
if not name:
return None
return name.pop()
class ZabbixConn(object):
"""
Zabbix connector class
Defines methods for managing Zabbix users and groups
"""
def __init__(self, server, username, password):
self.server = server
self.username = username
self.password = password
def verbose(self):
# Use logger to log information
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# Log to stdout
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch) # Use logger to log information
# Log from pyzabbix
log = logging.getLogger('pyzabbix')
log.addHandler(ch)
log.setLevel(logging.DEBUG)
def connect(self, nocheckcertificate):
"""
Establishes a connection to the Zabbix server
Args:
nocheckcertificate (bool): Don't check the server certificate
Raises:
SystemExit
"""
self.conn = ZabbixAPI(self.server)
if nocheckcertificate:
self.conn.session.verify = False
try:
self.conn.login(self.username, self.password)
except ZabbixAPIException as e:
raise SystemExit('Cannot login to Zabbix server: %s' % e)
def get_users(self):
"""
Retrieves the existing Zabbix users
Returns:
A list of the existing Zabbix users
"""
result = self.conn.user.get(output='extend')
users = [user['alias'] for user in result]
return users
def get_mediatype_id(self, description):
"""
Retrieves the mediatypeid by description
Args:
description (str): Zabbix media type description
Returns:
The mediatypeid for specified media type description
"""
result = self.conn.mediatype.get(filter={'description': description})
if result:
mediatypeid = result[0]['mediatypeid']
else:
mediatypeid = None
return mediatypeid
def get_user_id(self, user):
"""
Retrieves the userid of a specified user
Args:
user (str): The Zabbix username to lookup
Returns:
The userid of the specified user
"""
result = self.conn.user.get(output='extend')
userid = [u['userid'] for u in result if u['alias'] == user].pop()
return userid
def get_groups(self):
"""
Retrieves the existing Zabbix groups
Returns:
A dict of the existing Zabbix groups and their group ids
"""
result = self.conn.usergroup.get(status=0, output='extend')
groups = [{'name': group['name'], 'usrgrpid': group['usrgrpid']} for group in result]
return groups
def get_group_members(self, groupid):
"""
Retrieves group members for a Zabbix group
Args:
groupid (int): The group id
Returns:
A list of the Zabbix users for the specified group id
"""
result = self.conn.user.get(output='extend', usrgrpids=groupid)
users = [user['alias'] for user in result]
return users
def create_group(self, group):
"""
Creates a new Zabbix group
Args:
group (str): The Zabbix group name to create
Returns:
The groupid of the newly created group
"""
result = self.conn.usergroup.create(name=group, permission=3)
groupid = result['usrgrpids'].pop()
return groupid
def create_user(self, user, groupid, user_opt):
"""
Creates a new Zabbix user
Args:
user (dict): A dict containing the user details
groupid (int): The groupid for the new user
user_opt (dict): User options
"""
random_passwd = ''.join(random.sample(string.ascii_letters + string.digits, 32))
user_defaults = {'autologin': 0, 'type': 1, 'usrgrps': [{'usrgrpid': str(groupid)}], 'passwd': random_passwd}
user_defaults.update(user_opt)
user.update(user_defaults)
result = self.conn.user.create(user)
return result
def delete_user(self, user):
"""
Deletes Zabbix user
Args:
user (string): Zabbix username
"""
userid = self.get_user_id(user)
result = self.conn.user.delete(userid)
return result
def update_user(self, user, groupid):
"""
Adds an existing Zabbix user to a group
Args:
user (dict): A dict containing the user details
groupid (int): The groupid to add the user to
"""
userid = self.get_user_id(user)
result = self.conn.usergroup.massadd(usrgrpids=[str(groupid)], userids=[str(userid)])
return result
def update_media(self, user, description, sendto, media_opt):
"""
Adds media to an existing Zabbix user
Args:
user (dict): A dict containing the user details
description (str): A string containing Zabbix media description
sendto (str): A string containing address, phone number, etc...
media_opt (dict): Media options
"""
userid = self.get_user_id(user)
mediatypeid = self.get_mediatype_id(description)
if mediatypeid:
media_defaults = {
'mediatypeid': mediatypeid,
'sendto': sendto,
'active': '0',
'severity': '63',
'period': '1-7,00:00-24:00'
}
media_defaults.update(media_opt)
self.delete_media_by_description(user, description)
result = self.conn.user.addmedia(users=[{"userid": str(userid)}], medias=media_defaults)
else:
result = None
return result
def delete_media_by_description(self, user, description):
"""
Remove all media from user (with specific mediatype)
Args:
user (dict): A dict containing the user details
description (str): A string containing Zabbix media description
"""
userid = self.get_user_id(user)
mediatypeid = self.get_mediatype_id(description)
if mediatypeid:
user_full = self.conn.user.get(output="extend", userids=userid, selectMedias = ["mediatypeid", "mediaid"])
media_ids = [int(u['mediaid']) for u in user_full[0]['medias'] if u['mediatypeid'] == mediatypeid]
if media_ids:
print '>>> Remove other exist media from user %s (type=%s)' % (user, description)
for id in media_ids:
self.conn.user.deletemedia(id)
def create_missing_groups(self, ldap_groups):
"""
Creates any missing LDAP groups in Zabbix
Args:
ldap_groups (list): A list of LDAP groups to create
"""
missing_groups = set(ldap_groups) - set([g['name'] for g in self.get_groups()])
for eachGroup in missing_groups:
print '>>> Creating Zabbix group %s' % eachGroup
grpid = self.create_group(eachGroup)
print '>>> Group %s created with groupid %s' % (eachGroup, grpid)
def sync_users(self, ldap_uri, ldap_base, ldap_groups, ldap_user, ldap_pass, ldap_media, user_opt, media_description, media_opt):
"""
Syncs Zabbix with LDAP users
"""
ldap_conn = LDAPConn(ldap_uri, ldap_base, ldap_user, ldap_pass)
ldap_conn.connect()
zabbix_all_users = self.get_users()
for eachGroup in ldap_groups:
ldap_users = ldap_conn.get_group_members(eachGroup)
# Do nothing if LDAP group contains no users and "--delete-orphans" is not specified
if not ldap_users and not deleteorphans:
continue
zabbix_grpid = [g['usrgrpid'] for g in self.get_groups() if g['name'] == eachGroup].pop()
zabbix_group_users = self.get_group_members(zabbix_grpid)
missing_users = set(list(ldap_users.keys())) - set(zabbix_group_users)
# Add missing users
for eachUser in missing_users:
# Create new user if it does not exists already
if eachUser not in zabbix_all_users:
print '>>> Creating user %s, member of Zabbix group %s' % (eachUser, eachGroup)
user = {'alias': eachUser}
user['name'] = ldap_conn.get_user_givenName(ldap_users[eachUser])
user['surname'] = ldap_conn.get_user_sn(ldap_users[eachUser])
self.create_user(user, zabbix_grpid, user_opt)
zabbix_all_users.append(eachUser)
else:
# Update existing user to be member of the group
print '>>> Updating user %s, adding to group %s' % (eachUser, eachGroup)
self.update_user(eachUser, zabbix_grpid)
# Handle any extra users in the groups
extra_users = set(zabbix_group_users) - set(list(ldap_users.keys()))
if extra_users:
print '>>> Users in group %s which are not found in LDAP group:' % eachGroup
for eachUser in extra_users:
if deleteorphans:
print 'Deleting user: %s' % eachUser
self.delete_user(eachUser)
else:
print ' * %s' % eachUser
# update users media
zabbix_group_users = self.get_group_members(zabbix_grpid)
for eachUser in set(zabbix_group_users):
print '>>> Updating user media for %s, update %s' % (eachUser, media_description)
sendto = ldap_conn.get_user_media(ldap_users[eachUser], ldap_media)
if sendto:
self.update_media(eachUser, media_description, sendto, media_opt)
ldap_conn.disconnect()
class ZabbixLDAPConf(object):
"""
Zabbix-LDAP configuration class
Provides methods for parsing and retrieving config entries
"""
def __init__(self, config):
self.config = config
def try_get_item(self, parser, section, option, default):
"""
Gets config item
Args:
parser (ConfigParser): ConfigParser
section (str): Config section name
option (str): Config option name
default : Value to return if item doesn't exist
Returns:
Config item value or default value
"""
try:
result = parser.get(section, option)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
result = default
return result
def try_get_section(self, parser, section, default):
"""
Gets config section
Args:
parser (ConfigParser): ConfigParser
section (str): Config section name
default : Value to return if section doesn't exist
Returns:
Config section dict or default value
"""
try:
result = parser.items(section)
except ConfigParser.NoSectionError:
result = default
return result
def remove_config_section_items(self, section, items):
"""
Removes items from config section
Args:
section (list of tuples): Config section
items (list): Item names to remove
Returns:
Config section without specified items
"""
return [i for i in section if i[0] not in items]
def load_config(self):
"""
Loads the configuration file
Raises:
ConfigParser.NoOptionError
"""
parser = ConfigParser.ConfigParser()
parser.read(self.config)
try:
self.ldap_type = self.try_get_item(parser, 'ldap', 'type', None)
self.ldap_uri = parser.get('ldap', 'uri')
self.ldap_base = parser.get('ldap', 'base')
self.ldap_groups = [i.strip() for i in parser.get('ldap', 'groups').split(',')]
self.ldap_user = parser.get('ldap', 'binduser')
self.ldap_pass = parser.get('ldap', 'bindpass')
self.ldap_media = self.try_get_item(parser, 'ldap', 'media', 'mail')
self.ad_filtergroup = parser.get ('ad', 'filtergroup','(&(objectClass=group)(name=%s))')
self.ad_filteruser = parser.get ('ad','filteruser', '(objectClass=user)(objectCategory=Person))')
self.ad_filterdisabled = parser.get ('ad', 'filterdisabled','(!(userAccountControl:1.2.840.113556.1.4.803:=2))')
self.ad_filtermemberof = parser.get ('ad', 'filtermemberof','(memberOf:1.2.840.113556.1.4.1941:=%s)')
self.ad_groupattribute = parser.get ('ad', 'groupattribute','member')
self.ad_userattribute = parser.get ('ad', 'userattribute','sAMAccountName')
self.openldap_type = parser.get ('openldap', 'type','posixgroup')
self.openldap_filtergroup = parser.get ('openldap', 'filtergroup','(&(objectClass=posixGroup)(cn=%s))')
self.openldap_filteruser = parser.get ('openldap', 'filteruser','(&(objectClass=posixAccount)(uid=%s))')
self.openldap_groupattribute = parser.get ('openldap', 'groupattribute','memberUid')
self.openldap_userattribute = parser.get ('openldap', 'userattribute','uid')
self.zbx_server = parser.get('zabbix', 'server')
self.zbx_username = parser.get('zabbix', 'username')
self.zbx_password = parser.get('zabbix', 'password')
self.user_opt = self.try_get_section(parser, 'user', {})
self.media_description = self.try_get_item(parser, 'media', 'description', 'Email')
self.media_opt = self.remove_config_section_items(self.try_get_section(parser, 'media', {}), ('description', 'userid'))
except ConfigParser.NoOptionError as e:
raise SystemExit('Configuration issues detected in %s' % self.config)
def set_groups_with_wildcard(self):
"""
Set group from LDAP with wildcard
:return:
"""
result_groups=[]
ldap_conn = LDAPConn(self.ldap_uri, self.ldap_base, self.ldap_user, self.ldap_pass)
ldap_conn.connect()
for group in self.ldap_groups:
groups = ldap_conn.get_groups_with_wildcard(group)
result_groups = result_groups + groups
if result_groups:
self.ldap_groups = result_groups
else:
raise SystemExit('ERROR - No groups found with wildcard' )
def main():
usage = """
Usage: zabbix-ldap-sync [-lsrwdn] [--verbose] -f <config>
zabbix-ldap-sync -v
zabbix-ldap-sync -h
Options:
-h, --help Display this usage info
-v, --version Display version and exit
-l, --lowercase Create AD user names as lowercase
-s, --skip-disabled Skip disabled AD users
-r, --recursive Resolves AD group members recursively (i.e. nested groups)
-w, --wildcard-search Search AD group with wildcard (e.g. R.*.Zabbix.*) - TESTED ONLY with Active Directory
-d, --delete-orphans Delete Zabbix users that don't exist in a LDAP group
-n, --no-check-certificate Don't check Zabbix server certificate
--verbose Print debug message from ZabbixAPI
-f <config>, --file <config> Configuration file to use
"""
args = docopt(usage, version="0.1.1")
config = ZabbixLDAPConf(args['--file'])
config.load_config()
# set up AD differences, if necessary
global active_directory
global openldap_type
global group_filter
global disabled_filter
global user_filter
global memberof_filter
global group_member_attribute
global uid_attribute
global lowercase
global skipdisabled
global deleteorphans
global recursive
lowercase = args['--lowercase']
skipdisabled = args['--skip-disabled']
deleteorphans = args['--delete-orphans']
nocheckcertificate = args['--no-check-certificate']
recursive = args['--recursive']
verbose = args['--verbose']
if config.ldap_type == 'activedirectory':
active_directory = "true"
group_filter = config.ad_filtergroup
user_filter = config.ad_filteruser
disabled_filter = config.ad_filterdisabled
memberof_filter = config.ad_filtermemberof
group_member_attribute = config.ad_groupattribute
uid_attribute = config.ad_userattribute
else:
active_directory = None
openldap_type = config.openldap_type
group_filter = config.openldap_filtergroup
user_filter = config.openldap_filteruser
group_member_attribute = config.openldap_groupattribute
uid_attribute = config.openldap_userattribute
wildcard_search = args['--wildcard-search']
if wildcard_search:
config.set_groups_with_wildcard()
if nocheckcertificate:
from requests.packages.urllib3 import disable_warnings
disable_warnings()
zabbix_conn = ZabbixConn(config.zbx_server, config.zbx_username, config.zbx_password)
if verbose:
zabbix_conn.verbose()
zabbix_conn.connect(nocheckcertificate)
zabbix_conn.create_missing_groups(config.ldap_groups)
zabbix_conn.sync_users(config.ldap_uri,
config.ldap_base,
config.ldap_groups,
config.ldap_user,
config.ldap_pass,
config.ldap_media,
config.user_opt,
config.media_description,
config.media_opt)
if __name__ == '__main__':
main()
SEA-GHOST - SHELL CODING BY SEA-GHOST