chef/cookbooks/uber_helpers/libraries/node_utils.rb (879 lines of code) (raw):
#
# Cookbook:: uber_helpers
# Libraries:: node_utils
#
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
#
# Copyright:: (c) 2019-present, Uber Technologies, Inc.
# All rights reserved.
#
# This source code is licensed under the Apache 2.0 license found in the
# LICENSE file in the root directory of this source tree.
#
class Chef
class Node
def _config_profiles
@_config_profiles ||=
begin
if node.os_at_least?('10.13.0')
UberHelpers::MacUtils.get_installed_profiles
else
UberHelpers::MacUtils.get_installed_profiles_legacy
end
rescue Exception => e # rubocop:disable Lint/RescueException
Chef::Log.warn(
"Failed to retrieve installed profiles with error #{e}",
)
{}
end
end
## Looks for a team id within the profileidentifier specified
def _parse_kext_profile(profileid, kextid, profiles)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
if profiles.key?('_computerlevel')
profiles['_computerlevel'].each do |profile|
# Only check for kextid if the profile is installed.
return true if profile['ProfileIdentifier'] == profileid && profile.to_s.include?(kextid)
# Two methods to do here. Explicitly look at the array or convert
# to string.
# Array would be profile['ProfileItems'][0]['PayloadContent']\
# ['AllowedTeamIdentifiers'].include?(kextid)
# Opting for the string since it's less likely to fail.
end
end
false
end
## Looks for a team id within the profileidentifier specified
def _parse_sext_profile_removal(profileid, kextid, teamid, profiles)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
if profiles.key?('_computerlevel')
profiles['_computerlevel'].each do |profile|
if profile['ProfileIdentifier'] == profileid
removable_extensions = profile['ProfileItems'][0]['PayloadContent']['RemovableSystemExtensions']
unless removable_extensions&.nil?
removable_extensions.each do |key, value|
return true if key == teamid && value.to_s.include?(kextid)
end
end
end
end
end
false
end
## Will match any top level keys on a profile
# term is the term to match against
# key is what key to match (ProfileIdentifier, ProfileDisplayName, etc)
# profiles is a hash containing all the currently installed profiles
def _parse_profiles(type, value, profiles, mdm = nil)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
if profiles.key?('_computerlevel')
profiles['_computerlevel'].each do |profile|
profile_type = profile[type]
if mdm == 'ws1' && type == 'ProfileDisplayName'
profile_type = profile_type.split('/V_')[0]
end
return true if profile_type == value
end
end
false
end
## Looks for content specifically in the payload of the profile.
def _parse_profile_contents(profile_content, profile_identifier, profiles)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
if profiles.key?('_computerlevel')
profiles['_computerlevel'].each do |profile|
return true if profile['ProfileIdentifier'] == profile_identifier && profile.to_s.include?(profile_content)
end
end
false
end
def _parse_user_profiles(type, value, profiles, mdm = nil)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
if profiles.key?(node.console_user)
profiles[node.console_user].each do |profile|
profile_type = profile[type]
if mdm == 'ws1' && type == 'ProfileDisplayName'
profile_type = profile_type.split('/V_')[0]
end
return true if profile_type == value
end
end
false
end
def _user_config_profiles
@_user_config_profiles ||=
if node.os_at_least?('10.13.0')
UberHelpers::MacUtils.get_installed_user_profiles(node.console_user)
else
UberHelpers::MacUtils.get_installed_user_profiles_legacy(
node.console_user,
)
end
end
def _ws1_profile_version(display_name, profiles)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
# WS1 will never have a V_0 profile
profile_version = '0'
if profiles.key?('_computerlevel')
profiles['_computerlevel'].each do |profile|
if profile['ProfileDisplayName'].nil?
Chef::Log.warn("profile (#{profile['ProfileIdentifier']}) missing DisplayName key")
next
end
profile_contents = profile['ProfileDisplayName'].split('/V_')
return profile_contents[1] if profile_contents[0] == display_name
end
end
profile_version
end
def _ws1_user_profile_version(display_name, profiles)
fail 'profiles XML parsing cannot be nil!' if profiles.nil?
fail 'profiles XML parsing must be a Hash!' unless profiles.is_a?(Hash)
# WS1 will never have a V_0 profile
profile_version = '0'
if profiles.key?(node.console_user)
profiles[node.console_user].each do |profile|
if profile['ProfileDisplayName'].nil?
Chef::Log.warn("profile (#{profile['ProfileIdentifier']}) missing DisplayName key")
next
end
profile_contents = profile['ProfileDisplayName'].split('/V_')
return profile_contents[1] if profile_contents[0] == display_name
end
end
profile_version
end
def at_least?(version1, version2)
Gem::Version.new(version1) >= Gem::Version.new(version2)
end
def at_least_or_lower?(version1, version2)
Gem::Version.new(version1) <= Gem::Version.new(version2)
end
def bionic?
unless ubuntu?
Chef::Log.warn('node.bionic? called on non-ubuntu system')
return
end
return node['platform_version'].eql?('18.04')
end
def catalina?
unless macos?
Chef::Log.warn('node.catalina? called on non-OS X!')
return
end
return node.os_at_least?('10.15') && node.os_less_than?('10.16')
end
def big_sur?
unless macos?
Chef::Log.warn('node.big_sur? called on non-macOS!')
return
end
return node.os_at_least?('11.0') && node.os_less_than?('12.0') || \
(node.os_at_least?('10.16') && node.os_less_than?('10.17'))
end
def monterey?
unless macos?
Chef::Log.warn('node.monterey? called on non-macOS!')
return
end
return node.os_at_least?('12.0') && node.os_less_than?('13.0') || \
(node.os_at_least?('10.17') && node.os_less_than?('10.18'))
end
def ventura?
unless macos?
Chef::Log.warn('node.ventura? called on non-macOS!')
return
end
return node.os_at_least?('13.0') && node.os_less_than?('14.0') || \
(node.os_at_least?('10.18') && node.os_less_than?('10.19'))
end
def date_at_least?(date)
Date.today >= Date.parse(date)
end
def date_passed?(date)
Date.today > Date.parse(date)
end
def delete_file(path_of_file)
::File.delete(path_of_file) if ::File.exist?(path_of_file)
end
def el_capitan?
unless macos?
Chef::Log.warn('node.el_capitan? called on non-OS X!')
return
end
return node.os_at_least?('10.11') && node.os_less_than?('10.12')
end
def file_age_over_24_hours?(path_of_file)
file_age_over?(path_of_file, 86400)
end
def file_age_over?(path_of_file, seconds)
age_length = false
if path_of_file.nil?
Chef::Log.warn('node.file_age_over - cannot determine path')
return age_length
elsif ::File.exist?(path_of_file)
file_modified_time = File.mtime(path_of_file).to_i
diff_time = Time.now.to_i - file_modified_time
age_length = diff_time > seconds
end
age_length
end
def greater_than?(version1, version2)
Gem::Version.new(version1) > Gem::Version.new(version2)
end
def high_sierra?
unless macos?
Chef::Log.warn('node.high_sierra? called on non-OS X!')
return
end
return node.os_at_least?('10.13') && node.os_less_than?('10.14')
end
def kext_profile_contains_teamid?(kextid, profileid)
unless macos?
Chef::Log.warn('node.kext_profile_contains_teamid called on non-macOS!')
end
return false if profileid.nil?
node._parse_kext_profile(profileid, kextid, _config_profiles)
end
def sext_profile_removal_contains_extension?(sextid, teamid, profileid)
unless macos?
Chef::Log.warn('node.sext_profile_removal_contains_extension called on non-macOS!')
end
return false if profileid.nil?
node._parse_sext_profile_removal(profileid, sextid, teamid, _config_profiles)
end
def less_than?(version1, version2)
Gem::Version.new(version1) < Gem::Version.new(version2)
end
def logged_in_user
unless debian_family?
Chef::Log.warn('node.logged_in_user called on non-Debian!')
return
end
Etc.getlogin
end
def logged_on_user_profile
unless windows?
Chef::Log.warn('node.logged_on_user_profile called on non-Windows!')
return
end
ps_cmd = <<~PSCRIPT
$userProfile = Get-WmiObject -Class "Win32_UserProfile" -Filter "Special = 'False' and LastUseTime != NULL" |
Sort-Object -Property LastUseTime | Select-Object -Last 1 | Select-Object -Property LocalPath, SID
$user = $userProfile.LocalPath.substring(9)
$hash = @{LastLoggedOnUser = "CORP\\$user"; LastLoggedOnUserSID = $userProfile.SID} | ConvertTo-Json
return $hash
PSCRIPT
cmd = node.powershell_out(ps_cmd).stdout.to_str.chomp!
Chef::JSONCompat.parse(cmd)
end
def logged_on_user_registry
unless windows?
Chef::Log.warn('node.logged_on_user_registry called on non-Windows!')
return
end
require 'win32/registry'
logon_reg_key = \
'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Authentication\\LogonUI'
u = ::Win32::Registry::HKEY_LOCAL_MACHINE.open(
logon_reg_key, ::Win32::Registry::KEY_READ
) do |reg|
reg.to_a.each_with_object({}).each { |(a, _, c), obj| obj[a] = c }
end
return u.select! { |k, _| k =~ /user/i } unless node.vdi?
return logged_on_user_profile
end
def macos_application_version(apppath, key)
unless macos?
Chef::Log.warn('node.macos_application_version called on non-OS X!')
return ''
end
if ::File.exist?(apppath)
res = Plist.parse_xml(apppath)
res[key].to_s
else
return ''
end
rescue NoMethodError => e
Chef::Log.warn("#{e} on version lookup")
return ''
end
def macos_system_cert_installed?(cert_name)
unless macos?
Chef::Log.warn('node.macos_cert_installed? called on non-OS X!')
return false
end
shell_out(
"/usr/bin/security find-certificate -c \"#{cert_name}\" -Z /Library/Keychains/System.keychain",
).exitstatus.zero?
end
def macos_system_cert_hash?(cert_name)
unless macos?
Chef::Log.warn('node.macos_cert_hash? called on non-OS X!')
return ''
end
shell_out(
"/usr/bin/security find-certificate -c \"#{cert_name}\" -Z /Library/Keychains/System.keychain",
).run_command.stdout.to_s[/SHA-256 hash: (.*)/, 1]
end
def macos_package_installed?(pkg_identifier, pkg_version)
unless macos?
Chef::Log.warn('node.macos_package_installed? called on non-OS X!')
false
end
installed_pkg_version = shell_out(
"/usr/sbin/pkgutil --pkg-info \"#{pkg_identifier}\"",
).run_command.stdout.to_s[/version: (.*)/, 1]
# Compare the installed version to the maximum version
if installed_pkg_version.nil?
Chef::Log.warn("Package #{pkg_identifier} returned nil.")
false
end
Gem::Version.new(installed_pkg_version) == Gem::Version.new(pkg_version)
end
def macos_min_package_installed?(pkg_identifier, pkg_version)
unless macos?
Chef::Log.warn('node.macos_min_package_installed? called on non-OS X!')
false
end
installed_pkg_version = shell_out(
"/usr/sbin/pkgutil --pkg-info \"#{pkg_identifier}\"",
).run_command.stdout.to_s[/version: (.*)/, 1]
# Compare the installed version to the maximum version
if installed_pkg_version.nil?
Chef::Log.warn("Package #{pkg_identifier} returned nil.")
false
end
Gem::Version.new(installed_pkg_version) >= Gem::Version.new(pkg_version)
end
def macos_package_present?(pkg_identifier)
unless macos?
Chef::Log.warn('node.macos_package_present? called on non-OS X!')
return false
end
installed_pkg_version = shell_out(
"/usr/sbin/pkgutil --pkg-info \"#{pkg_identifier}\"",
).run_command.stdout.to_s[/version: (.*)/, 1]
if installed_pkg_version.nil?
Chef::Log.warn("Package #{pkg_identifier} returned nil.")
return false
end
true
end
def mojave?
unless macos?
Chef::Log.warn('node.mojave? called on non-OS X!')
return
end
return node.os_at_least?('10.14') && node.os_less_than?('10.15')
end
def not_eql?(version1, version2)
Gem::Version.new(version1) != Gem::Version.new(version2)
end
def parse_json(path)
Chef::JSONCompat.parse(::File.read(path))
end
def profile_contains_content?(profile_content, profile_identifier)
unless macos?
Chef::Log.warn('node.profile_contains_content called on non-macOS!')
return
end
_parse_profile_contents(profile_content, profile_identifier,
_config_profiles)
end
def profile_installed?(type, value, mdm = nil)
unless macos?
Chef::Log.warn('node.profile_installed called on non-macOS!')
return
end
_parse_profiles(type, value, _config_profiles, mdm)
end
def sierra?
unless macos?
Chef::Log.warn('node.sierra? called on non-OS X!')
return
end
return node.os_at_least?('10.12') && node.os_less_than?('10.13')
end
def user_profile_installed?(type, value, mdm = nil)
unless macos?
Chef::Log.warn('node.user_profile_installed called on non-macOS!')
return
end
_parse_user_profiles(type, value, _user_config_profiles, mdm)
end
def win_min_package_installed?(pkg_identifier, min_pkg)
unless windows?
false
end
installed_pkg_version = UberHelpers::WinUtils.win_pkg_ver(pkg_identifier)
# Compare the installed version to the minimum version
false if installed_pkg_version.nil?
Gem::Version.new(installed_pkg_version) >= Gem::Version.new(min_pkg)
end
def win_max_package_installed?(pkg_identifier, max_pkg)
unless windows?
Chef::Log.warn(
'node.win_max_package_installed? called on non-windows! system',
)
false
end
installed_pkg_version = UberHelpers::WinUtils.win_pkg_ver(pkg_identifier)
# Compare the installed version to the minimum version
if installed_pkg_version.nil?
Chef::Log.warn("Package #{pkg_identifier} returned nil.")
false
end
Gem::Version.new(installed_pkg_version) <= Gem::Version.new(max_pkg)
end
def debian_min_package_installed?(pkg_identifier, pkg_version)
unless debian_family?
Chef::Log.warn('node.debian_package_installed? called on non-Debian system!')
false
end
installed_pkg_version = shell_out(
"dpkg -s \"#{pkg_identifier}\"",
).run_command.stdout.to_s[/Version: (.*)/, 1]
# Compare the installed version to the maximum version
if installed_pkg_version.nil?
Chef::Log.warn("Package #{pkg_identifier} returned nil.")
false
end
Gem::Version.new(installed_pkg_version) >= Gem::Version.new(pkg_version)
rescue StandardError
return false
end
def write_contents_to_file(path, contents)
File.open(path, 'w') { |target_file| target_file.write(contents) }
end
def ws1_min_profile_installed?(display_name, version)
unless macos?
Chef::Log.warn('node.ws1_min_profile_installed called on non-macOS!')
return
end
installed_version = _ws1_profile_version(display_name, _config_profiles)
if installed_version == '0'
Chef::Log.warn("node.ws1_min_profile_installed did not find profile (#{display_name}) installed!")
return false
end
if installed_version.nil?
Chef::Log.warn("node.ws1_min_profile_installed profile (#{display_name}) compared does not have a version. "\
'Is this actually a ws1 profile?')
return false
end
Gem::Version.new(installed_version) >= Gem::Version.new(version)
end
def ws1_min_user_profile_installed?(display_name, version)
unless macos?
Chef::Log.warn('node.ws1_user_min_profile_installed called on non-macOS!')
return
end
installed_version = _ws1_user_profile_version(display_name, _user_config_profiles)
if installed_version == '0'
Chef::Log.warn("node.ws1_min_user_profile_installed did not find profile (#{display_name}) installed!")
return false
end
if installed_version.nil?
Chef::Log.warn("node.ws1_min_user_profile_installed profile (#{display_name}) compared does not have a "\
'version. Is this actually a ws1 profile?')
return false
end
Gem::Version.new(installed_version) >= Gem::Version.new(version)
end
def yosemite?
unless macos?
Chef::Log.warn('node.yosemite? called on non-OS X!')
return
end
return node.os_at_least?('10.10') && node.os_less_than?('10.11')
end
def cros?
unless debian_family?
Chef::Log.warn('node.cros? called on non debian!')
return
end
return false unless ::File.exists?('/sys/devices/virtual/dmi/id/bios_vendor')
return ::File.foreach('/sys/devices/virtual/dmi/id/bios_vendor').grep(/crosvm/i).any?
end
def chef_version
node['chef_packages']['chef']['version']
end
def at_least_chef12?
at_least?(chef_version, '12.0.0')
end
def at_least_chef13?
at_least?(chef_version, '13.0.0')
end
def at_least_chef14?
at_least?(chef_version, '14.0.0')
end
def at_least_chef15?
at_least?(chef_version, '15.0.0')
end
def at_least_chef16?
at_least?(chef_version, '16.0.0')
end
def at_least_chef17?
at_least?(chef_version, '17.0.0')
end
def powershell_package_provider?(pkg_identifier)
status = false
unless windows?
Chef::Log.warn('node.powershell_package_provider? called on non-windows device!')
return false
end
require 'chef/mixin/powershell_out'
powershell_cmd = '(Get-PackageProvider -WarningAction SilentlyContinue).Name | ConvertTo-Json'
cmd = powershell_out(powershell_cmd).stdout.to_s
if cmd.nil? || cmd.empty?
return status
else
status = Chef::JSONCompat.parse(cmd).include?(pkg_identifier)
end
status
end
def powershell_module?(pkg_identifier)
status = false
unless windows?
Chef::Log.warn('node.powershell_module_installed? called on non-windows device!')
return false
end
require 'chef/mixin/powershell_out'
powershell_cmd = "(Get-InstalledModule -Name \"#{pkg_identifier}\").Name -eq \"#{pkg_identifier}\" | "\
'ConvertTo-Json'
cmd = powershell_out(powershell_cmd).stdout.chomp.strip
if cmd.nil? || cmd.empty?
return status
else
status = Chef::JSONCompat.parse(cmd)
end
status
end
def dell_hw?
unless windows?
Chef::Log.warn('node.dell_hw? called on non-windows device!')
return false
end
require 'chef/mixin/powershell_out'
powershell_cmd = '(Get-CimInstance -ClassName Win32_ComputerSystem).Manufacturer'
cmd = powershell_out(powershell_cmd).stdout.to_s
if cmd.include?('Dell')
return true
else
return false
end
end
def connection_reachable?(destination)
unless macos? || windows? || debian_family?
Chef::Log.warn('node.connection_reachable? called on non-macOS/windows/ubuntu device!')
return false
end
status = false
if macos?
cmd = shell_out("/sbin/ping #{destination} -c 1")
elsif debian_family?
cmd = shell_out("/bin/ping #{destination} -c 2")
elsif windows?
powershell_cmd = "Test-Connection #{destination} -Count 1 -Quiet"
cmd = powershell_out(powershell_cmd)
end
if cmd.stdout.nil? || cmd.stdout.empty?
return status
elsif macos? || debian_family?
# If connected, will return 0, timeout is 68.
status = cmd.exitstatus.zero?
elsif windows?
# Powershell returns a string of True/False, which ruby can't natively handle, so we downcase everything and use
# JSON library to convert it to a BOOL.
status = Chef::JSONCompat.parse(cmd.stdout.chomp.downcase)
end
status
end
def macos_os_sub_version
@macos_os_sub_version ||=
begin
unless macos?
Chef::Log.warn('node.macos_os_sub_version called on non-OS X!')
return '0'
end
cmd = shell_out('/usr/sbin/sysctl -n kern.osversion').run_command.stdout
if cmd.nil?
Chef::Log.warn('node.macos_os_sub_version returned nil')
return '0'
end
cmd.strip
end
end
def orbit_token
unless macos?
return nil
end
orbit_token_path = '/opt/orbit/identifier'
if ::File.exists?(orbit_token_path)
return ::File.read(orbit_token_path)
else
return nil
end
end
def macos_mutate_version(version)
# This is stupid, but it works from 10.7 and higher (to date), so this is
# _likely_ safe.
# Split the version first - 19F101 becomes ["19", "F", "101"]
# Reject blank values: 19F101FF would otherwise be ["19", "F", "101", "F", "", "F"]
split_version = version.split(/([a-z]|[A-Z])/).reject(&:empty?)
# Join the list with dots and replace letters with numbers
# ["19", "F", "101"] => 19.F.101 => 19.5.101
split_version.join('.').tr('ABCDEFGHIJ', '0123456789')
end
def mac_os_sub_version_at_least?(version)
Gem::Version.new(macos_mutate_version(macos_os_sub_version)) >= Gem::Version.new(macos_mutate_version(version))
rescue ArgumentError
Chef::Log.warn('node.mac_os_sub_version_at_least? given a malformed version')
return false
end
def mac_os_sub_version_at_least_or_lower?(version)
Gem::Version.new(macos_mutate_version(macos_os_sub_version)) <= Gem::Version.new(macos_mutate_version(version))
rescue ArgumentError
Chef::Log.warn('node.mac_os_sub_version_at_least_or_lower? given a malformed version')
return false
end
def mac_os_sub_version_greater_than?(version)
Gem::Version.new(macos_mutate_version(macos_os_sub_version)) > Gem::Version.new(macos_mutate_version(version))
rescue ArgumentError
Chef::Log.warn('node.mac_os_sub_version_greater_than? given a malformed version')
return false
end
def mac_os_sub_version_less_than?(version)
Gem::Version.new(macos_mutate_version(macos_os_sub_version)) < Gem::Version.new(macos_mutate_version(version))
rescue ArgumentError
Chef::Log.warn('node.mac_os_sub_version_less_than? given a malformed version')
return false
end
def port_open?(destination, port, timeout = 1)
begin
socket = Socket.tcp(destination, port, :connect_timeout => timeout)
rescue Errno::ETIMEDOUT
Chef::Log.warn("node.port_open? #{destination} timed out")
return false
rescue SocketError
Chef::Log.warn("node.port_open? cannot resolve #{destination}")
return false
rescue Errno::ECONNREFUSED
Chef::Log.warn("node.port_open? #{destination} connection refused")
return false
rescue Errno::EHOSTUNREACH
Chef::Log.warn("node.port_open? #{destination} host unreachable")
return false
end
if socket
unless socket.closed?
socket.close
end
true
else
false
end
end
def distinguished_name?(ou_identifier)
status = false
unless windows? || macos?
Chef::Log.warn('node.distinguished_name? called on non-windows or macos device!')
return false
end
dn = node.machine['distinguishedName']
if dn.nil? || dn.empty?
return status
else
status = dn.include?(ou_identifier)
end
return status
end
# function returns an array of bools [ bool, bool ]
# first element is to indicate if the extension queried is enabled/disabled
# second element is to indicate if there was an error in the begin/rescue block
def network_extension_enabled(extension_identifier, type)
extension_enabled = false
unless macos?
Chef::Log.warn('node.network_extension_enabled? called on non-OS X!')
return false, true
end
extension_enabled = false
extension_error = false
gnes_path = '/usr/local/bin/gnes'
if ::File.exist?(gnes_path)
cmd = shell_out("#{gnes_path} -identifier #{extension_identifier} -type #{type} -stdout-json")
if cmd.exitstatus.zero?
begin
cmd_json = Chef::JSONCompat.parse(cmd.stdout.to_s)
extension_enabled = cmd_json.nil? ? false : cmd_json['enabled']
rescue Chef::Exceptions::JSON::ParseError, FFI_Yajl::ParseError
extension_error = true
Chef::Log.warn('node.network_extension_enabled threw an error')
end
else
extension_error = true
Chef::Log.warn('node.network_extension_enabled could not find extension with requested type')
end
else
Chef::Log.info('node.network_extension_enabled could not find gnes binary - reverting to legacy check')
unless node.at_least?(node.chef_version, '17.7.22')
Chef::Log.warn('node.network_extension_enabled? requires chef v17.7.22 and higher when using legacy check')
return false, true
end
# Everything is in a key of "$objects"
begin
network_extensions = CF::Preferences.get('$objects', 'com.apple.networkextension')
rescue TypeError
network_extensions = []
extension_error = true
Chef::Log.warn('node.network_extension_enabled threw an error')
end
# Apple uses an array of dictionaries but also puts a string or strings before some of
# the dictionaries to denote what tool it's configuration is, rather than use something sane like <key>.
# This condition grabs the current index, substracts one and compares it to the previous item in the array.
# It checks to see if the previous entry was the requested bundle ID and also that the value returned is not a
# string as Apple also has multiple string entries over and over in the array, which is not the data we need.
network_extensions.each_with_index do |value, index|
if network_extensions[index - 1] == extension_identifier && !value.instance_of?(String)
extension_enabled = value['Enabled']
break
end
end
end
return extension_enabled, extension_error
end
def system_extension_installed?(extension_identifier)
# examples: com.crowdstrike.falcon.Agent.systemextension, com.cisco.anyconnect.macos.acsockext.systemextension
system_extension_installed = false
unless node.at_least?(node.chef_version, '17.7.22')
Chef::Log.warn('node.system_extension_installed? requires chef v17.7.22 and higher!')
return system_extension_installed
end
unless macos?
Chef::Log.warn('node.system_extension_installed? called on non-OS X!')
return system_extension_installed
end
CF::Preferences.get('extensions', '/Library/SystemExtensions/db.plist').each do |k, _v|
relative_file_path = k['stagedBundleURL']['relative']
if relative_file_path&.include?(extension_identifier)
system_extension_installed = ::File.exists?(relative_file_path.split('file://')[1])
end
end
return system_extension_installed
end
# Return the Version Number as a String.
# nil value if the package is not installed.
def installed_pkg_version(pkg_identifier)
unless macos?
Chef::Log.warn('node.installed_pkg_version called on non-OS X!')
return nil
end
installed_pkg_version = shell_out(
"/usr/sbin/pkgutil --pkg-info \"#{pkg_identifier}\"",
).run_command.stdout.to_s[/version: (.*)/, 1]
Chef::Log.warn("Package #{pkg_identifier} returned nil.") if installed_pkg_version.nil?
installed_pkg_version
end
def macos_install_compat_check(file)
if ::File.exists?(file)
return shell_out("/usr/sbin/installer -volinfo -pkg #{file} -plist").stdout.include?('MountPoint')
else
Chef::Log.warn("#{file} does not exist.")
return false
end
end
def installed_pkg_major_version(pkg_identifier)
version = installed_pkg_version(pkg_identifier)
version.split('.')[0] unless version.nil?
end
def forget_pkg(receipt)
installed_pkg_version = installed_pkg_version(receipt)
if installed_pkg_version
shell_out("/usr/sbin/pkgutil --forget #{receipt}")
end
end
def forget_pkg_with_launchagent(receipt, launcha_path)
installed_pkg_version = installed_pkg_version(receipt)
if installed_pkg_version
shell_out("/usr/sbin/pkgutil --forget #{receipt}")
if ::File.exists?(launcha_path)
shell_out("/usr/bin/su -l #{node.console_user} -c "\
"'/bin/launchctl unload -w #{launcha_path}'", default_env: false) # rubocop:disable Style/HashSyntax
end
end
end
def forget_pkg_with_launchdaemon(receipt, launchd_path)
installed_pkg_version = installed_pkg_version(receipt)
if installed_pkg_version
shell_out("/usr/sbin/pkgutil --forget #{receipt}")
if ::File.exists?(launchd_path)
shell_out("/bin/launchctl unload -w #{launchd_path}", default_env: false) # rubocop:disable Style/HashSyntax
end
end
end
def chef_solo?
ChefConfig::Config.chef_server_url.include?('localhost')
end
def file_blocked?(target)
return unless windows?
ps = "(Get-Item #{target} -Stream \"Zone.Identifier\" -ErrorAction SilentlyContinue) -ne $null | ConvertTo-Json"
cmd = powershell_out(ps).stdout.to_s
return Chef::JSONCompat.parse(cmd)
end
def at_least_big_sur?
node.os_at_least?('11.0') || node.os_at_least?('10.16')
end
def bplist?(file_path)
if ::File.exists?(file_path)
shell_out("/usr/bin/file #{file_path}").run_command.stdout.include?('Apple binary property list')
end
end
def nslookup_txt_records(domain, timeout = 3)
results = {}
unless macos?
Chef::Log.warn('node.nslookup called on non-OS X!')
return nil
end
records = shell_out(
"/usr/bin/nslookup -type=txt #{domain} -timeout=#{timeout}",
).run_command.stdout.to_s.scan(/text = "(.*)"/).flatten
records.each do |line|
if domain == 'debug.opendns.com'
if line.include?('flags') || line.include?('dnscrypt')
split_line = line.split(' ', 2)
results[split_line[0]] = split_line[1]
else
split_line = line.rpartition(' ')
results[split_line.first] = split_line.last
end
else
split_line = line.rpartition(' ')
results[split_line.first] = split_line.last
end
end
results
end
def daemon_running?(daemon)
unless macos?
Chef::Log.warn('node.dameon_running? called on non-OS X!')
return nil
end
shell_out('/bin/launchctl list').run_command.stdout.to_s[/(.*)#{daemon}/].nil? ? false : true
end
def macos_boottime
unless macos?
Chef::Log.warn('node.macos_boottime called on non-OS X!')
return nil
end
shell_out('/usr/sbin/sysctl -n kern.boottime').run_command.stdout.to_s[/sec = (.*),/, 1].to_i
end
def macos_waketime
unless macos?
Chef::Log.warn('node.macos_boottime called on non-OS X!')
return nil
end
shell_out('/usr/sbin/sysctl -n kern.waketime').run_command.stdout.to_s[/sec = (.*),/, 1].to_i
end
def macos_kext_loaded?(bundle_identifier)
unless macos?
Chef::Log.warn('node.macos_kext_loaded? called on non-macOS!')
return false
end
shell_out(
"/usr/bin/kmutil showloaded --show loaded --filter \"\'CFBundleIdentifier\' == \'#{bundle_identifier}\'\" "\
'--variant-suffix release --list-only',
).run_command.stdout.to_s.include?(bundle_identifier)
end
def macos_process_uptime(process)
uptime = 0
unless macos?
Chef::Log.warn('node.macos_process_time called on non-OS X!')
return nil
end
time = shell_out('/bin/ps acxo etime,command').run_command.stdout.to_s[/(.*) #{process}/, 1]
unless time.nil?
safe_time = time.strip.split(':')
case safe_time.count
when 3
uptime = safe_time[0].to_i * 360 + safe_time[1].to_i * 60 + safe_time[2].to_i
when 2
uptime = safe_time[0].to_i * 60 + safe_time[1].to_i
end
end
uptime
end
def safe_nil_empty?(object)
object.nil? || object.empty?
end
def cpe_launchd_label(cpe_identifier)
# This portion is taken from cpe_launchd. Since we use cpe_launchd to
# create our launch agent, the label specified in the attributes will not
# match the actual label/path that's created. Doing this will result in
# the right file being targeted.
if cpe_identifier.start_with?('com')
name = cpe_identifier.split('.')
name.delete('com')
identifier = name.join('.')
identifier = "#{node['cpe_launchd']['prefix']}.#{identifier}"
end
identifier
end
def cpe_launchd_path(type, identifier)
label = cpe_launchd_label(identifier)
if type == 'agent'
::File.join('/Library/LaunchAgents', "#{label}.plist")
else
::File.join('/Library/LaunchDaemons', "#{label}.plist")
end
end
end
end