From a56e90cd68da2580f675d78eccbab856dfc19605 Mon Sep 17 00:00:00 2001 From: ttyson Date: Wed, 16 Aug 2023 06:56:20 -0700 Subject: [PATCH 1/2] Syncing from Internal Changes --- chef/cookbooks/cpe_anyconnect/README.md | 11 - .../cpe_anyconnect/attributes/default.rb | 6 +- chef/cookbooks/cpe_anyconnect/metadata.rb | 0 .../cpe_anyconnect/recipes/default.rb | 0 .../resources/cpe_anyconnect.rb | 194 +++++++++--------- 5 files changed, 102 insertions(+), 109 deletions(-) mode change 100644 => 100755 chef/cookbooks/cpe_anyconnect/metadata.rb mode change 100644 => 100755 chef/cookbooks/cpe_anyconnect/recipes/default.rb diff --git a/chef/cookbooks/cpe_anyconnect/README.md b/chef/cookbooks/cpe_anyconnect/README.md index f8d0d59..f417b7a 100644 --- a/chef/cookbooks/cpe_anyconnect/README.md +++ b/chef/cookbooks/cpe_anyconnect/README.md @@ -27,8 +27,6 @@ Attributes * node['cpe_anyconnect']['modules']['version'] * node['cpe_anyconnect']['modules']['checksum'] * node['cpe_anyconnect']['modules']['install_args'] -* node['cpe_anyconnect']['backup_logs'] -* node['cpe_anyconnect']['nslookup_failure_count_threshold'] * node['cpe_anyconnect']['organization_id'] * node['cpe_anyconnect']['pkg'] * node['cpe_anyconnect']['pkg']['allow_downgrade'] @@ -82,12 +80,3 @@ node.default['cpe_anyconnect']['modules'] = [ }, ] node.default['cpe_anyconnect']['desktop_shortcut'] = true -``` -An example of setting the nslookup_failure_count_threshold other than default (macOS only) -``` -node.default['cpe_anyconnect']['nslookup_failure_count_threshold'] = 5 -``` -An example of setting the attribute backup_logs to trigger a log backup before directory deletion (macOS only) -``` -node.default['cpe_anyconnect']['backup_logs'] = true -``` diff --git a/chef/cookbooks/cpe_anyconnect/attributes/default.rb b/chef/cookbooks/cpe_anyconnect/attributes/default.rb index 823857e..77cef00 100644 --- a/chef/cookbooks/cpe_anyconnect/attributes/default.rb +++ b/chef/cookbooks/cpe_anyconnect/attributes/default.rb @@ -25,8 +25,6 @@ 'version' => nil, }, ], - 'backup_logs' => false, - 'nslookup_failure_count_threshold' => 20, 'organization_id' => nil, 'pkg' => { 'allow_downgrade' => false, @@ -46,4 +44,8 @@ '/Applications/Cisco/Cisco AnyConnect Secure Mobility Client.app' default['cpe_anyconnect']['pkg']['dart_receipt'] = 'com.cisco.pkg.anyconnect.dart' default['cpe_anyconnect']['pkg']['receipt'] = 'com.cisco.pkg.anyconnect.vpn' + default['cpe_anyconnect']['pkg']['umbrella_receipt'] = 'com.cisco.pkg.anyconnect.umbrella' + default['cpe_anyconnect']['cookbook']['cookbook_name'] = nil + default['cpe_anyconnect']['cookbook']['template_source'] = nil + default['cpe_anyconnect']['final_xml_path'] = nil end diff --git a/chef/cookbooks/cpe_anyconnect/metadata.rb b/chef/cookbooks/cpe_anyconnect/metadata.rb old mode 100644 new mode 100755 diff --git a/chef/cookbooks/cpe_anyconnect/recipes/default.rb b/chef/cookbooks/cpe_anyconnect/recipes/default.rb old mode 100644 new mode 100755 diff --git a/chef/cookbooks/cpe_anyconnect/resources/cpe_anyconnect.rb b/chef/cookbooks/cpe_anyconnect/resources/cpe_anyconnect.rb index df36425..496b135 100644 --- a/chef/cookbooks/cpe_anyconnect/resources/cpe_anyconnect.rb +++ b/chef/cookbooks/cpe_anyconnect/resources/cpe_anyconnect.rb @@ -56,31 +56,44 @@ def macos_install # Download the anyconnect pkg download_package(pkg) - # Install the pacakge with the ChoiceChangesXML + # Install the package with the ChoiceChangesXML cc_xml_path = ::File.join(anyconnect_root_cache_path, 'pkg', 'ChoiceChanges.xml') allow_downgrade = pkg['allow_downgrade'] if allow_downgrade - if node.os_at_least?('12.0') && node.sext_profile_removal_contains_extension?( + if node.big_sur? + Chef::Log.warn('cpe_anyconnect - Big Sur has a bug that triggers UI popups that the user must accept if '\ + 'attempting to downgrade - you must manually uninstall the application.') + Chef::Log.warn('cpe_anyconnect - forcing downgrade to false') + allow_downgrade = false + elsif (node.os_at_least?('12.0') && node.sext_profile_removal_contains_extension?( 'com.cisco.anyconnect.macos.acsockext', 'DE8Y96K9QP', node['cpe_anyconnect']['profile_identifier'] - ) + )) || node.os_less_than?('11.0') execute '/opt/cisco/anyconnect/bin/anyconnect_uninstall.sh' do - not_if { node.macos_package_installed?(pkg['receipt'], pkg['version']) } + not_if { node.macos_installed_package_lower?(pkg['receipt'], pkg['version']) } not_if { anyconnect_vpn_connected? } only_if { ::File.exist?('/opt/cisco/anyconnect/bin/anyconnect_uninstall.sh') } end execute '/opt/cisco/anyconnect/bin/dart_uninstall.sh' do - not_if { node.macos_package_installed?(pkg['dart_receipt'], pkg['version']) } + not_if { node.macos_installed_package_lower?(pkg['dart_receipt'], pkg['version']) } not_if { anyconnect_vpn_connected? } only_if { ::File.exist?('/opt/cisco/anyconnect/bin/dart_uninstall.sh') } end else Chef::Log.warn('cpe_anyconnect - AnyConnect package has logic to fail if attempting to downgrade - you must '\ - 'manually uninstall the application first if you are not passing a system extension profile!') + 'manually uninstall the application.') Chef::Log.warn('cpe_anyconnect - forcing downgrade to false') allow_downgrade = false end end + daemon_path = '/Library/LaunchDaemons/com.cisco.anyconnect.vpnagentd.plist' + execute 'Load Cisco Daemon' do + command "/bin/launchctl load -w #{daemon_path}" + only_if { ::File.exist?(daemon_path) } + action :nothing + end + # anyconnect's postinstall copies the xml file that defines which portals + # are available to users into place execute "/usr/sbin/installer -applyChoiceChangesXML #{cc_xml_path} -pkg #{pkg_path(pkg)} -target /" do # functionally equivalent to allow_downgrade false on cpe_remote_pkg if allow_downgrade @@ -89,14 +102,15 @@ def macos_install not_if { node.macos_min_package_installed?(pkg['receipt'], pkg['version']) } end not_if { anyconnect_vpn_connected? } - notifies :create, 'file[trigger_gui]', :immediately + notifies :run, 'execute[Load Cisco Daemon]', :before + notifies :create, 'file[trigger_gui_install]', :immediately end # We only want the UI to trigger upon the first install and upgrades. # In testing, their own postinstall script logic is very unreliable. # Touch the gui_keepalive path and then restart the agent will trigger the UI gui_la_label = node['cpe_anyconnect']['la_gui_identifier'] - file 'trigger_gui' do + file 'trigger_gui_install' do action :nothing only_if { ::File.exist?("/Library/LaunchAgents/#{gui_la_label}.plist") } path '/opt/cisco/anyconnect/gui_keepalive' @@ -156,26 +170,34 @@ def debian_manage end def macos_manage - # If the Anyconnect App goes missing, either by accident or abuse, trigger re-install + # If the Anyconnect App or Umbrella goes missing, either by accident or abuse, trigger re-install ac_receipt = pkg['receipt'] - orgid = node['cpe_anyconnect']['organization_id'] - unless ::Dir.exist?(node['cpe_anyconnect']['app_path']) + + if (node.macos_package_present?(pkg['receipt']) && !::Dir.exist?(node['cpe_anyconnect']['app_path'])) || \ + !node.macos_package_present?(pkg['umbrella_receipt']) execute "/usr/sbin/pkgutil --forget #{ac_receipt}" do not_if { shell_out("/usr/sbin/pkgutil --pkg-info #{ac_receipt}").error? } end + + # Since we are triggering a re-installation, don't create additional popups by re-enabling the agents/daemons + return end # We only want the UI to trigger upon the first install and upgrades. # In testing, their own postinstall script logic is very unreliable. # Touch the gui_keepalive path and then restart the agent will trigger the UI gui_la_label = node['cpe_anyconnect']['la_gui_identifier'] - file 'trigger_gui' do + file 'trigger_gui_manage' do action :nothing only_if { ::File.exist?("/Library/LaunchAgents/#{gui_la_label}.plist") } path '/opt/cisco/anyconnect/gui_keepalive' notifies :restart, "launchd[#{gui_la_label}]", :immediately end + Chef::Log.warn("node['cpe_anyconnect']['final_xml_path'] = #{node['cpe_anyconnect']['final_xml_path']}") + # Continously manage VPN portals. Changes aren't noticable in AnyConnect UI without restarting client + render_anyconnect_portals_template(node['cpe_anyconnect']['final_xml_path']) + launchd gui_la_label do type 'agent' action :nothing @@ -186,50 +208,10 @@ def macos_manage action :enable label 'com.cisco.anyconnect.vpnagentd' only_if { ::File.exist?('/Library/LaunchDaemons/com.cisco.anyconnect.vpnagentd.plist') } + # If we are about to upgrade the installed version, don't trigger a UI prompt + not_if { node.macos_installed_package_lower?(pkg['receipt'], pkg['version']) } type 'daemon' - notifies :create, 'file[trigger_gui]', :immediately - end - - # Disable VPN and trigger a re-enroll by deleting the data folder if the nslookup fails - # Backup logs before directory deletion option. - unless orgid.nil? - ruby_block 'backup beacon logs' do - block do - Chef::Log.info( - 'Backup beacon /opt/cisco/anyconnect/umbrella/data/beacon-logs/ ' \ - '- trigger re-enroll of anyconnect client', - ) - require 'fileutils' - FileUtils.cp_r( - '/opt/cisco/anyconnect/umbrella/data/beacon-logs', - '/var/log', - ) - end - action :nothing - only_if { node['cpe_anyconnect']['backup_logs'] } - end - - directory '/opt/cisco/anyconnect/umbrella/data' do - action :nothing - notifies :disable, 'launchd[com.cisco.anyconnect.vpnagentd-manage]', :before - notifies :enable, 'launchd[com.cisco.anyconnect.vpnagentd-manage]', :immediately - recursive true - end - - ruby_block 'Delete /opt/cisco/anyconnect/umbrella/data - trigger re-enroll of anyconnect client' do - block do - Chef::Log.info('Delete /opt/cisco/anyconnect/umbrella/data - trigger re-enroll of anyconnect client') - end - notifies :run, 'ruby_block[backup beacon logs]', :immediately - notifies :delete, 'directory[/opt/cisco/anyconnect/umbrella/data]', :immediately - not_if { nslookup(orgid) } - only_if { node.macos_min_package_installed?(ac_receipt, '4.9.06037') } - only_if { node.daemon_running?('com.cisco.anyconnect.vpnagentd') } # must be loaded to return nslookup data - only_if { Time.now.to_i - Time.at(node.macos_boottime).to_i >= 300 } # takes a bit to fully load on boot - only_if { Time.now.to_i - Time.at(node.macos_waketime).to_i >= 300 } # takes a bit to fully load upon wake - # anyconnect must be on for a few to fully activate nslookup, otherwise this could loop infinitely - only_if { node.macos_process_uptime('vpnagentd') >= 300 } - end + notifies :create, 'file[trigger_gui_manage]', :immediately end end @@ -259,6 +241,11 @@ def windows_manage # Remove Icon for Cisco AnyConnect Secure Mobility Client remove_desktop_link end + + Chef::Log.warn("node['cpe_anyconnect']['final_xml_path'] = #{node['cpe_anyconnect']['final_xml_path']}") + + # Continously manage VPN portals. Changes aren't noticable in AnyConnect UI without restarting client + render_anyconnect_portals_template(node['cpe_anyconnect']['final_xml_path']) end def uninstall @@ -273,12 +260,32 @@ def debian_uninstall end def macos_uninstall - execute '/opt/cisco/anyconnect/bin/anyconnect_uninstall.sh' do - only_if { ::File.exist?('/opt/cisco/anyconnect/bin/anyconnect_uninstall.sh') } + uninstall_script = node['cpe_anyconnect']['uninstall_script'] + security_cmd = '/usr/bin/security authorizationdb write com.apple.system-extensions.admin' + app_path = '/Applications/Cisco/Cisco\ AnyConnect\ Socket\ Filter.app' + + # got rid of detection. defaulting to this behavior for every uninstall + if ::File.exist?(uninstall_script) + execute 'prepare_anyconnect_for_uninstall' do + command <<-SCRIPT + #{security_cmd} allow 2>/dev/null + sleep 1 + /bin/launchctl unload /Library/LaunchDaemons/com.cisco.anyconnect.vpnagentd.plist + sleep 1 + #{app_path}/Contents/MacOS/Cisco\\ AnyConnect\\ Socket\\ Filter -deactivateExt + SCRIPT + only_if { node.get_sys_ext_authorizations_rule == 'authenticate-admin-nonshared' } if node.at_least_big_sur? + end + + execute uninstall_script + + execute '/opt/cisco/anyconnect/bin/dart_uninstall.sh' do + only_if { ::File.exist?('/opt/cisco/anyconnect/bin/dart_uninstall.sh') } + end end - execute '/opt/cisco/anyconnect/bin/dart_uninstall.sh' do - only_if { ::File.exist?('/opt/cisco/anyconnect/bin/dart_uninstall.sh') } + execute "#{security_cmd} authenticate-admin-nonshared 2>/dev/null" do + only_if { node.get_sys_ext_authorizations_rule == 'allow' } end end @@ -350,14 +357,44 @@ def create_anyconnect_cache end end + def render_anyconnect_portals_template(on_disk_xml) + par_dir = Pathname.new(on_disk_xml).dirname.to_s + directory par_dir do + group node['root_group'] + owner root_owner + mode '0755' + recursive true + end + + template on_disk_xml do + cookbook node['cpe_anyconnect']['cookbook']['cookbook_name'] + source node['cpe_anyconnect']['cookbook']['template_source'] + group node['root_group'] + owner root_owner + mode '0755' + variables( + :host_entries => node['cpe_anyconnect']['vpn_portals'], + ) + not_if on_disk_xml.nil? + not_if node['cpe_anyconnect']['vpn_portals'].nil? + not_if node['cpe_anyconnect']['cookbook']['cookbook_name'].nil? + not_if node['cpe_anyconnect']['cookbook']['template_source'].nil? + end + end + def sync_anyconnect_cache # Sync the entire anyconnect folder to handle any files an admin would need + # files/default/anyconnect/profiles/vpn/UberCertPush_client_profile.xml is written to disk + # by remote_directory for configuring portals. This file is then utilized by AnyConnect's + # postinstall during installation to configure portals. remote_directory anyconnect_root_cache_path do group node['root_group'] owner root_owner mode '0755' source 'anyconnect' end + # Save portal xml into chef cache. This will be picked up by AnyConnect postinstall + render_anyconnect_portals_template("#{anyconnect_root_cache_path}/profiles/vpn/UberCertPush_client_profile.xml") end def download_package(pkg) @@ -383,9 +420,8 @@ def bfe_service_group # Either LocalServiceNoNetwork or LoclalServiceNoNetworkFirewall return nil unless windows? - cmd = "(Get-WMIObject Win32_Service -Filter \"Name='BFE'\")" - group = powershell_out("(#{cmd}.PathName).split(' ')[2]").stdout.to_s.chomp - group.empty? ? nil : group + group = ::Win32::Service.config_info('BFE').binary_path_name + group.nil? ? nil : group.split(/ /, 4)[2].chomp end def bfe_registry @@ -464,38 +500,4 @@ def windows_vpnagent_service_status status = powershell_out('(Get-Service vpnagent).status').stdout.to_s.chomp status.empty? ? nil : status end - - def nslookup(orgid) - count = 0 - json_path = ::File.join(anyconnect_root_cache_path, 'cpe_anyconnect.json') - guard_successful = true - unless node.nslookup_txt_records('debug.opendns.com')['orgid'] == orgid - if ::File.exists?(json_path) - count = node.parse_json(json_path)['nslookup_failures'] + 1 - else - count = 1 - end - end - - if count > node['cpe_anyconnect']['nslookup_failure_count_threshold'] - count = 0 - guard_successful = false - end - - write_json(count, json_path) - return guard_successful - end - - def write_json(count, json_path) - if count == 0 - file json_path do - action :delete - end - else - file json_path do - action :create - content Chef::JSONCompat.to_json_pretty({ 'nslookup_failures' => count }) - end - end - end end From e728e4947740a137f8a1fb290e1214ac04b33378 Mon Sep 17 00:00:00 2001 From: ttyson Date: Wed, 16 Aug 2023 08:11:53 -0700 Subject: [PATCH 2/2] Syncing from Internal --- chef/cookbooks/cpe_appstream/README.md | 66 +++++ .../cpe_appstream/attributes/default.rb | 37 +++ chef/cookbooks/cpe_appstream/chefignore | 110 ++++++++ .../cpe_appstream/libraries/appstream.rb | 42 +++ chef/cookbooks/cpe_appstream/metadata.rb | 10 + .../cpe_appstream/recipes/default.rb | 16 ++ .../cpe_appstream/resources/cpe_appstream.rb | 140 ++++++++++ chef/cookbooks/cpe_bitlocker/README.md | 60 ++++ .../cpe_bitlocker/attributes/default.rb | 18 ++ chef/cookbooks/cpe_bitlocker/chefignore | 104 +++++++ chef/cookbooks/cpe_bitlocker/metadata.rb | 9 + .../cpe_bitlocker/recipes/default.rb | 16 ++ .../cpe_bitlocker/resources/cpe_bitlocker.rb | 262 ++++++++++++++++++ chef/cookbooks/cpe_office365/README.md | 80 ++++++ .../cpe_office365/attributes/default.rb | 58 ++++ chef/cookbooks/cpe_office365/metadata.rb | 9 + .../cpe_office365/recipes/default.rb | 15 + .../cpe_office365/resources/cpe_office365.rb | 193 +++++++++++++ .../templates/default/office_365.xml.erb | 42 +++ chef/cookbooks/cpe_winlogbeat/README.md | 63 +++++ .../cpe_winlogbeat/attributes/default.rb | 24 ++ chef/cookbooks/cpe_winlogbeat/chefignore | 104 +++++++ chef/cookbooks/cpe_winlogbeat/metadata.rb | 10 + .../cpe_winlogbeat/recipes/default.rb | 16 ++ .../resources/cpe_winlogbeat.rb | 155 +++++++++++ 25 files changed, 1659 insertions(+) create mode 100755 chef/cookbooks/cpe_appstream/README.md create mode 100755 chef/cookbooks/cpe_appstream/attributes/default.rb create mode 100755 chef/cookbooks/cpe_appstream/chefignore create mode 100755 chef/cookbooks/cpe_appstream/libraries/appstream.rb create mode 100755 chef/cookbooks/cpe_appstream/metadata.rb create mode 100755 chef/cookbooks/cpe_appstream/recipes/default.rb create mode 100755 chef/cookbooks/cpe_appstream/resources/cpe_appstream.rb create mode 100755 chef/cookbooks/cpe_bitlocker/README.md create mode 100755 chef/cookbooks/cpe_bitlocker/attributes/default.rb create mode 100755 chef/cookbooks/cpe_bitlocker/chefignore create mode 100755 chef/cookbooks/cpe_bitlocker/metadata.rb create mode 100755 chef/cookbooks/cpe_bitlocker/recipes/default.rb create mode 100644 chef/cookbooks/cpe_bitlocker/resources/cpe_bitlocker.rb create mode 100755 chef/cookbooks/cpe_office365/README.md create mode 100755 chef/cookbooks/cpe_office365/attributes/default.rb create mode 100755 chef/cookbooks/cpe_office365/metadata.rb create mode 100755 chef/cookbooks/cpe_office365/recipes/default.rb create mode 100755 chef/cookbooks/cpe_office365/resources/cpe_office365.rb create mode 100755 chef/cookbooks/cpe_office365/templates/default/office_365.xml.erb create mode 100755 chef/cookbooks/cpe_winlogbeat/README.md create mode 100755 chef/cookbooks/cpe_winlogbeat/attributes/default.rb create mode 100755 chef/cookbooks/cpe_winlogbeat/chefignore create mode 100755 chef/cookbooks/cpe_winlogbeat/metadata.rb create mode 100755 chef/cookbooks/cpe_winlogbeat/recipes/default.rb create mode 100644 chef/cookbooks/cpe_winlogbeat/resources/cpe_winlogbeat.rb diff --git a/chef/cookbooks/cpe_appstream/README.md b/chef/cookbooks/cpe_appstream/README.md new file mode 100755 index 0000000..2513cc1 --- /dev/null +++ b/chef/cookbooks/cpe_appstream/README.md @@ -0,0 +1,66 @@ +cpe_appstream Cookbook +======================== +This cookbook utilizes AWS's Image Builder application + +This cookbook depends on the following cookbooks + +* cpe_utils + +These cookbooks are offered by Facebook in the [IT-CPE](https://github.com/facebook/IT-CPE) repository. + +Attributes +---------- +* node['cpe_appstream'] +* node['cpe_appstream']['install'] +* node['cpe_appstream']['image_assistant_path'] +* node['cpe_appstream']['image_builder_tag'] +* node['cpe_appstream']['applications']['name'] +* node['cpe_appstream']['applications']['display_name'] +* node['cpe_appstream']['applications']['path'] +* node['cpe_appstream']['applications']['working_dir'] +* node['cpe_appstream']['applications']['manifest_path'] +* node['cpe_appstream']['image']['name'] +* node['cpe_appstream']['image']['description'] +* node['cpe_appstream']['image']['display_name'] +* node['cpe_appstream']['image']['enable_dynamic_app_catalog'] +* node['cpe_appstream']['image']['use_latest_agent_version'] +* node['cpe_appstream']['image']['tags'] +* node['cpe_appstream']['image']['name'] + + +Usage +--- +* node['cpe_appstream']['image_builder_tag'] specifies the location of a file used by chef to tag an Appstream image_builder device. This tag is used to guard other resources so that chef can run as normal while the image builder is being prepared. After appstream_image builder is ran successfully, `cpe_appstream` will remove this file and perform an image capture. + +A base install config might look like this +``` +node.default['install'] = true +node.default['cpe_appstream']['image_builder_tag'] = ::File.join( + node['cpe_uber_utils']['dirs']['cpe']['tags'], + '.image_builder', +) +node.default['cpe_appstream']['applications'] = [ + { + 'name' => 'chrome', + 'display_name' => 'Google Chrome', + 'path' => 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'key' => nil, + }, + { + 'name' => 'notepad', + 'display_name' => 'Notepad', + 'path' => 'C:\\Windows\\system32\\notepad.exe', + 'key' => nil, + }, +] +date = Time.now.strftime('%m_%d_%Y') +node.default['cpe_appstream']['image'] = { + 'name' => "CPE_Appstream_#{date}", + 'description' => 'Image Created Automatically using Chef Client', + 'display_name' => "CPE_Appstream_#{date}", + 'tags' => { + 'Owner' => 'cpe', + }, +} +``` + diff --git a/chef/cookbooks/cpe_appstream/attributes/default.rb b/chef/cookbooks/cpe_appstream/attributes/default.rb new file mode 100755 index 0000000..3cc9190 --- /dev/null +++ b/chef/cookbooks/cpe_appstream/attributes/default.rb @@ -0,0 +1,37 @@ +# +# Cookbook:: cpe_appstream +# Attributes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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. +# + +default['cpe_appstream'] = { + 'install' => false, + 'image_assistant_path' => 'C:\\Program Files\\Amazon\\Photon\\ConsoleImageBuilder\\image-assistant.exe', + 'image_builder_tag' => nil, + 'applications' => [ + { + 'name' => nil, + 'display_name' => nil, + 'path' => nil, + 'working_dir' => nil, + 'manifest_path' => nil, + }, + ], + 'image' => { + 'name' => nil, + 'description' => nil, + 'display_name' => nil, + 'enable_dynamic_app_catalog' => nil, + 'use_latest_agent_version' => nil, + 'tags' => { + 'Owner' => nil, + }, + }, +} diff --git a/chef/cookbooks/cpe_appstream/chefignore b/chef/cookbooks/cpe_appstream/chefignore new file mode 100755 index 0000000..5039e1c --- /dev/null +++ b/chef/cookbooks/cpe_appstream/chefignore @@ -0,0 +1,110 @@ +# Put files/directories that should be ignored in this file when uploading +# to a Chef Infra Server or Supermarket. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +ehthumbs.db +Icon? +nohup.out +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +.#* +.project +.settings +*_flymake +*_flymake.* +*.bak +*.sw[a-z] +*.tmproj +*~ +\#* +mkmf.log +REVISION +TAGS* +tmtags + +## COMPILED ## +############## +*.class +*.com +*.dll +*.exe +*.o +*.pyc +*.so +*/rdoc/ +a.out + +# Testing # +########### +.circleci/* +.codeclimate.yml +.foodcritic +.kitchen* +.rspec +.rubocop.yml +.travis.yml +.watchr +azure-pipelines.yml +examples/* +features/* +Guardfile +kitchen.yml* +Procfile +Rakefile +spec/* +spec/* +spec/fixtures/* +test/* + +# SCM # +####### +.git +.gitattributes +.gitconfig +.github/* +.gitignore +.gitmodules +.svn +*/.bzr/* +*/.git +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Bundler # +########### +vendor/* +Gemfile +Gemfile.lock + +# Policyfile # +############## +Policyfile.rb +Policyfile.lock.json + +# Cookbooks # +############# +CHANGELOG* +CONTRIBUTING* +TESTING* +CODE_OF_CONDUCT* + +# Vagrant # +########### +.vagrant +Vagrantfile diff --git a/chef/cookbooks/cpe_appstream/libraries/appstream.rb b/chef/cookbooks/cpe_appstream/libraries/appstream.rb new file mode 100755 index 0000000..9fb9190 --- /dev/null +++ b/chef/cookbooks/cpe_appstream/libraries/appstream.rb @@ -0,0 +1,42 @@ +# Cookbook:: cpe_appstream +# Library:: appstream +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-present, Uber Technologies, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# + +class Chef + class Node + def image_assistant_path + 'C:/Program Files/Amazon/Photon/ConsoleImageBuilder/image-assistant.exe' + end + + def image_builder_add_application(name, path, display_name) + return unless windows? + + ps_cmd = "& \'#{image_assistant_path}\' add-application --name \"#{name}\""\ + " --absolute-app-path \"#{path}\" --display-name \"#{display_name}\"" + Chef::JSONCompat.parse(powershell_out(ps_cmd).stdout.to_s.chomp) + end + + def image_builder_remove_application(name) + return unless windows? + + ps_cmd = "& \'#{image_assistant_path}\' remove-application --name \"#{name}\"" + Chef::JSONCompat.parse(powershell_out(ps_cmd).stdout.to_s.chomp) + end + + def image_builder_list_applications + return unless windows? + + ps_cmd = "& \'#{image_assistant_path}\' list-applications" + Chef::JSONCompat.parse(powershell_out(ps_cmd).stdout.to_s.chomp) + end + end +end diff --git a/chef/cookbooks/cpe_appstream/metadata.rb b/chef/cookbooks/cpe_appstream/metadata.rb new file mode 100755 index 0000000..1266116 --- /dev/null +++ b/chef/cookbooks/cpe_appstream/metadata.rb @@ -0,0 +1,10 @@ +name 'cpe_appstream' +maintainer 'Uber Technologies, Inc.' +maintainer_email 'noreply@uber.com' +license 'All Rights Reserved' +description 'AWS Appstream Image Automation' +long_description 'Automates the Imagebuilder process for AWS Appstream Service' +version '0.1.0' +chef_version '>= 17.0' + +depends 'cpe_utils' diff --git a/chef/cookbooks/cpe_appstream/recipes/default.rb b/chef/cookbooks/cpe_appstream/recipes/default.rb new file mode 100755 index 0000000..4d7a86a --- /dev/null +++ b/chef/cookbooks/cpe_appstream/recipes/default.rb @@ -0,0 +1,16 @@ +# +# Cookbook:: cpe_appstream +# Recipes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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. +# + +return unless windows? + +cpe_appstream 'Prepare Appstream Image' diff --git a/chef/cookbooks/cpe_appstream/resources/cpe_appstream.rb b/chef/cookbooks/cpe_appstream/resources/cpe_appstream.rb new file mode 100755 index 0000000..27e2136 --- /dev/null +++ b/chef/cookbooks/cpe_appstream/resources/cpe_appstream.rb @@ -0,0 +1,140 @@ +# +# Cookbook:: cpe_appstream +# Resources:: cpe_appstream +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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_appstream +provides :cpe_appstream, :os => 'windows' + +default_action :manage + +action :manage do + install if install? && valid_config? +end + +action_class do # rubocop:disable Metrics/BlockLength + def install? + config['install'] + end + + def install + remove_all_applications unless image_builder_added_applications.empty? + image_builder_add_applications + image_builder_capture_image + end + + def remove_all_applications + image_builder_added_applications.each do |app| + node.image_builder_remove_application(app['Name']) + end + end + + def requested_application_names + requested = [] + application_catalog.each do |app| + requested << app['name'] if app.keys.include?('name') + end + requested + end + + def image_builder_added_applications + installed = [] + apps = node.image_builder_list_applications + unless apps.nil? || apps.empty? + apps['applications'].each do |app| + installed << app['Name'] if app.keys.include?('Name') + end + end + installed + end + + def image_builder_add_applications + # add all applications to Appstream config + application_catalog.each do |app| + cmd = "\'#{config['image_assistant_path']}\' add-application --name \'#{app['name']}\' "\ + "--absolute-app-path \'#{app['path']}\' --display-name \'#{app['display_name']}\'" + powershell_script "Image Builder - Adding #{app['name']}" do + code <<-PSSCRIPT + $result = & #{cmd} + $result = $result | convertfrom-json + if ($result.status -eq 1) { Write-Output $result.message } + exit $result.status + PSSCRIPT + not_if { image_builder_added_applications.include?(app['name']) } + only_if { ::File.exists?(app['path']) } + end + end + + file 'C:/ProgramData/Amazon/Photon/Prewarm/PrewarmManifest.txt' do + rights :full_control, 'S-1-1-0' # Everyone + action :create_if_missing + end + end + + def image_builder_capture_image + cmd = "\'#{config['image_assistant_path']}\' create-image --name \'#{image_config['name']}\'" + cmd += " --description \'#{image_config['description']}\'" if image_config['description'] + cmd += " --display-name \'#{image_config['display_name']}\'" if image_config['display_name'] + cmd += " --tags #{image_config_tags}" if image_config_tags + + windows_task 'CaptureImage' do + command "powershell.exe -noprofile -executionpolicy bypass -command \"Start-Sleep -Seconds 180; & #{cmd}\"" + frequency :none + run_level :highest + end + + file image_builder_tag do + action :nothing + notifies :run, 'windows_task[CaptureImage]', :delayed + end + + powershell_script 'CaptureImage Test' do + code <<-PSSCRIPT + $result = & #{cmd} --dry-run + $result = $result | convertfrom-json + if ($result.status -eq 1) { Write-Output $result.message } + exit $result.status + PSSCRIPT + action :run + only_if { requested_application_names.sort == image_builder_added_applications.sort } + notifies :delete, "file[#{image_builder_tag}]", :delayed + end + end + + def application_catalog + config['applications'].to_a.each(&:compact!).reject(&:empty?) + end + + def image_config + config['image'].to_h.reject { |_k, v| v.nil? } + end + + def image_config_tags + tags = image_config['tags'].to_h.reject { |_k, v| v.nil? } + tag_string = '' + tags.each { |k, v| tag_string += "\'#{k}\' \'#{v}\' " } + tag_string.chomp(' ') + end + + def image_builder_tag + config['image_builder_tag'] + end + + def config + node['cpe_appstream'] + end + + def valid_config? + image_config['name'] && image_builder_tag && + ::File.exists?(image_builder_tag) && ::File.exists?(config['image_assistant_path']) + end +end diff --git a/chef/cookbooks/cpe_bitlocker/README.md b/chef/cookbooks/cpe_bitlocker/README.md new file mode 100755 index 0000000..885963a --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/README.md @@ -0,0 +1,60 @@ +cpe_bitlocker Cookbook +======================== + +Encrypts and Configures Bitlocker for Windows + +This cookbook depends on the following cookbooks + +* cpe_uber_utils + +These cookbooks are offered by Uber in the [IT-CPE](https://github.com/uber/IT-CPE)) repository. + +Attributes +---------- + +* node['cpe_bitlocker'] +* node['cpe_bitlocker']['encrypt'] +* node['cpe_bitlocker']['configure'] + +Notes +----- + +For details on configuration options, see the official documentation: + +Usage +----- + +Before using this cookbook to encrypt bitlocker, you will need to ensure that the bitlocker Powershell Module is installed on the target device and available. See : + + Backup-BitLockerKeyProtector + [-MountPoint] + [-KeyProtectorId] + [-WhatIf] + [-Confirm] + [] + + $BLV = Get-BitLockerVolume -MountPoint "C:" + Backup-BitLockerKeyProtector -MountPoint "C:" -KeyProtectorId $BLV.KeyProtector[1].KeyProtectorId + +Background +---------- + +**BitLocker encryption process** +The following steps describe the flow of events that should result in a successful encryption of a Windows device that has *not* been previously encrypted with BitLocker. + +INTUNE + +1. An administrator configures a BitLocker policy in Intune with the desired settings, and targets a user group or device group. +2. The policy is saved to a tenant in the Intune service. +3. A Windows Device Management (MDM) client syncs with the Intune service and processes the BitLocker policy settings. +4. The BitLocker MDM policy Refresh scheduled task runs on the device that replicates the BitLocker policy settings to full volume encryption (FVE) registry key. +5. BitLocker encryption is initiated on the drives. +CHEF +***NOTE:*** In the event that intune has not encrypted the device for whatever reason (See ) We are using chef to kick encryption + +6. Removes Existing FVE Registry settings +7. Takes TPM Ownership +8. Removes GPO/WS1 KeyProtectors +9. Enables Bitlocker +10. Backs up Bitlocker Key to AAD +11. Creates New Bitlocker Registry Setting Mirroring the policies we set with Intune Profile diff --git a/chef/cookbooks/cpe_bitlocker/attributes/default.rb b/chef/cookbooks/cpe_bitlocker/attributes/default.rb new file mode 100755 index 0000000..b6f9a30 --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/attributes/default.rb @@ -0,0 +1,18 @@ +# +# Cookbook:: cpe_bitlocker +# Attributes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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. +# + +default['cpe_bitlocker'] = { + 'encrypt' => false, + 'configure' => false, + 'cleanup' => false, +} diff --git a/chef/cookbooks/cpe_bitlocker/chefignore b/chef/cookbooks/cpe_bitlocker/chefignore new file mode 100755 index 0000000..4439807 --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/chefignore @@ -0,0 +1,104 @@ +# Put files/directories that should be ignored in this file when uploading +# to a chef-server or supermarket. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +Icon? +nohup.out +ehthumbs.db +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED ## +############## +a.out +*.o +*.pyc +*.so +*.com +*.class +*.dll +*.exe +*/rdoc/ + +# Testing # +########### +.watchr +.rspec +spec/* +spec/fixtures/* +test/* +features/* +examples/* +Guardfile +Procfile +.kitchen* +kitchen.yml* +.rubocop.yml +spec/* +Rakefile +.travis.yml +.foodcritic +.codeclimate.yml + +# SCM # +####### +.git +*/.git +.gitignore +.gitmodules +.gitconfig +.gitattributes +.svn +*/.bzr/* +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Bundler # +########### +vendor/* + +# Policyfile # +############## +Policyfile.rb +Policyfile.lock.json + +# Cookbooks # +############# +CONTRIBUTING* +CHANGELOG* +TESTING* + +# Vagrant # +########### +.vagrant +Vagrantfile diff --git a/chef/cookbooks/cpe_bitlocker/metadata.rb b/chef/cookbooks/cpe_bitlocker/metadata.rb new file mode 100755 index 0000000..4bd0291 --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/metadata.rb @@ -0,0 +1,9 @@ +name 'cpe_bitlocker' +maintainer 'Uber Technologies, Inc.' +maintainer_email 'noreply@uber.com' +license 'Apache-2.0' +description 'Installs/Configures cpe_bitlocker' +version '0.1.0' +chef_version '>= 16.13' + +depends 'cpe_uber_utils' diff --git a/chef/cookbooks/cpe_bitlocker/recipes/default.rb b/chef/cookbooks/cpe_bitlocker/recipes/default.rb new file mode 100755 index 0000000..e048f2b --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/recipes/default.rb @@ -0,0 +1,16 @@ +# +# Cookbook:: cpe_bitlocker +# Recipes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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. +# + +return unless windows? + +cpe_bitlocker 'Encrypt and Configure Bitlocker' diff --git a/chef/cookbooks/cpe_bitlocker/resources/cpe_bitlocker.rb b/chef/cookbooks/cpe_bitlocker/resources/cpe_bitlocker.rb new file mode 100644 index 0000000..ba35139 --- /dev/null +++ b/chef/cookbooks/cpe_bitlocker/resources/cpe_bitlocker.rb @@ -0,0 +1,262 @@ +# +# Cookbook:: cpe_bitlocker +# Resources:: cpe_bitlocker +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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_bitlocker +provides :cpe_bitlocker, :os => ['windows'] + +default_action :manage + +action :manage do + encrypt if encrypt? + configure if configure? + cleanup if cleanup? +end + +action_class do # rubocop:disable Metrics/BlockLength + def encrypt? + node['cpe_bitlocker']['encrypt'] + end + + def configure? + node['cpe_bitlocker']['configure'] + end + + def cleanup? + node['cpe_bitlocker']['cleanup'] + end + + def encrypt + # Return if disk fully encrypted or encryption in progress + return if encryption_status['ConversionStatus'] == 2 || encryption_status['ProtectionStatus'] == 1 + + # Remove existing encryption remnants + cleanup if node.tpm_owned? + + # Enable Bitlocker + enable_bitlocker + + case encryption_status['ConversionStatus'] + when 0 + node.windows_create_eventlog( + 'Application', 'chef-bitlocker', '1005', 'Warn', "#{encryption_status['DriveLetter']}" \ + 'Drive Disk Not Encrypted' + ) + when 1 + node.windows_create_eventlog( + 'Application', 'chef-bitlocker', '1006', 'Info', "#{encryption_status['DriveLetter']}" \ + 'Drive Disk Fully Encrypted' + ) + when 2 + node.windows_create_eventlog( + 'Application', 'chef-bitlocker', '1006', 'Info', "#{encryption_status['DriveLetter']}" \ + 'Drive Disk Encryption In Progress' + ) + when 3 + node.windows_create_eventlog( + 'Application', 'chef-bitlocker', '1005', 'Warn', "#{encryption_status['DriveLetter']}" \ + 'Drive Decryption In Progress' + ) + else + node.windows_create_eventlog( + 'Application', 'chef-bitlocker', '1005', 'Warn', "#{encryption_status['DriveLetter']}" \ + 'Drive Encryption Paused' + ) + end + end + + def configure + # Set Bitlocker Policy Registry Settings + registry_key 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\FVE' do + values [ + { :name => 'EncryptionMethodWithXtsOs', :type => :dword, :data => 7 }, + { :name => 'EncryptionMethodWithXtsFdv', :type => :dword, :data => 7 }, + { :name => 'EncryptionMethodWithXtsRdv', :type => :dword, :data => 4 }, + { :name => 'FDVRecovery', :type => :dword, :data => 1 }, + { :name => 'FDVRecoveryPassword', :type => :dword, :data => 2 }, + { :name => 'FDVManageDRA', :type => :dword, :data => 1 }, + { :name => 'FDVHideRecoveryPage', :type => :dword, :data => 1 }, + { :name => 'FDVActiveDirectoryBackup', :type => :dword, :data => 1 }, + { :name => 'FDVRequireActiveDirectoryBackup', :type => :dword, :data => 1 }, + { :name => 'RDVDenyCrossOrg', :type => :dword, :data => 0 }, + { :name => 'OSRecovery', :type => :dword, :data => 1 }, + { :name => 'OSManageDRA', :type => :dword, :data => 1 }, + { :name => 'OSRecoveryPassword', :type => :dword, :data => 1 }, + { :name => 'OSRecoveryKey', :type => :dword, :data => 2 }, + { :name => 'OSHideRecoveryPage', :type => :dword, :data => 1 }, + { :name => 'OSActiveDirectoryBackup', :type => :dword, :data => 1 }, + { :name => 'OSRequireActiveDirectoryBackup', :type => :dword, :data => 1 }, + { :name => 'OSActiveDirectoryInfoToStore', :type => :dword, :data => 1 }, + { :name => 'UseAdvancedStartup', :type => :dword, :data => 1 }, + { :name => 'EnableBDEWithNoTPM', :type => :dword, :data => 0 }, + { :name => 'UseTPMKey', :type => :dword, :data => 0 }, + { :name => 'UseTPMKeyPIN', :type => :dword, :data => 0 }, + { :name => 'UseTPM', :type => :dword, :data => 1 }, + ] + action :create + end + end + + def cleanup + # Remove Existing Bitlocker Settings + registry_key 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\FVE' do + recursive true + action :delete_key + end + + # Take Ownership of TPM + set_tpm_ownsership + if node.tpm_owned? + node.windows_create_eventlog('Application', 'chef-bitlocker', '1002', 'Info', 'TPM is Owned') + else + node.windows_create_eventlog('Application', 'chef-bitlocker', '1002', 'Warn', 'Unable to take TPM Ownership') + end + + # Remove Any Existing Recovery Password KeyProtector (GPO/USB/MDM) + if !encryption_status['KeyProtector'].nil? + remove_bitlocker_key_protectors + if encryption_status['KeyProtector'].nil? + node.windows_create_eventlog('Application', 'chef-bitlocker', '1003', 'Info', 'Key Protector Cleared') + else + node.windows_create_eventlog('Application', 'chef-bitlocker', '1003', 'Warn', 'Unable to clear Key Protector') + end + else + node.windows_create_eventlog('Application', 'chef-bitlocker', '1003', 'Info', 'Key Protector Empty') + end + end + + def encryption_status + node.disk_encryption_info + end + + def set_tpm_ownsership + return unless windows? && node.tpm? + + unless node.tpm_owned? || node.provisioning_in_progress? + powershell_script 'Set TPM Ownership' do + ignore_failure true + code <<-PSSCRIPT + try { + $TPMClass = Get-WmiObject -Namespace "root\\cimv2\\Security\\MicrosoftTPM" -Class "Win32_TPM" + $NewPassPhrase = (New-Guid).Guid.Replace("-", "").SubString(0, 14) + $NewOwnerAuth = $TPMClass.ConvertToOwnerAuth($NewPassPhrase).OwnerAuth + $TPMClass.TakeOwnership($NewOwnerAuth) + exit 0 + } + catch { + exit 1 + } + PSSCRIPT + end + end + end + + def remove_bitlocker_key_protectors + unless windows? + Chef::Log.warn('node.remove_bitlocker_key_protectors called on non-windows!') + return {} + end + + powershell_script 'Remove Bitlocker Key Protectors' do + only_if { !node.provisioning_in_progress? } + ignore_failure true + code <<-PSSCRIPT + try { + Get-BitLockerVolume -MountPoint $env:SystemRoot | select -expandproperty 'KeyProtector' ` + | foreach $_.KeyProtectorId { + Remove-BitLockerKeyProtector -MountPoint $env:SystemRoot -KeyProtectorId $_.KeyProtectorId + } + exit 0 + } + catch { + exit 1 + } + PSSCRIPT + end + Chef::Log.info("Clearing Recovery Password KeyProtectors for: #{encryption_status['DriveLetter']}") + end + + def enable_bitlocker + unless windows? + Chef::Log.warn('node.enable_bitlocker called on non-windows!') + return {} + end + + powershell_script 'Enable Bitlocker' do + only_if { node.tpm? && node.powershell_module?('BitLocker') } + not_if { node.provisioning_in_progress? } + ignore_failure true + code <<-PSSCRIPT + try { + ($encrypt = Enable-BitLocker -MountPoint #{encryption_status['DriveLetter']} -TpmProtector -UsedSpaceOnly ` + -EncryptionMethod "XtsAes256" -SkipHardwareTest -ErrorAction SilentlyContinue -WarningAction silentlyContinue) | out-null + ($encrypt = Enable-BitLocker -MountPoint #{encryption_status['DriveLetter']} -RecoveryPasswordProtector -UsedSpaceOnly ` + -EncryptionMethod "XtsAes256" -SkipHardwareTest -ErrorAction SilentlyContinue -WarningAction silentlyContinue) | out-null + BackupToAAD-BitLockerKeyProtector -MountPoint #{encryption_status['DriveLetter']} -KeyProtectorId ` + ((Get-BitLockerVolume -MountPoint #{encryption_status['DriveLetter']} ).KeyProtector ` + | where {$_.KeyProtectorType -eq "RecoveryPassword" }).KeyProtectorId -ErrorAction SilentlyContinue ` + -WarningAction silentlyContinue | Out-Null + exit 0 + } + catch { + exit 1 + } + PSSCRIPT + end + Chef::Log.info("Enabling Bitlocker for: #{encryption_status['DriveLetter']}") + end + + def suspend_bitlocker(reboot_count) + unless windows? + Chef::Log.warn('node.suspend_bitlocker called on non-windows!') + return {} + end + + powershell_script 'Suspend Bitlocker' do + only_if { !node.provisioning_in_progress? } + ignore_failure true + code <<-PSSCRIPT + try { + Suspend-BitLocker -MountPoint #{encryption_status['DriveLetter']} -RebootCount #{reboot_count} + exit 0 + } + catch { + exit 1 + } + PSSCRIPT + end + Chef::Log.info("Suspending Bitlocker for: #{encryption_status['DriveLetter']}") + end + + def disable_bitlocker + unless windows? + Chef::Log.warn('node.disable_bitlocker called on non-windows!') + return {} + end + + powershell_script 'Disabling Bitlocker' do + only_if { !node.provisioning_in_progress? } + ignore_failure true + code <<-PSSCRIPT + try { + Disable-BitLocker -MountPoint #{encryption_status['DriveLetter']} + exit 0 + } + catch { + exit 1 + } + PSSCRIPT + end + Chef::Log.info("Disabling Bitlocker for: #{encryption_status['DriveLetter']}") + end +end diff --git a/chef/cookbooks/cpe_office365/README.md b/chef/cookbooks/cpe_office365/README.md new file mode 100755 index 0000000..6589571 --- /dev/null +++ b/chef/cookbooks/cpe_office365/README.md @@ -0,0 +1,80 @@ +cpe_office365 Cookbook +======================== +Installs office365, and manages the config and services. + +This cookbook depends on the following cookbooks + +* cpe_remote + +This cookbook is offered by Uber in the [IT-CPE](https://github.com/uber/IT-CPE) repository. + +Attributes +---------- +* node['cpe_office365'] +* node['cpe_office365']['install'] +* node['cpe_office365']['uninstall'] +* node['cpe_office365']['catalog'] +* node['cpe_office365']['config'] +* node['cpe_office365']['bin'] + +Notes +----- +For details on configuration options, see the official microsoft documentation: https://docs.microsoft.com/en-us/deployoffice/office-deployment-tool-configuration-options + +Usage +----- +Before using this cookbook to install Office365, you will need to determine how you will scope the + +By default, this cookbook will not set any configuration options, the office 365 +installer will fail to install office if no configuration is specified. + +A config for installing O365 Pro Plus Retail might look like this: + +The below is an example of our Config Options. +In This example we validate Active Directory Group membership +```ruby +if node.person_in_group?('office365-group') +node.default['cpe_office365']['install'] = true + { # Set Custom configuration settings + 'conf_name' => 'office_365', + 'migrate_arch' => true, + 'channel' => 'Current', + 'office_client_edition' => 64, + 'product_id' => 'O365ProPlusRetail', + 'shared_computer_licensing' => 0, + 'scl_cache_override' => 0, + 'auto_activate' => 0, + 'force_app_shutdown' => true, + 'device_based_licensing' => 0, + 'updates_enabled' => true, + 'company' => 'UBER', + 'display_level' => 'None', + 'accept_eula' => true, + 'remove_msi' => true, + 'match_os' => true, + 'match_previous_msi' => true, + 'exclude' => true, + }.each do |k, v| + node.default['cpe_office365']['config'][k] = v + end +``` +To determine which Applications are installed you will need to update the Catalog Hash values +```ruby + 'catalog' => { + 'access' => false, + 'bing' => false, + 'excel' => false, + 'groove' => false, + 'lync' => false, + 'onenote' => false, + 'onedrive' => false, + 'outlook' => false, + 'powerpoint' => false, + 'publisher' => false, + 'teams' => false, + 'word' => false, + }, +``` + +Note: There are several combinations of configuration options, +test thoroughly before using in production. diff --git a/chef/cookbooks/cpe_office365/attributes/default.rb b/chef/cookbooks/cpe_office365/attributes/default.rb new file mode 100755 index 0000000..f32a03c --- /dev/null +++ b/chef/cookbooks/cpe_office365/attributes/default.rb @@ -0,0 +1,58 @@ +# +# Cookbook:: cpe_office365 +# Attributes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2021-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. +# + +default['cpe_office365'] = { + 'install' => false, + 'uninstall' => false, + 'version' => nil, + 'bin' => { + 'bin_name' => nil, + 'checksum' => nil, + }, + 'config' => { + 'conf_name' => nil, + 'migrate_arch' => nil, + 'channel' => nil, + 'office_client_edition' => nil, + 'product_id' => nil, + 'shared_computer_licensing' => nil, + 'scl_cache_override' => nil, + 'auto_activate' => nil, + 'force_app_shutdown' => nil, + 'device_based_licensing' => nil, + 'updates_enabled' => nil, + 'company' => nil, + 'display_level' => nil, + 'accept_eula' => nil, + 'remove_all' => nil, + 'remove_msi' => nil, + 'match_os' => nil, + 'match_previous_msi' => nil, + 'exclude' => nil, + 'ignored_products' => [], # Products not included in uninstall + }, + 'catalog' => { + 'access' => false, + 'bing' => false, + 'excel' => false, + 'groove' => false, + 'lync' => false, + 'onenote' => false, + 'oneDrive' => false, + 'outlook' => false, + 'powerpoint' => false, + 'publisher' => false, + 'teams' => false, + 'word' => false, + }, +} diff --git a/chef/cookbooks/cpe_office365/metadata.rb b/chef/cookbooks/cpe_office365/metadata.rb new file mode 100755 index 0000000..c418f34 --- /dev/null +++ b/chef/cookbooks/cpe_office365/metadata.rb @@ -0,0 +1,9 @@ +name 'cpe_office365' +maintainer 'Uber Technologies, Inc.' +maintainer_email 'noreply@uber.com' +license 'Apache-2.0' +description 'Installs/Configures office365' +version '0.1.0' +chef_version '>= 14.14' + +depends 'cpe_remote' diff --git a/chef/cookbooks/cpe_office365/recipes/default.rb b/chef/cookbooks/cpe_office365/recipes/default.rb new file mode 100755 index 0000000..bbcec0d --- /dev/null +++ b/chef/cookbooks/cpe_office365/recipes/default.rb @@ -0,0 +1,15 @@ +# +# Cookbook:: cpe_office365 +# Recipes:: default +# +# 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. +# +return unless windows? + +cpe_office365 'Install and Configure Office365' diff --git a/chef/cookbooks/cpe_office365/resources/cpe_office365.rb b/chef/cookbooks/cpe_office365/resources/cpe_office365.rb new file mode 100755 index 0000000..bdc0dfe --- /dev/null +++ b/chef/cookbooks/cpe_office365/resources/cpe_office365.rb @@ -0,0 +1,193 @@ +# +# Cookbook:: cpe_office365 +# Resources:: cpe_office365 +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2021-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_office365 +provides :cpe_office365, :os => ['windows'] + +default_action :manage + +action :manage do + install if install? + uninstall if !install? && uninstall? +end + +action_class do # rubocop:disable Metrics/BlockLength + def install? + node['cpe_office365']['install'] + end + + def uninstall? + node['cpe_office365']['uninstall'] + end + + def configure_install + template conf_path do + source "#{conf_name}.erb" + variables( + :excluded_apps => office_365_excluded_apps, + :migrate_arch => office_365_config_hash['migrate_arch'], + :channel => office_365_config_hash['channel'], + :office_client_edition => office_365_config_hash['office_client_edition'], + :product_id => office_365_config_hash['product_id'], + :shared_computer_licensing => office_365_config_hash['shared_computer_licensing'], + :scl_cache_override => office_365_config_hash['scl_cache_override'], + :auto_activate => office_365_config_hash['auto_activate'], + :force_app_shutdown => office_365_config_hash['force_app_shutdown'], + :device_based_licensing => office_365_config_hash['device_based_licensing'], + :updates_enabled => office_365_config_hash['updates_enabled'], + :company => office_365_config_hash['company'], + :display_level => office_365_config_hash['display_level'], + :accept_eula => office_365_config_hash['accept_eula'], + :remove_msi => office_365_config_hash['remove_msi'], + :match_os => office_365_config_hash['match_os'], + :match_previous_msi => office_365_config_hash['match_previous_msi'], + :exclude => office_365_config_hash['exclude'], + :ignored_products => office_365_config_hash['ignored_products'], + ) + end + end + + def configure_uninstall + template conf_path do + source "#{conf_name}.erb" + variables( + :display_level => office_365_config_hash['display_level'], + :accept_eula => office_365_config_hash['accept_eula'], + :remove_all => office_365_config_hash['remove_all'], + :remove_msi => office_365_config_hash['remove_msi'], + :force_app_shutdown => office_365_config_hash['force_app_shutdown'], + ) + only_if { office_365_pkg_installed? } + end + end + + def install + configure_install + # Download Office Executable + cpe_remote_file bin_name do + file_name exe_name + checksum office_365_bin['checksum'] + path exe_path + end + # Create Scheduled task + ps_cmd = "#{exe_path} /configure #{conf_path}" + windows_task office_365_config_hash['conf_name'] do + command ps_cmd + frequency :none + run_level :highest + end + # Run Scheduled Task + windows_task office_365_config_hash['conf_name'] do + action :run + execution_time_limit 1800 + not_if { office_365_requested_apps_installed? } + only_if { office_365_config_exist? } + end + end + + def uninstall + configure_uninstall + # Download Office Executable + cpe_remote_file bin_name do + file_name exe_name + checksum office_365_bin['checksum'] + only_if { office_365_pkg_installed? } + path exe_path + end + # Uninstall the package per the specified config file + execute bin_name do + command "#{exe_path} /configure #{conf_path}" + notifies :delete, "file[#{conf_path}]", :delayed + notifies :delete, "file[#{exe_path}]", :delayed + timeout 600 + only_if { office_365_pkg_installed? } + only_if { office_365_config_exist? } + end + # Cleanup Installation + windows_task office_365_config_hash['conf_name'] do + action :delete + end + + file conf_path do + action :nothing + end + + file exe_path do + action :nothing + end + end + + def office_365_requested_apps + office_365_catalog_hash.select { |_k, v| v == true }.keys + end + + def office_365_excluded_apps + office_365_catalog_hash.select { |_k, v| v == false }.keys + end + + def office_365_pkg_installed? + node['packages'].keys.any? { |k| k.include?('Microsoft 365 Apps for enterprise') } + end + + def office_365_app_installed?(app) + # Unlike every other application, teams is an IM provider and lives under a seperate registry location + regkey = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Office\\ClickToRun\\REGISTRY\\MACHINE\\Software\\' + app.include?('teams') ? key = regkey + 'IM Providers\\Teams' : key = regkey + "Classes\\#{app}.Application" + registry_key_exists?(key, :x86_64) + end + + def office_365_requested_apps_installed? + office_365_requested_apps.all? { |app| office_365_app_installed?(app) } + end + + def office_365_config_exist? + ::File.exist?(conf_path) + end + + def office_365_bin + node['cpe_office365']['bin'].to_h.reject { |_k, v| v.nil? } + end + + def office_365_config_hash + node['cpe_office365']['config'].to_h.reject { |_k, v| v.nil? } + end + + def office_365_catalog_hash + node['cpe_office365']['catalog'].to_h.reject { |_k, v| v.nil? } + end + + def bin_name + office_365_bin['bin_name'] + end + + def exe_name + "office_365-#{office_365_bin['version']}.exe" + end + + def conf_name + "#{office_365_config_hash['conf_name']}.xml" + end + + def exe_path + ::File.join(cache_path, exe_name) + end + + def conf_path + ::File.join(cache_path, conf_name) + end + + def cache_path + Chef::Config[:file_cache_path] + end +end diff --git a/chef/cookbooks/cpe_office365/templates/default/office_365.xml.erb b/chef/cookbooks/cpe_office365/templates/default/office_365.xml.erb new file mode 100755 index 0000000..edc9d5a --- /dev/null +++ b/chef/cookbooks/cpe_office365/templates/default/office_365.xml.erb @@ -0,0 +1,42 @@ + +<%- if node['cpe_office365']['install']%> + + + <% if @match_os%> + + <% end %> + <% if @match_previous_msi%> + + <% end %> + <% if @exclude%> + <% @excluded_apps.each do |apps| -%> + + <% end -%> + <% end %> + + + + + + + + +<%- if @remove_msi.to_s%> + + <%- if @ignored_products.any?%> + <% @ignored_products.each do |prod_id| -%> + + <% end -%> + <%- end %> + +<%- end %> + +<%- elsif node.default['cpe_office365']['uninstall']%> + + + +<%- if @remove_msi%> + +<%- end %> +<%- end %> + \ No newline at end of file diff --git a/chef/cookbooks/cpe_winlogbeat/README.md b/chef/cookbooks/cpe_winlogbeat/README.md new file mode 100755 index 0000000..da1a7ec --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/README.md @@ -0,0 +1,63 @@ +cpe_winlogbeat Cookbook +======================== +Installs winlogbeat, manages config and services. + +This cookbook depends on the following cookbooks + +* cpe_remote + +Attributes +---------- +* node['cpe_winlogbeat'] +* node['cpe_winlogbeat']['install'] +* node['cpe_winlogbeat']['configure'] +* node['cpe_winlogbeat']['config'] + +Notes +----- +For details on configuration options, see the official documentation: https://www.elastic.co/guide/en/beats/winlogbeat/7.10/winlogbeat-reference-yml.html + +Usage +----- +Before using this cookbook to install winlogbeat, you will need to re-pack the zip/tar.gz files provided by Elastic. To do so, create a zip file where the contents are not in a subdirectory (as is the case with the files provided by Elastic): + +```Powershell + unzip winlogbeat-7.10.2-windows-x86_64.zip + cd winlogbeat-7.10.2-windows-x86_64 + zip -r ../winlogbeat-7.10.2-windows-x86_64_repack.zip ./* +``` + +Once they are repacked, they should be ready to use with this cookbook. cpe_remote_zip will ensure they are installed into `c:\Program Files\winlogbeat` + +By default, this cookbook will not install winlogbeat or its preferences. You may enable management of each of these things individually (Pkg and Config ). + +A config for shipping Security logs from the event viewer might look like this: + +```Ruby +node.default['cpe_winlogbeat']['config'] = { + 'output.logstash' => + { + 'hosts' => ['host:port'], + 'ssl.certificate_authorities' => [cert_path], + }, + 'fields_under_root' => true, + 'close_inactive' => '6m', + 'fields' => { + 'type' => 'winlogbeat', + 'platform' => platform, + 'chef_tags' => chef_tags.uniq, + }, + 'winlogbeat.event_logs' => [ + { + 'name' => 'Security', + 'event_id' => 0, + 'ignore_older' => '24h', + 'level' => 'information', + 'provider' => ['gupdate'], + }, + ], + 'tags' => ['cpe-winlog'], +} +``` + +Note: The winlogbeat config format is nesty and can be confusing, so be *absolutely* sure you have the format correct, otherwise the yaml will be wrong. diff --git a/chef/cookbooks/cpe_winlogbeat/attributes/default.rb b/chef/cookbooks/cpe_winlogbeat/attributes/default.rb new file mode 100755 index 0000000..4700aaf --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/attributes/default.rb @@ -0,0 +1,24 @@ +# +# Cookbook:: cpe_metricbeat +# Attributes:: default +# +# 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. +# + +default['cpe_winlogbeat'] = { + 'dir' => 'C:\ProgramData\winlogbeat', + 'bin' => 'winlogbeat.exe', + 'install' => false, + 'zip_info' => { + 'version' => nil, + 'checksum' => nil, + }, + 'configure' => false, + 'config' => {}, +} diff --git a/chef/cookbooks/cpe_winlogbeat/chefignore b/chef/cookbooks/cpe_winlogbeat/chefignore new file mode 100755 index 0000000..4439807 --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/chefignore @@ -0,0 +1,104 @@ +# Put files/directories that should be ignored in this file when uploading +# to a chef-server or supermarket. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +Icon? +nohup.out +ehthumbs.db +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED ## +############## +a.out +*.o +*.pyc +*.so +*.com +*.class +*.dll +*.exe +*/rdoc/ + +# Testing # +########### +.watchr +.rspec +spec/* +spec/fixtures/* +test/* +features/* +examples/* +Guardfile +Procfile +.kitchen* +kitchen.yml* +.rubocop.yml +spec/* +Rakefile +.travis.yml +.foodcritic +.codeclimate.yml + +# SCM # +####### +.git +*/.git +.gitignore +.gitmodules +.gitconfig +.gitattributes +.svn +*/.bzr/* +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Bundler # +########### +vendor/* + +# Policyfile # +############## +Policyfile.rb +Policyfile.lock.json + +# Cookbooks # +############# +CONTRIBUTING* +CHANGELOG* +TESTING* + +# Vagrant # +########### +.vagrant +Vagrantfile diff --git a/chef/cookbooks/cpe_winlogbeat/metadata.rb b/chef/cookbooks/cpe_winlogbeat/metadata.rb new file mode 100755 index 0000000..6cf6434 --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/metadata.rb @@ -0,0 +1,10 @@ +name 'cpe_winlogbeat' +maintainer 'Uber Technologies, Inc.' +maintainer_email 'noreply@uber.com' +license 'Apache-2.0' +description 'Installs/Configures cpe_winlogbeat' +version '0.1.0' +chef_version '>= 16.13' + +depends 'cpe_remote' +depends 'cpe_uber_utils' diff --git a/chef/cookbooks/cpe_winlogbeat/recipes/default.rb b/chef/cookbooks/cpe_winlogbeat/recipes/default.rb new file mode 100755 index 0000000..97a8ce7 --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/recipes/default.rb @@ -0,0 +1,16 @@ +# +# Cookbook:: cpe_winlogbeat +# Recipes:: default +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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. +# + +return unless windows? + +cpe_winlogbeat 'Install and Configure winlogbeat' diff --git a/chef/cookbooks/cpe_winlogbeat/resources/cpe_winlogbeat.rb b/chef/cookbooks/cpe_winlogbeat/resources/cpe_winlogbeat.rb new file mode 100644 index 0000000..f76c99b --- /dev/null +++ b/chef/cookbooks/cpe_winlogbeat/resources/cpe_winlogbeat.rb @@ -0,0 +1,155 @@ +# +# Cookbook:: cpe_winlogbeat +# Resources:: cpe_winlogbeat +# +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright:: (c) 2022-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_winlogbeat +provides :cpe_winlogbeat, :os => ['windows'] + +default_action :manage + +action :manage do + install if install? + configure if configure? + cleanup if !install? && !configure? +end + +action_class do # rubocop:disable Metrics/BlockLength + def install? + node['cpe_winlogbeat']['install'] + end + + def configure? + node['cpe_winlogbeat']['configure'] + end + + def install + pkg_name = "winlogbeat-#{pkg_info['version']}-windows-x86_64.zip" + return if pkg_info.nil? || pkg_info.empty? || pkg_name.nil? || pkg_name.empty? + + # Create winlogbeat directory + create_winlogbeat_directory + # Remove older zip archive from cache + remove_stale_cache unless current_version? + # Stop and remove the service if we are updating binaries + remove_winlogbeat_service unless current_version? + # Download and extract zip archive + cpe_remote_zip 'winlogbeat_zip' do + zip_name pkg_name + zip_checksum pkg_info['checksum'] + folder_name 'winlogbeat' + extract_location winlogbeat_dir + not_if { winlogbeat_running? } + end + end + + def configure + # Return if no configuration is specified + if winlogbeat_conf.empty? || winlogbeat_conf.nil? + Chef::Log.warn('winlogbeat config is not populated, skipping configuration') + return + end + + file ::File.join(winlogbeat_dir, 'winlogbeat.chef.yml') do + owner root_owner + group node['root_group'] + content YAML.dump(winlogbeat_conf) + notifies :restart, 'windows_service[winlogbeat]' + end + # Because windows services are annoying and start immediately, so this + # must come after the config is placed + windows_service 'winlogbeat' do + action %i[create start] + binary_path_name "#{winlogbeat_bin} -c #{winlogbeat_dir}\\winlogbeat.chef.yml" + startup_type :automatic + delayed_start true + only_if { ::File.exists?(winlogbeat_bin) } + timeout 180 + end + end + + def cleanup + remove_winlogbeat_service + + directory node['cpe_winlogbeat']['dir'] do + action :delete + recursive true + end + end + + def current_version? + if winlogbeat_exists? + powershell_cmd = "(Get-Item #{winlogbeat_bin}).VersionInfo.FileVersion" + return powershell_out(powershell_cmd).stdout.include?(pkg_info['version']) + + end + return false + end + + def winlogbeat_running? + if windows? + status = node.get_local_service_status('winlogbeat', :State) + node.safe_nil_empty?(status) ? false : status&.include?('Running') + elsif macos? + node.daemon_running?('winlogbeat') + else + shell_out('systemctl status winlogbeat').run_command.stdout.to_s[/Active: active \(running\)/].nil? ? false : true + end + end + + def winlogbeat_exists? + # Check if winlogbeat Exist + Dir.exist?(winlogbeat_dir) && ::File.exist?(winlogbeat_bin) + end + + def winlogbeat_dir + node['cpe_winlogbeat']['dir'] + end + + def winlogbeat_bin + ::File.join(winlogbeat_dir, node['cpe_winlogbeat']['bin']) + end + + def winlogbeat_conf + node['cpe_winlogbeat']['config'].to_h.compact + end + + def pkg_info + node['cpe_winlogbeat']['zip_info'].compact.reject { |_k, v| v.nil? } + end + + def create_winlogbeat_directory + directory winlogbeat_dir do + recursive true + owner root_owner + group node['root_group'] + end + end + + def winlogbeat_cache + ::File.join(Chef::Config[:file_cache_path], 'remote_zip\\C\\ProgramData\\winlogbeat') + end + + def remove_stale_cache + directory winlogbeat_cache do + recursive true + action :delete + ignore_failure true + end + end + + def remove_winlogbeat_service + windows_service 'winlogbeat' do + action %i[stop delete] + end + end +end