class Ucert::AdTracker
Class to handle the ative directory local data cache repository. Note this class depends on the 3rd party program 'openldap' @ www.openldap.org
Attributes
Public Class Methods
Instance default variables
# File lib/ucert/ad_tracker.rb, line 21 def initialize (params ={}) # Specify the dependency 3rd party application 'ldapsearch' installation path @verbose=params.fetch(:verbose, false) @refresh_dns_records=params.fetch(:refresh_dns_records, false) # params to turn on/off DNS records refreshment. Use with caution as it's time intensive task. @program_ldapsearch = "/usr/bin/ldapsearch" # Specify default ldap connector and connecting credential; currently we support: 1) 'openldap' @ldap_connector = "openldap" @ldap_connector_id=params.fetch(:ldap_connector_id,"") @ldap_connector_pass=params.fetch(:ldap_connector_pass,"") @ldap_host = params.fetch(:ldap_host, "CMBNY-OADC2.ny.cmbchina.com") @ldap_port = params.fetch(:ldap_port, "389") @ldap_base = params.fetch(:ldap_base, "DC=ny,DC=cmbchina,DC=com") # Specify the dependency 3rd party application 'ldapsearh' command argument options #@ldapsearch_argv_1 = " -b \"DC=ny,DC=cmbchina,DC=com\" -h CMBNY-OADC2.ny.cmbchina.com -p 389 -D " @ldapsearch_argv_1 = " -b " + @ldap_base + " -h " + @ldap_host + " -p " + @ldap_port + " -D " @ldapsearch_argv_2 = " -s sub \"objectcategory=" # Specify the cache file location for 'ldapsearch' @ldapsearch_cache_person = File.dirname(__FILE__) + "/../../data/ad/ldap_person.txt" @ldapsearch_cache_computer = File.dirname(__FILE__) + "/../../data/ad/ldap_computer.txt" # Specify the local hosts cache file for fast DNS resolving @hosts_cache = File.dirname(__FILE__) + "/../../data/ad/hosts" @known_hosts=load_known_hosts(@hosts_cache) # AD Delta detection @ad_delta_map = File.dirname(__FILE__) + "/../../data/ad/ad_delta.txt" @ad_delta = load_known_user_map_from_file (@ad_delta_map) # class instance variables to keep track of the AD record changes @ad_person_records = Hash.new @ad_computer_records = Hash.new # Refer to microsoft KB 'How to use the UserAccountControl flags' - https://support.microsoft.com/en-us/kb/305144 @acct_cntl_code={"SCRIPT"=>1, "ACCOUNTDISABLE"=>2, "HOMEDIR_REQUIRED"=>4, "LOCKOUT"=>5, "PASSWD_NOTREQD"=>6, \ "PASSWD_CANT_CHANGE"=>7, "ENCRYPTED_TEXT_PWD_ALLOWED"=>8, "TEMP_DUPLICATE_ACCOUNT"=>9, "NORMAL_ACCOUNT"=>10, \ "'INTERDOMAIN_TRUST_ACCOUNT'"=>12, "WORKSTATION_TRUST_ACCOUNT"=>13, "SERVER_TRUST_ACCOUNT"=>14, \ "DONT_EXPIRE_PASSWORD"=>17, "MNS_LOGON_ACCOUNT"=>18, "SMARTCARD_REQUIRED"=>19, \ "TRUSTED_FOR_DELEGATION"=>20, "NOT_DELEGATED"=>21, "USE_DES_KEY_ONLY"=>22, "DONT_REQ_PREAUTH"=>23, \ "PASSWORD_EXPIRED"=>24, "TRUSTED_TO_AUTH_FOR_DELEGATION"=>25, "PARTIAL_SECRETS_ACCOUNT"=>27} # loading the instance variables load_ad(@ldap_connector) end
Public Instance Methods
generic text string search of ad local cache for matched record, return the record primary key, i.e. DN
# File lib/ucert/ad_tracker.rb, line 210 def ad_search_by_text (keyword,opt="person") begin puts "Begin search on the cache for: #{keyword}" if @verbose return nil if keyword.nil? keywords=keyword.downcase.split(/(\,|\s|\;|\&|\!|\@|\#|\$|\%|\^|\*|\(|\)|\-|\=|\+|\=|\{|\}|\[|\]|\:|\~)/) \ - [" ", "", nil, ",", ";", "&", "!", "@", "#", "$", "%", "^", "*", "(", ")", "-", "+", "=", "{", "}", "[", "]", ":" "~"] #txt.downcase! case opt when "computer" @ad_computer_records.each do |key, val| puts "Searching computer cache data." if @verbose val.map do |entry| every=entry.to_s.downcase success=keywords.inject(true) {|found,word| break unless found; every.include?(word) && found; } if success return key end end end when "person" @ad_person_records.each do |key, val| puts "Searching person cache data." if @verbose word_1=keywords.join(" ") word_2=keywords.reverse.join(" ") val.map do |entry| puts "First order searching: #{entry} for #{word_1} or #{word_2}" if @verbose every=entry.to_s.downcase if every.include?(" " + word_1) or every.include?(" " + word_2) puts "Best match found: #{entry}" if @verboseirb return key elsif every.include?(word_1) or every.include?(word_2) puts "Best match found: #{entry}" if @verbose return key end end end @ad_person_records.each do |key, val| val.map do |entry| puts "Secondary order searching: #{entry}" if @verbose every=entry.to_s.downcase success2=keywords.inject(true) {|found,word| break unless found; every.include?(word) && found; } if success2 puts "Close match found: #{entry}" if @verbose return key end end end else raise "Unknow cache: #{opt}" end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
generic text string search of ad local cache for matched records. Input is a keyword string, return the record primary keys, i.e. multiple DNs in an array.
# File lib/ucert/ad_tracker.rb, line 269 def ad_searches_by_text (keyword,opt="person",max=10) begin puts "Begin searches on the cache for: #{keyword}" if @verbose return nil if keyword.nil? keywords=keyword.downcase.split(/(\,|\s|\;|\&|\!|\@|\#|\$|\%|\^|\*|\(|\)|\-|\=|\+|\=|\{|\}|\[|\]|\:|\~)/) \ - [" ", "", nil, ",", ";", "&", "!", "@", "#", "$", "%", "^", "*", "(", ")", "-", "+", "=", "{", "}", "[", "]", ":" "~"] search_result=Array.new case opt when "computer" cnt=0 @ad_computer_records.each do |key, val| puts "Searching computer cache data on: #{key}" if @verbose val.map do |entry| break if cnt >= max # limit returned records to max every = entry.to_s.downcase success=keywords.inject(true) {|found,word| break unless found; every.include?(word) && found; } if success puts "Match found: #{entry}" if @verbose search_result.push(key) cnt+=1 break end end end when "person" cnt=0 # Perform 1st order search of CN portion of DN only for the best match @ad_person_records.keys.map do |x| puts "Searching person DN entry for #{key}" if @verbose word_1=keywords.join(" ").downcase word_2=keywords.reverse.join(" ").downcase y=get_cn(x).downcase if y.include?(word_1) or y.include?(word_2) puts "Match found: #{x}" if @verbose search_result.push(x) cnt+=1 end end # Perform 2nd order search of complete keyword string on person record attributes @ad_person_records.each do |key, val| puts "Searching person cache data on: #{key}" if @verbose word_1=keywords.join(" ") word_2=keywords.reverse.join(" ") val.map do |entry| break if cnt >= max # limit returned records to max every = entry.to_s.downcase if every.include?(word_1) or every.include?(word_2) puts "Match found: #{entry}" if @verbose search_result.push(key) cnt+=1 break end end end # Perform 3rd order search of partial keyword string on the person record attributes @ad_person_records.each do |key, val| puts "Searching person cache data on: #{key}" if @verbose val.map do |entry| break if cnt >= max # limit returned records to max every = entry.to_s.downcase success=keywords.inject(true) {|found,word| break unless found; every.include?(word) && found; } #entry_clean=entry.chomp #if entry_clean.downcase.include?(txt) if success puts "Match found: #{entry}" if @verbose search_result.push(key) cnt+=1 break end end end # else raise "Unknow cache: #{opt}" end return search_result.uniq rescue => ee puts "Exception on method #{__method__}: #{ee}" return search_result end end
Convertion of MS UserAccountControl code in decimal value back to 'Property flag'
# File lib/ucert/ad_tracker.rb, line 504 def cntl_code_2_property_flag (code) begin puts "Perform user account status lookup for user account control code: #{code}" if @verbose code_2_status = Hash.new @acct_cntl_code.each { |k,v| code_2_status[v]=k } account_status = Array.new b_code = code.to_i.to_s(2) a_code = b_code.split('').reverse (0...a_code.count).each do |x| if a_code[x] === "1" status = code_2_status[x+1] account_status.push(status) end end return account_status.join(' + ') rescue => ee puts "Exception on method #{__method__}: #{ee}" if @verbose return "Unknown" end end
String manipulation to extract the first CN from the DN
# File lib/ucert/ad_tracker.rb, line 453 def extract_first_cn (dn) begin return nil if dn.nil? or dn.empty? dn.split(',').map do |x| return x if x=~ /^cn=/i end rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Retrieve the specific dn object
# File lib/ucert/ad_tracker.rb, line 353 def get_ad_record (dn) begin return @ad_person_records[dn] if @ad_person_records.key?(dn) return @ad_computer_records[dn] if @ad_computer_records.key?(dn) return "Unfound" rescue => ee puts "Exception on method #{__method__}: #{ee}" return "Unfound" end end
Retrieve the first CN “Full Name” for the DN
# File lib/ucert/ad_tracker.rb, line 435 def get_cn (dn) begin attrs=@ad_person_records[dn] if @ad_person_records.key?(dn) attrs=@ad_computer_records[dn] if @ad_computer_records.key?(dn) attrs.map do |line| (key,val)=line.chomp.split(':') if key=="cn" return val.strip end end return "Unfound" rescue => ee puts "Exception on method #{__method__}: #{ee}" return "Unfound" end end
Retrieve a list of CNs i.e. “Full Name” for the dns
# File lib/ucert/ad_tracker.rb, line 465 def get_cns (dns) begin result=Array.new dns.map do |dn| cn=get_cn(dn) result.push(cn) end return result rescue => ee puts "Exception on method #{__method__}: #{ee}" return result end end
Retrieve the specific attribute from the dn's attribute collection
# File lib/ucert/ad_tracker.rb, line 365 def get_dn_attribute (opt="person",dn,attr_name) begin case opt when "person" attrs=@ad_person_records[dn] if @ad_person_records.key?(dn) when "computer" attrs=@ad_computer_records[dn] if @ad_computer_records.key?(dn) else raise "Error - unknown objectcategory: #{opt}" end #puts attrs attrs.map do |line| (key,val)=line.chomp.split(':') if key==attr_name return val.strip end end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" return nil end end
Retrieve the attributes from the dn's attribute collection; the output is the matchs inside an array
# File lib/ucert/ad_tracker.rb, line 390 def get_dn_attributes (opt,dn,attr_name) begin founds = Array.new case opt when "person" attrs=@ad_person_records[dn] if @ad_person_records.key?(dn) when "computer" attrs=@ad_computer_records[dn] if @ad_computer_records.key?(dn) else raise "Error - unknown objectcategory: #{opt}" end #puts attrs attrs.map do |line| (key,val)=line.chomp.split(':') if key==attr_name founds.push(val.strip) end end return founds rescue => ee puts "Exception on method #{__method__}: #{ee}" return nil end end
# File lib/ucert/ad_tracker.rb, line 479 def get_os_info (dn) begin puts "Retrieve Operation System information for: #{dn}" if @verbose os_info = String.new attrs=@ad_computer_records[dn] if @ad_computer_records.key?(dn) attrs.map do |line| (key,val)=line.chomp.split(':') case key when "operatingSystem" os_info += val.strip when "operatingSystemVersion" os_info += val when "operatingSystemServicePack" os_info += val end end return os_info rescue => ee puts "Exception on method #{__method__}: #{ee}" if @verbose return "Unfound" end end
wrapper to Net::LDAP ldap.bind method
# File lib/ucert/ad_tracker.rb, line 78 def is_ldap_bind? begin ldap = Net::LDAP.new ldap.host = @ldap_host ldap.port = @ldap_port ldap.auth @ldap_connector_id, @ldap_connector_pass if ldap.bind puts "LDAP bind to #{@ldap_host} successfully!" return true else puts "LDAP bind to #{@ldap_host} fail!" return false end rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Load AD tracker instance variables from the cache files; re-create the cache files if non-existing.
# File lib/ucert/ad_tracker.rb, line 61 def load_ad (connector) begin case connector when "openldap" update_openldap_cache("person") unless File.exist?(@ldapsearch_cache_person) parse_openldap_cache("person") update_openldap_cache("computer") unless File.exist?(@ldapsearch_cache_computer) parse_openldap_cache("computer") else #do nothing end rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Load the hosts cache file for fast DNS resolving
# File lib/ucert/ad_tracker.rb, line 632 def load_known_hosts (f_hosts=@file_hosts) puts "Loading local hosts from file: #{f_hosts} ..." if @verbose begin known_hosts=Hash.new f=File.open(f_hosts, 'r') f.each do |line| next unless line =~ /\d+\.\d+\.\d+\.\d+/ entry=line.chomp.split(%r{\t+|\s+|\,}) key=entry[0].downcase value=entry[1] puts "Loading value pair: #{key} - #{value}" if @verbose known_hosts[key] = Hash.new unless known_hosts.key?(key) known_hosts[key]= value end f.close return known_hosts rescue => ee puts "Exception on method #{__method__}: #{ee}" return Hash.new end end
Local DNS reverse lookup
# File lib/ucert/ad_tracker.rb, line 655 def local_ip_2_host (ip) puts "Perform local IP to hostname lookup on IP: #{ip}" if @verbose begin ip.chomp! raise "Invalid IP address format: #{ip}" unless ip =~ /\d+\.\d+\.\d+\.\d+/ @known_hosts.to_a.reverse.to_h.each do |key,val| return key.to_s if val == ip end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" return nil end end
method to parse the openldap cache file, save the record into a hash data structure
# File lib/ucert/ad_tracker.rb, line 131 def parse_openldap_cache (opt) begin case opt when "person" file = @ldapsearch_cache_person when "computer" file = @ldapsearch_cache_computer else raise "Unkown log file type: #{opt}" end puts "Start working on openldap log file: #{file}" if @verbose raise "File not found. Please check your path again: #{file}" unless File.exist?(file) f_cache = File.open(file, 'r:ISO-8859-1:UTF-8') # recording bit to flag start / end of new ad record recording=false ad_objs=Hash.new dn_key=String.new # flag to check next line in file where the dn string may be split over to dn_key_nextline_check=false f_cache.each do |line| line.chomp! if dn_key_nextline_check puts "Perform DN Key next line check for its completeness: #{dn_key}" if @verbose if line =~ /^\s/ puts "Incomplete DN Key found: #{dn_key}" if @verbose dn_key=dn_key+line.sub(/^\s/,'') ad_objs[dn_key]=Array.new dn_key_nextline_check=false recording=true next else puts "DN Key found completed: #{dn_key}" if @verbose dn_key_nextline_check=false ad_objs[dn_key]=Array.new dn_key_nextline_check=false recording=true end end if line =~ /^dn\:/ puts "DN Key found in line: #{line}" if @verbose dn_key=line.split(':')[1].sub(/^\s/,'') puts "DN found: #{dn_key}" if @verbose dn_key_nextline_check=true next end if line.nil? or line.empty? puts "Done processing one record." if @verbose #sleep(2) recording=false next end if recording puts "Start recording for record: #{dn_key}" if @verbose if line=~ /^\s/ puts "Modify last attribute for its incompleteness: #{ad_objs[dn_key].last} - #{line} " if @verbose ad_objs[dn_key][-1] = ad_objs[dn_key].last + line.sub(/^\s/,'') else puts "Adding new record attribute: #{line}" if @verbose ad_objs[dn_key].push(line) end end end f_cache.close case opt when "person" @ad_person_records=ad_objs when "computer" @ad_computer_records=ad_objs save_hosts # refresh the hosts file else raise "Unkown log file type: #{opt}" end puts "Done processing the cache file: #{file}" if @verbose rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Print out Adstore cache record in tab-delimited file format
# File lib/ucert/ad_tracker.rb, line 527 def print (opt) begin puts "Print out the Active Directory Cache #{opt} record in tab-delimited format: " if @verbose case opt when "computer" @ad_computer_records.keys.map do |my_dn| my_sam_id=get_dn_attribute(opt,my_dn,"sAMAccountName") my_os=get_os_info(my_dn) my_host=get_dn_attribute(opt,my_dn,"dNSHostName") my_membership=get_dn_attributes(opt,my_dn,"memberOf") if @known_hosts.key?(my_host) my_ip=@known_hosts[my_host] else my_ip=nslookup(my_host) if @refresh_dns_records @known_hosts[my_host]=my_ip end my_created=get_dn_attribute(opt,my_dn,"whenCreated") puts "#{my_dn}|#{my_sam_id}|#{my_os}|#{my_host}|#{my_ip}|#{my_created}|#{my_membership}" end # Save the known hosts into cache file for future reference save_hosts(@hosts_cache) when "person" @ad_person_records.keys.map do |my_dn| my_sam_id=get_dn_attribute(opt,my_dn,"sAMAccountName") my_email=get_dn_attribute(opt,my_dn,"mail") my_name=get_cn(my_dn) my_dept=get_dn_attribute(opt,my_dn,"department") my_acct_code=get_dn_attribute(opt,my_dn,"userAccountControl") my_acct_status=cntl_code_2_property_flag(my_acct_code) my_workstations=get_dn_attribute(opt,my_dn,"userWorkstations") my_membership=get_dn_attributes(opt,my_dn,"memberOf") puts "#{my_dn}|#{my_name}|#{my_sam_id}|#{my_email}|#{my_dept}|#{my_acct_status}|#{my_workstations}|#{my_membership}" end else raise "Unknow cache table: #{opt} \t Please check your input Type again." end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
refresh @known_hosts instance based on the lastest ad_computer_records
# File lib/ucert/ad_tracker.rb, line 571 def refresh_hosts begin @ad_computer_records.keys.map do |my_dn| my_sam_id=get_dn_attribute("computer",my_dn,"sAMAccountName") my_os=get_os_info(my_dn) my_host=get_dn_attribute("computer",my_dn,"dNSHostName") my_membership=get_dn_attributes("computer",my_dn,"memberOf") if @known_hosts.key?(my_host) my_ip=@known_hosts[my_host] else my_ip=nslookup(my_host) @known_hosts[my_host]=my_ip end end rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
SAM ID to DN reverse lookup
# File lib/ucert/ad_tracker.rb, line 671 def sam_2_dn (id) puts "Perform SAM ID to DN lookup for: #{id}" if @verbose begin id.strip! @ad_person_records.keys.map do |my_dn| sam_id=get_dn_attribute("person",my_dn,"sAMAccountName") if sam_id == id return my_dn end end @ad_computer_records.keys.map do |my_dn| sam_id=get_dn_attribute("computer",my_dn,"sAMAccountName") if sam_id == id return my_dn end end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" return nil end end
Save the AD change back into cache file
# File lib/ucert/ad_tracker.rb, line 613 def save_delta (f_delta=@ad_delta_map) begin puts "Save the AD Delta table from memory to file: #{f_delta} ..." if @verbose timestamp=Time.now f=File.open(f_delta, 'w') f.write "# AD Delta created by the #{self.class} class #{__method__} method at: #{timestamp}" @ad_delta.each do |key, value| unless key =~ /\d+\.\d+\.\d+\.\d+/ or key.nil? f.write "\n#{key}|#{value}" end end f.close puts "AD delta records are successfully saved to: #{f_hosts}" if @verbose rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Save the known hosts table into cache file
# File lib/ucert/ad_tracker.rb, line 591 def save_hosts(f_hosts=@hosts_cache) begin puts "Save the hosts table from memory to file: #{f_hosts} ..." if @verbose # update the @known_hosts instance refresh_hosts if @refresh_dns_records # Write the @known_hosts instance back to hosts file timestamp=Time.now f=File.open(f_hosts, 'w') f.write "# local hosts file created by the #{self.class} class #{__method__} method at: #{timestamp}" (@known_hosts.keys-[nil]).sort.map do |key| unless key =~ /\d+\.\d+\.\d+\.\d+/ f.write "\n#{key}\t#{@known_hosts[key]}" end end f.close puts "local host repository is successfully saved to: #{f_hosts}" if @verbose rescue => ee puts "Exception on method #{__method__}: #{ee}" end end
Lookup DN by sAMAccountName attribute
# File lib/ucert/ad_tracker.rb, line 416 def sid_2_dn (sid) begin @ad_person_records.keys.map do |dn| attrs=@ad_person_records[dn] attrs.map do |line| (key,val)=line.chomp.split(':') if val.strip.upcase==sid.strip.upcase return dn end end end return nil rescue => ee puts "Exception on method #{__method__}: #{ee}" return "Unfound" end end
method to update the active directory local cache file
# File lib/ucert/ad_tracker.rb, line 97 def update_ad_cache (opt) begin puts "Start AD cache file update process for objectcategory: #{opt}" case @ldap_connector when "openldap" my_dn=sid_2_dn(@ldap_connector_id) abort "Error. Unknown user ID: #{@ldap_connector_id}" if my_dn.nil? case opt when "person" # cmd = @program_ldapsearch + @ldapsearch_argv_1 + "\"" + my_dn + "\" -w " + @ldap_connector_pass + @ldapsearch_argv_2 + opt + "\" > " + @ldapsearch_cache_person cmd = @program_ldapsearch + @ldapsearch_argv_1 + "\"" + @ldap_connector_id + "\" -w " + @ldap_connector_pass + @ldapsearch_argv_2 + opt + "\" > " + @ldapsearch_cache_person when "computer" # cmd = @program_ldapsearch + @ldapsearch_argv_1 + "\"" + my_dn + "\" -w " + @ldap_connector_pass + @ldapsearch_argv_2 + opt + "\" > " + @ldapsearch_cache_computer cmd = @program_ldapsearch + @ldapsearch_argv_1 + "\"" + @ldap_connector_id + "\" -w " + @ldap_connector_pass + @ldapsearch_argv_2 + opt + "\" > " + @ldapsearch_cache_computer else raise "Error - unknown objectcategory: #{opt}" end else raise "Error - unknown LDAP connector: #{@ldap_connector}" end # Test connection before the update if is_ldap_bind? puts "Execute shell command: #{cmd}" if @verbose system(cmd) puts "Done!" end sleep (2) rescue => ee puts "Exception on method #{__method__}: #{ee}" end end