diff --git a/ldap_extra/getAttribs.py b/ldap_extra/getAttribs.py new file mode 100644 index 0000000..b704a84 --- /dev/null +++ b/ldap_extra/getAttribs.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import sys + +import ldap +from ldap.filter import escape_filter_chars + +# ============================================================================ +# Configuration +# ============================================================================ + +LDAP_URI = "ldaps://ldap.example.com" +BIND_DN = "uid=admin,ou=People,dc=example,dc=com" +BIND_PW = "password" +BASE_DN = "dc=example,dc=com" + +DEFAULT_KEY_ATTRIBUTE = "customStaffNo" +DEFAULT_DELIMITER = "|" +DEFAULT_OUTPUT_FILE = "/tmp/ldap_output.csv" + +INFO = "[i]" +START = "[*]" +SUCCESS = "[+]" +WARN = "[!]" +ERROR = "[x]" + +# ============================================================================ + + +def read_input_file(filename): + values = [] + + with open(filename, "r") as f: + for line in f: + line = line.strip() + if line: + values.append(line) + + return values + + +def ldap_connect(): + conn = ldap.initialize(LDAP_URI) + conn.protocol_version = ldap.VERSION3 + conn.simple_bind_s(BIND_DN, BIND_PW) + return conn + + +def decode(value): + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + return str(value) + + +def update_progress(current, total, matched, not_found): + """Update the progress display in-place.""" + + sys.stdout.write("\033[3A") + + sys.stdout.write(f"\r{INFO} Progress : {current:<6} / {total}\n") + sys.stdout.write(f"\r{INFO} Entries matched : {matched:<6}\n") + sys.stdout.write(f"\r{INFO} Not found : {not_found:<6}\n") + + sys.stdout.flush() + + +def main(): + parser = argparse.ArgumentParser() + + group = parser.add_mutually_exclusive_group(required=True) + + group.add_argument( + "-F", + "--file", + help="Input file containing one value per line", + ) + + group.add_argument( + "-v", + "--values", + help="Comma-separated list of search values", + ) + + parser.add_argument( + "-k", + "--key", + default=DEFAULT_KEY_ATTRIBUTE, + help=f"LDAP attribute used for searching (default: {DEFAULT_KEY_ATTRIBUTE})", + ) + + parser.add_argument( + "-a", + "--attributes", + required=True, + help="Comma-separated LDAP attributes to retrieve", + ) + + parser.add_argument( + "-o", + "--output", + default=DEFAULT_OUTPUT_FILE, + help=f"Output CSV file (default: {DEFAULT_OUTPUT_FILE})", + ) + + parser.add_argument( + "-d", + "--delimiter", + default=DEFAULT_DELIMITER, + help=f"Output CSV delimiter (default: {DEFAULT_DELIMITER})", + ) + + args = parser.parse_args() + + attributes = [x.strip() for x in args.attributes.split(",") if x.strip()] + + if args.file: + search_values = read_input_file(args.file) + else: + search_values = [ + x.strip() + for x in args.values.split(",") + if x.strip() + ] + + total = len(search_values) + + matched = 0 + not_found = [] + incomplete = [] + + print(f"{START} Fetching LDAP attributes...") + print(f"{INFO} Search key : {args.key}") + print(f"{INFO} Requested attributes: {', '.join(attributes)}") + print(f"{INFO} Input values : {total}") + + print(f"{INFO} Progress : 0 / {total}") + print(f"{INFO} Entries matched : 0") + print(f"{INFO} Not found : 0") + + conn = ldap_connect() + + with open(args.output, "w", newline="") as csvfile: + writer = csv.writer(csvfile, delimiter=args.delimiter) + + writer.writerow(attributes) + + for current, value in enumerate(search_values, start=1): + flt = f"({args.key}={escape_filter_chars(value)})" + + result = conn.search_s( + BASE_DN, + ldap.SCOPE_SUBTREE, + flt, + attributes, + ) + + if not result: + not_found.append(value) + update_progress(current, total, matched, len(not_found)) + continue + + matched += 1 + + _, entry = result[0] + + row = [] + missing_attribute = False + + for attr in attributes: + vals = entry.get(attr) + + if vals: + row.append(";".join(decode(v) for v in vals)) + else: + row.append("") + missing_attribute = True + + if missing_attribute: + incomplete.append(value) + + writer.writerow(row) + + update_progress(current, total, matched, len(not_found)) + + conn.unbind_s() + + print() + + if not_found: + print(f"{WARN} Missing entries") + print(f" {args.key}: {', '.join(not_found)}") + print() + + if incomplete: + print(f"{WARN} Incomplete entries") + print(" Missing one or more requested attributes:") + print(f" {args.key}: {', '.join(incomplete)}") + print() + + print(f"{SUCCESS} Output written to {args.output}") + + +if __name__ == "__main__": + main() \ No newline at end of file