chef/cookbooks/cpe_osquery/resources/cpe_osquery.rb (461 lines of code) (raw):
# Cookbook:: cpe_osquery
#
# Resources:: cpe_osquery
#
# 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.
#
unified_mode true
resource_name :cpe_osquery
provides :cpe_osquery, :os => ['darwin', 'linux', 'windows']
default_action :manage
action :manage do
install if install?
manage if manage? && !uninstall?
uninstall if uninstall? && !install?
end
action_class do # rubocop:disable Metrics/BlockLength
def install?
node['cpe_osquery']['install']
end
def manage?
node['cpe_osquery']['manage']
end
def uninstall?
node['cpe_osquery']['uninstall']
end
def base_bin_path
node['cpe_osquery']['base_bin_path']
end
def official_pack_list
[
'hardware-monitoring',
'incident-response',
'it-compliance',
'osquery-monitoring',
'ossec-rootkit',
'osx-attacks',
'unwanted-chrome-extensions',
'vuln-management',
'windows-attacks',
'windows-hardening',
]
end
def install
debian_install if debian?
macos_install if macos?
windows_install if windows?
end
def debian_install
# Download installer package
download_package
# Install the package
dpkg_package pkg_filename do
source pkg_filepath
version osquery_pkg['dpkg_version']
action :install
end
end
def macos_osquery_file_integrity_healthy?
healthy = true
if Gem::Version.new(pkg_version) >= Gem::Version.new('5.0.1')
files_to_check = %w[
/opt/osquery/lib/osquery.app/Contents/MacOS/osqueryd
/opt/osquery/lib/osquery.app/Contents/Resources/osqueryctl
]
elsif Gem::Version.new(pkg_version) == Gem::Version.new('4.9.0.1')
files_to_check = %w[
/opt/osquery/osqueryctl
/opt/osquery/osqueryd
]
else
files_to_check = %w[
/usr/local/bin/osqueryctl
/usr/local/bin/osqueryd
]
end
files_to_check.each do |cs_file|
unless ::File.exists?(cs_file)
healthy = false
end
end
healthy
end
def macos_install
ld_label = 'com.facebook.osqueryd'
receipt = osquery_pkg['receipt']
# Force a re-install of osquery if files are missing
execute "/usr/sbin/pkgutil --forget #{receipt}" do
not_if { macos_osquery_file_integrity_healthy? }
not_if { shell_out("/usr/sbin/pkgutil --pkg-info #{receipt}").error? }
notifies :disable, "launchd[#{ld_label}]", :immediately
end
cpe_remote_pkg 'osquery' do
app pkg_name
version pkg_version
checksum pkg_checksum
receipt receipt
if ::File.exists?("/Library/LaunchDaemons/#{ld_label}.plist")
notifies :restart, "launchd[#{ld_label}]", :immediately
end
end
# Make sure logs get rotated
syslog_conf = 'com.facebook.osqueryd.conf'
syslog_conf_path = ::File.join('/etc/newsyslog.d', syslog_conf)
cookbook_file syslog_conf_path do
source syslog_conf
end
# Triger launchd restart
launchd ld_label do
action :nothing
end
end
def windows_install
download_package
# Install the MSI
windows_package "Install #{pkg_name}" do
source pkg_filepath
options '/norestart /passive /qn'
checksum pkg_checksum
end
end
def service_info
# Set service/daemon information so we can trigger restarts cross platform
value_for_platform_family(
'mac_os_x' => { 'launchd' => 'com.facebook.osqueryd' },
'debian' => { 'service' => 'osqueryd.service' },
'windows' => { 'service' => 'osqueryd' },
'default' => nil,
)
end
def manage
## TODO: - Manage configs on disk as well as flag files.
## Not everyone will have a TLS server for query scheduling
# file paths
# yara
# prometheus_targets
# views
# decorators
# Make sure directory exists and permissions are correct
directory osquery_dir do
if macos? || debian?
owner root_owner
group node['root_group']
mode '0700'
end
end
service_type, service_name = service_info.first
# We need to make this mutable to inject values into it for extensions
options = node['cpe_osquery']['options'].to_hash
ext_file = ::File.join(osquery_dir, 'extensions.load')
extensions = node['cpe_osquery']['extensions']
extension_paths = []
unless extensions.empty?
options['extensions_autoload'] = ext_file
directory osquery_ext_dir do
recursive true
unless windows?
mode '0755'
owner root_owner
group node['root_group']
end
end
extensions.each do |name, values|
ext_extension = windows? ? 'exe' : 'ext'
ext_path = ::File.join(osquery_ext_dir, "#{name}.#{ext_extension}")
extension_paths << ext_path
cpe_remote_file "#{name}-#{values['version']}" do
file_name "#{name}-#{values['version']}"
folder_name "osquery/extensions/#{node['platform_family']}"
checksum values['checksum']
path ext_path
unless windows?
mode '0755'
owner root_owner
group node['root_group']
end
notifies :restart, "#{service_type}[#{service_name}]"
end
end
template ext_file do
source 'extensions.load.erb'
variables(
'extensions' => extension_paths,
)
notifies :restart, "#{service_type}[#{service_name}]"
end
end
# Lay down config via osquery flags file
flag_file = ::File.join(osquery_dir, 'osquery.flags')
template flag_file do
source 'osquery.flags.erb'
variables(
'options' => options,
)
not_if { options.nil? }
notifies :restart, "#{service_type}[#{service_name}]"
end
# We need to make this mutable to inject values into it for query packs
conf = node['cpe_osquery']['conf'].to_hash
packs = node['cpe_osquery']['packs']
packs_dir = ::File.join(osquery_dir, 'packs')
managed_packs = []
unless packs.empty?
# Create initial packs directory
directory packs_dir do
if macos? || debian?
owner root_owner
group node['root_group']
mode '0755'
end
end
# Inject an empty hash into conf so in the for loop below we can inject the name/paths
conf['packs'] = {}
# Loop through the packs, lay them down and add to the conf file
packs.each do |name, values|
pack_path = ::File.join(packs_dir, "#{name}.conf")
conf['packs'][name] = pack_path
managed_packs.push(pack_path)
file pack_path do
if macos? || debian?
owner root_owner
group node['root_group']
mode '0644'
end
content Chef::JSONCompat.to_json_pretty(values)
notifies :restart, "#{service_type}[#{service_name}]"
end
end
end
# Support the official packs that come from osquery pkg
official_packs_to_install = node['cpe_osquery']['official_packs_install_list']
if node['cpe_osquery']['manage_official_packs'] && !official_packs_to_install.empty?
official_pack_list.each do |name|
if official_packs_to_install.include?(name)
pack_path = ::File.join(packs_dir, "#{name}.conf")
conf['packs'][name] = pack_path
managed_packs.push(pack_path)
cookbook_file pack_path do
source "packs/#{name}.conf"
if macos? || debian?
owner root_owner
group node['root_group']
mode '0644'
end
notifies :restart, "#{service_type}[#{service_name}]"
end
end
end
end
conf_path = ::File.join(osquery_dir, 'osquery.conf')
# Cleanup the packs before updating the conf file
cleanup_packs(managed_packs, conf_path)
# Sort all hash sub keys. This is so something like "select config_hash from osquery_info;"
# can return consistant hash results across devices
sortedconf = {}
conf.each do |k, v|
if v.is_a?(Hash)
sortedconf[k] = v.sort.to_h
else
sortedconf[k] = v
end
end
# Lay down conf file
unless sortedconf.empty?
file conf_path do
if macos? || debian?
owner root_owner
group node['root_group']
mode '0700'
end
content Chef::JSONCompat.to_json_pretty(sortedconf.sort.to_h)
notifies :restart, "#{service_type}[#{service_name}]"
end
end
if windows?
service 'osqueryd' do
action :nothing
end
end
debian_manage_service if debian?
macos_manage_service(flag_file) if macos?
windows_manage_service if windows?
end
def debian_manage_service
# Ensure osqueryd is running
service_type, service_name = service_info.first
template '/etc/default/osqueryd' do
source 'osqueryd.erb'
notifies :restart, "#{service_type}[#{service_name}]"
end
template '/usr/lib/systemd/system/osqueryd.service' do
source 'osqueryd.service.systemd.erb'
notifies :restart, "#{service_type}[#{service_name}]"
end
service service_name do
action :start
end
end
def macos_manage_service(flag_file)
launchd 'com.facebook.osqueryd' do
program_arguments [
::File.join(base_bin_path, 'osqueryd'),
'--flagfile',
flag_file,
]
environment_variables({ 'SYSTEM_VERSION_COMPAT' => '0' }) unless node.os_at_least_or_lower?('10.15.99')
keep_alive true
run_at_load true
throttle_interval 60
action :enable
end
end
def windows_manage_service
service 'enforce osqueryd service' do
action :start
not_if { osquery_service_status&.include?('Running') }
service_name 'osqueryd' # Use the service name to avoid namespace collision
end
end
def osquery_service_status
return nil unless windows?
status = powershell_out('(Get-Service osqueryd).status').stdout.to_s.chomp
status.empty? ? nil : status
end
def uninstall
debian_uninstall if debian?
macos_uninstall if macos?
windows_uninstall if windows?
end
def debian_uninstall
# Ensure osqueryd is stopped
service service_info.first[1] do
action :stop
end
# Purge all traces of the package
dpkg_package 'osquery' do
action :purge
end
# Clean up osquery directories the purge doesn't handle
%w[
/etc/osquery
/opt/osquery
/usr/lib/osquery/
/usr/share/osquery
/var/osquery
/var/log/osquery
].each do |osquery_directory|
directory osquery_directory do
recursive true
action :delete
end
end
# Clean up osquery files the purge may not always clean up
%w[
/etc/default/osqueryd
/usr/lib/systemd/system/osqueryd.service
].each do |osquery_file|
file osquery_file do
action :delete
end
end
end
def macos_uninstall
# Clean up the launch daemon
launchd 'com.facebook.osqueryd' do
action :delete
end
# Clean up osquery files
[
::File.join(base_bin_path, 'osqueryctl'),
::File.join(base_bin_path, 'osqueryd'),
'/etc/newsyslog.d/com.facebook.osqueryd.conf',
].each do |osquery_file|
file osquery_file do
action :delete
end
end
# osqueryi is a sometimes a link and sometimes a file
osqueryi = ::File.join(base_bin_path, 'osqueryi')
if ::File.symlink?(osqueryi)
link osqueryi do
action :delete
end
else
file osqueryi do
action :delete
end
end
# Clean up osquery directories
%w[
/opt/osquery
/var/osquery
/var/log/osquery
].each do |osquery_dir|
directory osquery_dir do
recursive true
action :delete
end
end
execute '/usr/sbin/pkgutil --forget com.facebook.osquery' do
not_if { shell_out('/usr/sbin/pkgutil --pkg-info com.facebook.osquery').error? }
end
end
def windows_uninstall
# Only download if we need to uninstall
download_package if node['packages'].key?(pkg_name)
# Invoke MSI uninstall
windows_package "uninstall #{pkg_name}" do
source pkg_filepath
checksum pkg_checksum
action :remove
options '/qn /norestart'
only_if { node['packages'].key?(pkg_name) && ::File.exists?(pkg_filepath) }
end
# Run custom script to force cleanup of remaining files
powershell_script 'uninstall osquery - force cleanup' do
code <<-PSCRIPT
$serviceName = 'osqueryd'
$serviceDescription = 'osquery daemon service'
$progFiles = [System.Environment]::GetEnvironmentVariable('ProgramFiles')
$targetFolder = Join-Path $progFiles 'osquery'
# Remove the osquery path from the System PATH variable. Note: Here
# we don't make use of our local vars, as Regex requires escaping the '\'
$oldPath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine')
if ($oldPath -imatch [regex]::escape($targetFolder)) {
$newPath = $oldPath -replace [regex]::escape($targetFolder), $NULL
[System.Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine')
}
if ((Get-Service $serviceName -ErrorAction SilentlyContinue)) {
Stop-Service $serviceName
# If we find zombie processes, ensure they're termintated
$proc = Get-Process | Where-Object { $_.ProcessName -eq 'osqueryd' }
if ($null -ne $proc) {
Stop-Process -Force $proc -ErrorAction SilentlyContinue
}
Set-Service $serviceName -startuptype 'manual'
Get-CimInstance -ClassName Win32_Service -Filter "Name='osqueryd'" | Invoke-CimMethod -methodName Delete
}
if (Test-Path $targetFolder) {
Remove-Item -Force -Recurse $targetFolder
} else {
Write-Debug 'osquery was not found on the system. Nothing to do.'
}
PSCRIPT
only_if { ::File.directory?(::File.join(ENV['ProgramFiles'], 'osquery')) }
end
end
def cleanup_packs(managed_packs, conf_path)
# Parse the osquery conf and see which packs are managed
if ::File.exists?(conf_path)
configured_packs = Chef::JSONCompat.parse(::File.read(conf_path))['packs']
else
Chef::Log.warn('cpe_osquery cannot find conf file')
return
end
# Loop through the configured packs
configured_packs.each_value do |value|
# If file is not in our new list of items to manage, we need to delete it
unless managed_packs.include?(value)
file value do
action :delete
end
end
end
# Loop through official packs and remove ones not being managed via chef
official_pack_list.each do |pack|
pack_path = ::File.join(osquery_dir, 'packs', "#{pack}.conf")
unless managed_packs.include?(pack_path)
file pack_path do
action :delete
end
end
end
end
def osquery_pkg
node['cpe_osquery']['pkg'].to_hash
end
def pkg_version
osquery_pkg['version']
end
def pkg_name
osquery_pkg['name']
end
def pkg_checksum
osquery_pkg['checksum']
end
def osquery_dir
node['cpe_osquery']['osquery_dir']
end
def osquery_ext_dir
node['cpe_osquery']['osquery_ext_dir']
end
def pkg_filename
filetype = value_for_platform_family(
'windows' => 'msi',
'debian' => 'deb',
'mac_os_x' => 'pkg',
'default' => nil,
)
"#{pkg_name}-#{pkg_version}.#{filetype}"
end
def pkg_filepath
::File.join(Chef::Config[:file_cache_path], pkg_filename)
end
def download_package
cpe_remote_file pkg_name do
backup 1
file_name pkg_filename
checksum pkg_checksum
path pkg_filepath
end
end
end