require 'rexml/document' require 'cgi' require 'digest' require 'set' require_relative 'oms_common' require_relative 'omslog' class ChangeTracking PREV_HASH = "PREV_HASH" LAST_UPLOAD_TIME = "LAST_UPLOAD_TIME" @@force_send_last_upload = Time.now @@lastInventorySnapshotTime = Time.now @@prev_hash = "" def self.prev_hash= (value) @@prev_hash = value end def self.instanceXMLtoHash(instanceXML) ret = {} propertyXPath = "PROPERTY" instanceXML.elements.each(propertyXPath) { |inst| name = inst.attributes['NAME'] rexmlText = REXML::XPath.first(inst, 'VALUE').get_text # TODO escape unicode chars like "&" value = rexmlText ? rexmlText.value.strip : '' ret[name] = value } ret end def self.serviceXMLtoHash(serviceXML, isInventorySnapshot = false) serviceHash = instanceXMLtoHash(serviceXML) serviceHash["CollectionName"] = serviceHash["Name"] #InventoryChecksum should be calculated before InventorySnapshot is filled. if isInventorySnapshot == false serviceHash["InventoryChecksum"] = Digest::SHA256.hexdigest(serviceHash.to_json) end serviceHash["IsInventorySnapshot"] = isInventorySnapshot serviceHash end def self.packageXMLtoHash(packageXML, isInventorySnapshot = false) packageHash = instanceXMLtoHash(packageXML) ret = {} ret["Architecture"] = packageHash["Architecture"] ret["CollectionName"] = packageHash["Name"] ret["CurrentVersion"] = packageHash["Version"] ret["Name"] = packageHash["Name"] ret["Publisher"] = packageHash["Publisher"] ret["Size"] = packageHash["Size"] ret["Timestamp"] = OMS::Common.format_time(packageHash["InstalledOn"].to_i) #InventoryChecksum should be calculated before InventorySnapshot is filled. if isInventorySnapshot == false ret["InventoryChecksum"] = Digest::SHA256.hexdigest(packageHash.to_json) end ret["IsInventorySnapshot"] = isInventorySnapshot ret end def self.fileInventoryXMLtoHash(fileInventoryXML, isInventorySnapshot = false) fileInventoryHash = instanceXMLtoHash(fileInventoryXML) ret = {} ret["FileContentChecksum"] = fileInventoryHash["Checksum"] ret["FileSystemPath"] = fileInventoryHash["DestinationPath"] ret["CollectionName"] = fileInventoryHash["DestinationPath"] ret["Size"] = fileInventoryHash["FileSize"] ret["Owner"] = fileInventoryHash["Owner"] ret["Group"] = fileInventoryHash["Group"] ret["Mode"] = fileInventoryHash["Mode"] ret["Contents"] = fileInventoryHash["Contents"] ret["DateModified"] = OMS::Common.format_time_str(fileInventoryHash["ModifiedDate"]) ret["DateCreated"] = OMS::Common.format_time_str(fileInventoryHash["CreatedDate"]) #InventoryChecksum should be calculated before InventorySnapshot is filled. if isInventorySnapshot == false ret["InventoryChecksum"] = Digest::SHA256.hexdigest(fileInventoryHash.to_json) end ret["IsInventorySnapshot"] = isInventorySnapshot ret end def self.strToXML(xml_string) xml_unescaped_string = CGI::unescapeHTML(xml_string) REXML::Document.new xml_unescaped_string end # Returns an array of xml instances (all types) def self.getInstancesXML(inventoryXML) instances = [] xpathFilter = "INSTANCE/PROPERTY.ARRAY/VALUE.ARRAY/VALUE/INSTANCE" inventoryXML.elements.each(xpathFilter) { |inst| instances << inst } instances end def self.removeDuplicateCollectionNames(data_items) collection_names = Set.new data_items.select { |data_item| collection_names.add?(data_item["CollectionName"]) && true || false } end def self.isPackageInstanceXML(instanceXML) instanceXML.attributes['CLASSNAME'] == 'MSFT_nxPackageResource' end def self.isServiceInstanceXML(instanceXML) instanceXML.attributes['CLASSNAME'] == 'MSFT_nxServiceResource' end def self.isFileInventoryInstanceXML(instanceXML) instanceXML.attributes['CLASSNAME'] == 'MSFT_nxFileInventoryResource' end def self.transform(inventoryXMLstr, isInventorySnapshot = false) # Extract the instances in xml format inventoryXML = strToXML(inventoryXMLstr) instancesXML = getInstancesXML(inventoryXML) # Split packages from services packagesXML = instancesXML.select { |instanceXML| isPackageInstanceXML(instanceXML) } servicesXML = instancesXML.select { |instanceXML| isServiceInstanceXML(instanceXML) } fileInventoriesXML = instancesXML.select { |instanceXML| isFileInventoryInstanceXML(instanceXML) } # Convert to xml to hash/json representation packages = packagesXML.map { |package| packageXMLtoHash(package, isInventorySnapshot)} services = servicesXML.map { |service| serviceXMLtoHash(service, isInventorySnapshot)} fileInventories = fileInventoriesXML.map { |fileInventory| fileInventoryXMLtoHash(fileInventory, isInventorySnapshot)} # Remove duplicate services because duplicate CollectionNames are not supported. TODO implement ordinal solution packages = removeDuplicateCollectionNames(packages) services = removeDuplicateCollectionNames(services) fileInventories = removeDuplicateCollectionNames(fileInventories) ret = {} if packages.size > 0 ret["packages"] = packages end if services.size > 0 ret["services"] = services end if fileInventories.size > 0 ret["fileInventories"] = fileInventories end return ret end def self.computechecksum(inventory_hash) inventory = {} inventoryChecksum = {} if inventory_hash.has_key?("packages") inventory = inventory_hash["packages"] elsif inventory_hash.has_key?("services") inventory = inventory_hash["services"] elsif inventory_hash.has_key?("fileInventories") inventory = inventory_hash["fileInventories"] end inventory.each do |inventory_item| inventoryChecksum[inventory_item["CollectionName"]] = inventory_item["InventoryChecksum"] inventory_item.delete("InventoryChecksum") end return inventoryChecksum end def self.comparechecksum(previous_inventory, current_inventory) inventoryChecksumInstalled = {} if !current_inventory.nil? inventoryChecksumInstalled = current_inventory.select { |key, value| lookupchecksum(key, value, previous_inventory) } end inventoryChecksumRemoved = {} if !previous_inventory.nil? inventoryChecksumRemoved = previous_inventory.select { |key, value| lookupchecksum(key, value, current_inventory) } end return inventoryChecksumRemoved.merge!(inventoryChecksumInstalled) end def self.lookupchecksum(key, value, previous_inventory) if !previous_inventory.nil? and previous_inventory.has_key?(key) if value == previous_inventory[key] return false end end return true end def self.markchangedinventory(checksum_filter, inventory_hash) inventory = {} if inventory_hash.has_key?("fileInventories") inventory = inventory_hash["fileInventories"] inventory.each {|inventory_item| markchanged(inventory_item["CollectionName"], checksum_filter, inventory_item)} filteredInventory = inventory inventory_hash["fileInventories"] = filteredInventory end return inventory_hash end def self.filterchecksum(key, checksum_filter) if checksum_filter.has_key?(key) return true end return false end def self.markchanged(key, checksum_filter, inventory_item) if checksum_filter.has_key?(key) inventory_item["FileContentBlobLink"] = " " end end def self.wrap (inventory_hash, host, time) timestamp = OMS::Common.format_time(time) wrapper = { "DataType"=>"CONFIG_CHANGE_BLOB", "IPName"=>"changetracking", "DataItems"=>[] } # Add entries to DataItems array only if they exist. if inventory_hash.has_key?("packages") wrapper["DataItems"] << { "Timestamp" => timestamp, "Computer" => host, "ConfigChangeType"=> "Software.Packages", "Collections"=> inventory_hash["packages"] } end if inventory_hash.has_key?("services") wrapper["DataItems"] << { "Timestamp" => timestamp, "Computer" => host, "ConfigChangeType"=> "Daemons", "Collections"=> inventory_hash["services"] } end if inventory_hash.has_key?("fileInventories") wrapper["DataItems"] << { "Timestamp" => timestamp, "Computer" => host, "ConfigChangeType"=> "Files", "Collections"=> inventory_hash["fileInventories"] } end # Returning the default wrapper. This can be nil as well (nothing in the # DatatItems array) if wrapper["DataItems"].size == 1 return wrapper elsif wrapper["DataItems"].size > 1 return {} # Returning null. else return {} # Returning null. end end def self.getHash(file_path) ret = {} if File.exist?(file_path) # If file exists File.open(file_path, "r") do |f| # Open file f.each_line do |line| line.split(/\r?\n/).reject{ |l| !l.include? "=" }.map { |s| s.split("=")}.map { |key, value| ret[key] = value } end end return ret else return nil end end def self.setHash(prev_hash, last_upload_time, file_path) # File.write('/path/to/file', 'Some glorious content') File.open(file_path, "w+", 0644) do |f| # Open file f.puts "#{PREV_HASH}=#{prev_hash}" f.puts "#{LAST_UPLOAD_TIME}=#{last_upload_time}" end end def self.setInventoryTimestamp(timestamp, file_path) File.open(file_path, "w+", 0644) do |f| f.puts "#{timestamp}" end end def self.getInventoryTimestampInRubyTime(file_path) time = Time.now - (10 * 60 * 60) # default time to return if file not found or read error if File.exist?(file_path) content = File.open(file_path, &:gets) if !content.nil? and !content.empty? time = DateTime.parse(content).to_time end end return time end def self.transform_and_wrap(inventoryFile, inventoryHashFile, inventoryTimestampFile) if File.exist?(inventoryFile) # Get the parameters ready. time = Time.now force_send_run_interval_hours = 10 force_send_run_interval = force_send_run_interval_hours.to_i * 3600 @hostname = OMS::Common.get_hostname or "Unknown host" # Read the inventory XML. file = File.open(inventoryFile, "rb") xml_string = file.read; nil # To top the output to show up on STDOUT. # ########### INVENTORY ##################### # if its time to send inventory # send the inventory snapshot and dont update any hashes, so change tracking sends a snapshot subsequently isInventorySnapshot = false @@lastInventorySnapshotTime = getInventoryTimestampInRubyTime(inventoryTimestampFile) if Time.now.to_i - @@lastInventorySnapshotTime.to_i >= force_send_run_interval isInventorySnapshot = true end transformed_hash_map = ChangeTracking.transform(xml_string, isInventorySnapshot) if isInventorySnapshot output = ChangeTracking.wrap(transformed_hash_map, @hostname, time) setInventoryTimestamp(Time.now, inventoryTimestampFile) return output end ############ END INVENTORY ############## previousSnapshot = ChangeTracking.getHash(inventoryHashFile) previous_inventory_checksum = {} begin if !previousSnapshot.nil? previous_inventory_checksum = JSON.parse(previousSnapshot[PREV_HASH]) end rescue previousSnapshot = nil end current_inventory_checksum = ChangeTracking.computechecksum(transformed_hash_map) changed_checksum = ChangeTracking.comparechecksum(previous_inventory_checksum, current_inventory_checksum) transformed_hash_map_with_changes_marked = ChangeTracking.markchangedinventory(changed_checksum, transformed_hash_map) output = ChangeTracking.wrap(transformed_hash_map_with_changes_marked, @hostname, time) hash = current_inventory_checksum.to_json # Send inventory irrespectve of changes if !changed_checksum.nil? and !changed_checksum.empty? ChangeTracking.setHash(hash, Time.now, inventoryHashFile) return output else return {} end else return {} end end end