diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4546b39..0000000 --- a/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = crlf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 18f54e8..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -build/ -dist/ -ldapdomaindump.egg-info -domainlookup.py -*.pyc \ No newline at end of file diff --git a/LICENSE b/LICENSE index 4b7fbe1..4dbbfcf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Dirk-jan +Copyright (c) 2020 Dirk-jan Mollema Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 854b2a2..0131a8f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include ldapdomaindump/style.css +include LICENSE Readme.md diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..2a3bc12 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: ldapdomaindump +Version: 0.9.3 +Summary: Active Directory information dumper via LDAP +Home-page: https://github.com/dirkjanm/ldapdomaindump/ +Author: Dirk-jan Mollema +Author-email: dirkjan@sanoweb.nl +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/Readme.md b/Readme.md index ca8a0cb..beda023 100644 --- a/Readme.md +++ b/Readme.md @@ -25,9 +25,9 @@ - *domain_computers_by_os*: Domain computers sorted by Operating System ## Dependencies and installation -Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0 and [dnspython](https://github.com/rthalley/dnspython) +Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0, [dnspython](https://github.com/rthalley/dnspython) and [future](https://python-future.org/). ldapdomaindump runs on both python 2 and 3. -Both can be installed with `pip install ldap3 dnspython` +Dependencies can be installed manually with `pip install ldap3 dnspython future`, but should in most cases be handled by pip when you install the main package either from git or pypi. The ldapdomaindump package can be installed with `python setup.py install` from the git source, or for the latest release with `pip install ldapdomaindump`. @@ -100,7 +100,10 @@ ## Visualizing groups with BloodHound LDAPDomainDump includes a utility that can be used to convert ldapdomaindumps `.json` files to CSV files suitable for BloodHound. The utility is called `ldd2bloodhound` and is added to your path upon installation. Alternatively you can run it with `python -m ldapdomaindump.convert` or with `python ldapdomaindump/convert.py` if you are running it from the source. -The conversion tool will take the users/groups/computers/trusts `.json` file and convert those to `group_membership.csv` and `trust.csv` which you can add to BloodHound. +The conversion tool will take the users/groups/computers/trusts `.json` file and convert those to `group_membership.csv` and `trust.csv` which you can add to BloodHound. *Note that these files are only compatible with **BloodHound 1.x** which is quite old. There are no plans to support the latest version as the [BloodHound.py project](https://github.com/fox-it/BloodHound.py) was made for this. With the DCOnly collection method this tool will also only talk to LDAP and collect more information than ldapdomaindump would*. + +## Visualizing dump with a pretty output like enum4linux +LDAPDomainDump includes a utility that can be used to output ldapdomaindumps `.json` files to an enum4linux like output. The utility is called `ldd2pretty` and is added to your path upon installation. Alternatively you can run it with `python -m ldapdomaindump.pretty` or with `python ldapdomaindump/pretty.py` if you are running it from the source. ## License MIT diff --git a/bin/ldd2pretty b/bin/ldd2pretty new file mode 100644 index 0000000..82a9136 --- /dev/null +++ b/bin/ldd2pretty @@ -0,0 +1,3 @@ +#!/usr/bin/env python +from ldapdomaindump import pretty +pretty.main() diff --git a/ldapdomaindump/__init__.py b/ldapdomaindump/__init__.py index 533f057..7aad299 100644 --- a/ldapdomaindump/__init__.py +++ b/ldapdomaindump/__init__.py @@ -24,7 +24,7 @@ from __future__ import unicode_literals import sys, os, re, codecs, json, argparse, getpass, base64 # import class and constants -from datetime import datetime +from datetime import datetime, timedelta try: from urllib.parse import quote_plus except ImportError: @@ -48,6 +48,7 @@ 'ACCOUNT_LOCKED':0x00000010, 'PASSWD_NOTREQD':0x00000020, 'PASSWD_CANT_CHANGE': 0x00000040, + 'PASSWORD_STORE_CLEARTEXT': 0x00000080, 'NORMAL_ACCOUNT': 0x00000200, 'WORKSTATION_ACCOUNT':0x00001000, 'SERVER_TRUST_ACCOUNT': 0x00002000, @@ -81,6 +82,7 @@ 'TREAT_AS_EXTERNAL':0x00000040, 'USES_RC4_ENCRYPTION':0x00000080, 'CROSS_ORGANIZATION_NO_TGT_DELEGATION':0x00000200, + 'CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION':0x00000800, 'PIM_TRUST':0x00000400} # Domain trust direction @@ -112,7 +114,11 @@ 'lockoutThreshold':'Lockout Threshold', 'maxPwdAge':'Max password age', 'minPwdAge':'Min password age', - 'minPwdLength':'Min password length'} + 'minPwdLength':'Min password length', + 'pwdHistoryLength':'Password history length', + 'pwdProperties':'Password properties', + 'ms-DS-MachineAccountQuota':'Machine Account Quota', + 'flatName':'NETBIOS Domain name'} MINIMAL_COMPUTERATTRIBUTES = ['cn', 'sAMAccountName', 'dNSHostName', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description', 'objectClass'] MINIMAL_USERATTRIBUTES = ['cn', 'name', 'sAMAccountName', 'memberOf', 'primaryGroupId', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description', 'objectClass'] @@ -378,7 +384,11 @@ except (LDAPAttributeError, LDAPCursorError): ugroups = [] #Add the user default group - ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value])) + try: + ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value])) + # Sometimes we can't query this group or it doesn't exist + except KeyError: + pass for group in ugroups: try: groupsdict[group].append(user) @@ -431,7 +441,7 @@ #In grouped view, don't include the memberOf property to reduce output size self.userattributes_grouped = ['cn', 'name', 'sAMAccountName', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description'] self.groupattributes = ['cn', 'sAMAccountName', 'memberOf', 'description', 'whenCreated', 'whenChanged', 'objectSid'] - self.policyattributes = ['cn', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties'] + self.policyattributes = ['distinguishedName', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties', 'ms-DS-MachineAccountQuota'] self.trustattributes = ['cn', 'flatName', 'securityIdentifier', 'trustAttributes', 'trustDirection', 'trustType'] #Escape HTML special chars @@ -446,28 +456,36 @@ #Convert password max age (in 100 nanoseconds), to days def nsToDays(self, length): - return abs(length) * .0000001 / 86400 + # ldap3 >= 2.6 returns timedelta + if isinstance(length, timedelta): + return length.total_seconds() / 86400 + else: + return abs(length) * .0000001 / 86400 def nsToMinutes(self, length): - return abs(length) * .0000001 / 60 + # ldap3 >= 2.6 returns timedelta + if isinstance(length, timedelta): + return length.total_seconds() / 60 + else: + return abs(length) * .0000001 / 60 #Parse bitwise flags into a list def parseFlags(self, attr, flags_def): + outflags = [] + if attr is None or attr.value is None: + return outflags + for flag, val in iteritems(flags_def): + if int(attr.value) & val: + outflags.append(flag) + return outflags + + #Parse bitwise trust direction - only one flag applies here, 0x03 overlaps + def parseSingleFlag(self, attr, flags_def): outflags = [] if attr is None: return outflags for flag, val in iteritems(flags_def): - if attr.value & val: - outflags.append(flag) - return outflags - - #Parse bitwise trust direction - only one flag applies here, 0x03 overlaps - def parseTrustDirection(self, attr, flags_def): - outflags = [] - if attr is None: - return outflags - for flag, val in iteritems(flags_def): - if attr.value == val: + if int(attr.value) == val: outflags.append(flag) return outflags @@ -594,7 +612,10 @@ return self.formatGroupsHtml(att.values) #Primary group if aname == 'primarygroupid': - return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]]) + try: + return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]]) + except KeyError: + return 'NOT FOUND!' #Pwd flags if aname == 'pwdproperties': return ', '.join(self.parseFlags(att, pwd_flags)) @@ -605,9 +626,9 @@ if att.value == 0: return 'DISABLED' else: - return ', '.join(self.parseTrustDirection(att, trust_directions)) + return ', '.join(self.parseSingleFlag(att, trust_directions)) if aname == 'trusttype': - return ', '.join(self.parseFlags(att, trust_type)) + return ', '.join(self.parseSingleFlag(att, trust_type)) if aname == 'securityidentifier': return format_sid(att.raw_values[0]) if aname == 'minpwdage' or aname == 'maxpwdage': @@ -677,7 +698,10 @@ if aname == 'member' or aname == 'memberof' and type(att.values) is list: return self.formatGroupsGrep(att.values) if aname == 'primarygroupid': - return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]]) + try: + return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]]) + except KeyError: + return 'NOT FOUND!' #Domain trust flags if aname == 'trustattributes': return ', '.join(self.parseFlags(att, trust_flags)) @@ -685,9 +709,9 @@ if att.value == 0: return 'DISABLED' else: - return ', '.join(self.parseTrustDirection(att, trust_directions)) + return ', '.join(self.parseSingleFlag(att, trust_directions)) if aname == 'trusttype': - return ', '.join(self.parseFlags(att, trust_type)) + return ', '.join(self.parseSingleFlag(att, trust_type)) if aname == 'securityidentifier': return format_sid(att.raw_values[0]) #Pwd flags @@ -768,60 +792,60 @@ if self.config.outputhtml: html = self.generateHtmlTable(dd.users, self.userattributes, 'Domain users') self.writeHtmlFile('%s.html' % self.config.usersfile, html) + if self.config.outputgrep: + grepout = self.generateGrepList(dd.users, self.userattributes) + self.writeGrepFile('%s.grep' % self.config.usersfile, grepout) if self.config.outputjson: jsonout = self.generateJsonList(dd.users) self.writeJsonFile('%s.json' % self.config.usersfile, jsonout) - if self.config.outputgrep: - grepout = self.generateGrepList(dd.users, self.userattributes) - self.writeGrepFile('%s.grep' % self.config.usersfile, grepout) #Generate report with just a table of all computer accounts def generateComputersReport(self, dd): if self.config.outputhtml: html = self.generateHtmlTable(dd.computers, self.computerattributes, 'Domain computer accounts') self.writeHtmlFile('%s.html' % self.config.computersfile, html) + if self.config.outputgrep: + grepout = self.generateGrepList(dd.computers, self.computerattributes) + self.writeGrepFile('%s.grep' % self.config.computersfile, grepout) if self.config.outputjson: jsonout = self.generateJsonList(dd.computers) self.writeJsonFile('%s.json' % self.config.computersfile, jsonout) - if self.config.outputgrep: - grepout = self.generateGrepList(dd.computers, self.computerattributes) - self.writeGrepFile('%s.grep' % self.config.computersfile, grepout) #Generate report with just a table of all computer accounts def generateGroupsReport(self, dd): if self.config.outputhtml: html = self.generateHtmlTable(dd.groups, self.groupattributes, 'Domain groups') self.writeHtmlFile('%s.html' % self.config.groupsfile, html) + if self.config.outputgrep: + grepout = self.generateGrepList(dd.groups, self.groupattributes) + self.writeGrepFile('%s.grep' % self.config.groupsfile, grepout) if self.config.outputjson: jsonout = self.generateJsonList(dd.groups) self.writeJsonFile('%s.json' % self.config.groupsfile, jsonout) - if self.config.outputgrep: - grepout = self.generateGrepList(dd.groups, self.groupattributes) - self.writeGrepFile('%s.grep' % self.config.groupsfile, grepout) #Generate policy report def generatePolicyReport(self, dd): if self.config.outputhtml: html = self.generateHtmlTable(dd.policy, self.policyattributes, 'Domain policy') self.writeHtmlFile('%s.html' % self.config.policyfile, html) + if self.config.outputgrep: + grepout = self.generateGrepList(dd.policy, self.policyattributes) + self.writeGrepFile('%s.grep' % self.config.policyfile, grepout) if self.config.outputjson: jsonout = self.generateJsonList(dd.policy) self.writeJsonFile('%s.json' % self.config.policyfile, jsonout) - if self.config.outputgrep: - grepout = self.generateGrepList(dd.policy, self.policyattributes) - self.writeGrepFile('%s.grep' % self.config.policyfile, grepout) #Generate policy report def generateTrustsReport(self, dd): if self.config.outputhtml: html = self.generateHtmlTable(dd.trusts, self.trustattributes, 'Domain trusts') self.writeHtmlFile('%s.html' % self.config.trustsfile, html) + if self.config.outputgrep: + grepout = self.generateGrepList(dd.trusts, self.trustattributes) + self.writeGrepFile('%s.grep' % self.config.trustsfile, grepout) if self.config.outputjson: jsonout = self.generateJsonList(dd.trusts) self.writeJsonFile('%s.json' % self.config.trustsfile, jsonout) - if self.config.outputgrep: - grepout = self.generateGrepList(dd.trusts, self.trustattributes) - self.writeGrepFile('%s.grep' % self.config.trustsfile, grepout) #Some quick logging helpers def log_warn(text): diff --git a/ldapdomaindump/pretty.py b/ldapdomaindump/pretty.py new file mode 100644 index 0000000..285f05a --- /dev/null +++ b/ldapdomaindump/pretty.py @@ -0,0 +1,152 @@ +from __future__ import print_function +from builtins import str +import argparse +import json +import os.path +import re +from time import strftime, gmtime + +class PrettyOuput(object): + + def d2b(self, a): + tbin = [] + while a: + tbin.append(a % 2) + a //= 2 + + t2bin = tbin[::-1] + if len(t2bin) != 8: + for x in range(6 - len(t2bin)): + t2bin.insert(0, 0) + return ''.join([str(g) for g in t2bin]) + + def convert(self, time): + if isinstance(time, str): + return time + if time == 0: + return "None" + if time == -9223372036854775808: + return "Not Set" + sec = abs(time) // 10000000 + days = sec // 86400 + sec -= 86400*days + hrs = sec // 3600 + sec -= 3600*hrs + mins = sec // 60 + sec -= 60*mins + result = "" + if days > 1: + result += "{0} days ".format(days) + elif days == 1: + result += "{0} day ".format(days) + if hrs > 1: + result += "{0} hours ".format(hrs) + elif hrs == 1: + result += "{0} hour ".format(hrs) + if mins > 1: + result += "{0} minutes ".format(mins) + elif mins == 1: + result += "{0} minute ".format(mins) + return result + + def password_complexity(self, data): + + print(''' + +-----------------------------------------+ + | Password Policy Information | + +-----------------------------------------+ + ''') + + print("[+] Password Info for Domain:", data[0]['attributes']['dc'][0].upper()) + print("\t[+] Minimum password length: ", data[0]['attributes']['instanceType'][0]) + print("\t[+] Password history length:", data[0]['attributes']['pwdHistoryLength'][0]) + + password_properties = self.d2b(data[0]['attributes']['pwdProperties'][0]) + print("\t[+] Password Complexity Flags:", password_properties) + print("") + print("\t\t[+] Domain Refuse Password Change:", password_properties[0]) + print("\t\t[+] Domain Password Store Cleartext:", password_properties[1]) + print("\t\t[+] Domain Password Lockout Admins:", password_properties[2]) + print("\t\t[+] Domain Password No Clear Change:", password_properties[3]) + print("\t\t[+] Domain Password No Anon Change:", password_properties[4]) + print("\t\t[+] Domain Password Complex:", password_properties[5]) + print("") + print("\t[+] Maximum password age:", self.convert(data[0]['attributes']['maxPwdAge'][0])) + print("\t[+] Minimum password age:", self.convert(data[0]['attributes']['minPwdAge'][0])) + print("\t[+] Reset Account Lockout Counter:", self.convert(data[0]['attributes']['lockoutDuration'][0])) + print("\t[+] Account Lockout Threshold:", data[0]['attributes']['lockoutThreshold'][0]) + print("\t[+] Forced Log off Time:", self.convert(data[0]['attributes']['forceLogoff'][0])) + + def domain_info(self, data): + print(''' + +--------------------------------------+ + | Getting Domain Sid For | + +--------------------------------------+ + ''') + print('[+] Domain Name:', data[0]['attributes']['dc'][0]) + print('Domain Sid:', data[0]['attributes']['objectSid'][0]) + print('') + return data[0]['attributes']['dc'][0] + + def user_info(self, users, dc): + print(''' + +------------------------+ + | Users Infos | + +------------------------+ + ''') + for user in users: + desc = user['attributes'].get('description')[0] if user['attributes'].get('description') else "(null)" + print("Account: " + dc + "\\" + user['attributes']['sAMAccountName'][0] + "\tName: " + user['attributes']['name'][0] + "\tDesc: " + desc) + + print("") + for user in users: + print("user:[" + user['attributes']['sAMAccountName'][0] + "]") + print("") + + def groups_info(self, groups, dc): + print(''' + +------------------------+import os.path + | Groups Infos | + +------------------------+ + ''') + for group in groups: + print("group:[" + group['attributes']['name'][0] + "]") + + for group in groups: + if group['attributes'].get('member'): + users = re.findall(r"^CN=([\w\s\-\_\{\}\.\$\#]+)", '\n'.join(group['attributes']['member']), re.M) + if users: + print("\n[+] Getting domain group memberships:") + for user in users: + if user == "S-1-5-11": + user = "NT AUTHORITY\\Authenticated Users" + elif user == "S-1-5-4": + user = "NT AUTHORITY\\INTERACTIVE" + elif user == "S-1-5-17": + user = "NT AUTHORITY\\IUSR" + print("Group '" + group['attributes']['name'][0] + "' has member: " + dc + "\\" + user) + +def main(): + parser = argparse.ArgumentParser(description='LDAPDomainDump to pretty output like enum4linux.') + + #Main parameters + parser.add_argument("-d", "--directory", help="The ldapdomaindump directory where the json files are saved. Required files: domain_users.json, domain_groups.json and domain_policy.json") + args = parser.parse_args() + + if args.directory: + with open(args.directory + '/domain_policy.json') as f: + domain = json.load(f) + with open(args.directory + '/domain_users.json') as f: + users = json.load(f) + with open(args.directory + '/domain_groups.json') as f: + groups = json.load(f) + pretty = PrettyOuput() + dc = pretty.domain_info(domain) + pretty.password_complexity(domain) + pretty.user_info(users, dc.upper()) + pretty.groups_info(groups, dc.upper()) + else: + print("Missing parameter --directory /output/") + +if __name__ == "__main__": + main() diff --git a/ldapdomaindump.egg-info/PKG-INFO b/ldapdomaindump.egg-info/PKG-INFO new file mode 100644 index 0000000..2a3bc12 --- /dev/null +++ b/ldapdomaindump.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: ldapdomaindump +Version: 0.9.3 +Summary: Active Directory information dumper via LDAP +Home-page: https://github.com/dirkjanm/ldapdomaindump/ +Author: Dirk-jan Mollema +Author-email: dirkjan@sanoweb.nl +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/ldapdomaindump.egg-info/SOURCES.txt b/ldapdomaindump.egg-info/SOURCES.txt new file mode 100644 index 0000000..f0588cf --- /dev/null +++ b/ldapdomaindump.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +LICENSE +MANIFEST.in +Readme.md +setup.py +bin/ldapdomaindump +bin/ldd2bloodhound +bin/ldd2pretty +ldapdomaindump/__init__.py +ldapdomaindump/__main__.py +ldapdomaindump/convert.py +ldapdomaindump/pretty.py +ldapdomaindump/style.css +ldapdomaindump.egg-info/PKG-INFO +ldapdomaindump.egg-info/SOURCES.txt +ldapdomaindump.egg-info/dependency_links.txt +ldapdomaindump.egg-info/requires.txt +ldapdomaindump.egg-info/top_level.txt \ No newline at end of file diff --git a/ldapdomaindump.egg-info/dependency_links.txt b/ldapdomaindump.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ldapdomaindump.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/ldapdomaindump.egg-info/requires.txt b/ldapdomaindump.egg-info/requires.txt new file mode 100644 index 0000000..74a9dfb --- /dev/null +++ b/ldapdomaindump.egg-info/requires.txt @@ -0,0 +1,3 @@ +dnspython +future +ldap3!=2.5.0,!=2.5.2,!=2.6,>=2.5 diff --git a/ldapdomaindump.egg-info/top_level.txt b/ldapdomaindump.egg-info/top_level.txt new file mode 100644 index 0000000..88b4d4f --- /dev/null +++ b/ldapdomaindump.egg-info/top_level.txt @@ -0,0 +1 @@ +ldapdomaindump diff --git a/ldapdomaindump.py b/ldapdomaindump.py deleted file mode 100644 index c0bb795..0000000 --- a/ldapdomaindump.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -import ldapdomaindump -ldapdomaindump.main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py index 0d16462..8a1b0e7 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ -from setuptools import setup -setup(name='ldapdomaindump', - version='0.9.1', - description='Active Directory information dumper via LDAP', - author='Dirk-jan Mollema', - author_email='dirkjan@sanoweb.nl', - url='https://github.com/dirkjanm/ldapdomaindump/', - packages=['ldapdomaindump'], - install_requires=['dnspython', 'ldap3==2.5.1', 'future'], - package_data={'ldapdomaindump': ['style.css']}, - include_package_data=True, - scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound'] - ) +from setuptools import setup +setup(name='ldapdomaindump', + version='0.9.3', + description='Active Directory information dumper via LDAP', + author='Dirk-jan Mollema', + author_email='dirkjan@sanoweb.nl', + url='https://github.com/dirkjanm/ldapdomaindump/', + packages=['ldapdomaindump'], + install_requires=['dnspython', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6', 'future'], + package_data={'ldapdomaindump': ['style.css']}, + include_package_data=True, + scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound', 'bin/ldd2pretty'] + )