From 7748fbea2bd872e8eb9fd854bad4786af0c53b07 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 29 Aug 2024 15:24:08 -0500 Subject: [PATCH] Added package-module-winget and needed dependency powershell-execution-policy Ticket: ENT-12144 --- cfbs.json | 38 +++++++++++ management/package-method-winget/README | 30 +++++++++ .../install-winget-cli.ps1 | 6 ++ .../package-method-winget/install-winget.ps1 | 9 +++ .../package-method-winget.cf | 67 +++++++++++++++++++ management/package-method-winget/test.sh | 11 +++ .../package-method-winget/winget-installed.cf | 30 +++++++++ .../powershell-execution-policy.cf | 32 +++++++++ 8 files changed, 223 insertions(+) create mode 100644 management/package-method-winget/README create mode 100644 management/package-method-winget/install-winget-cli.ps1 create mode 100644 management/package-method-winget/install-winget.ps1 create mode 100644 management/package-method-winget/package-method-winget.cf create mode 100644 management/package-method-winget/test.sh create mode 100644 management/package-method-winget/winget-installed.cf create mode 100644 management/powershell-execution-policy/powershell-execution-policy.cf diff --git a/cfbs.json b/cfbs.json index 555f71a..cda65bf 100644 --- a/cfbs.json +++ b/cfbs.json @@ -154,6 +154,44 @@ "bundles maintainers_in_motd" ] }, + "powershell-execution-policy": { + "description": "Inventory and bundle for PowerShell Execution Policy", + "subdirectory": "management/powershell-execution-policy", + "steps": [ + "directory ./ services/cfbs/powershell-execution-policy/", + "policy_files services/cfbs/powershell-execution-policy/", + "bundles powershell_execution_policy_inventory" + ] + }, + "package-method-winget": { + "description": "Package method for Windows winget package manager.", + "subdirectory": "management/package-method-winget", + "dependencies": ["powershell-execution-policy"], + "steps": [ + "input ./input.json def.json", + "directory ./ services/cfbs/modules/package-method-winget/", + "policy_files services/cfbs/modules/package-method-winget/", + "bundles package_method_winget:package_method_winget winget_installed" + ], + "input": [ + { + "type": "string", + "variable": "accept_source_agreements", + "namespace": "package_method_winget", + "bundle": "package_method_winget", + "label": "Accept Source Agreements", + "question": "Would you like to accept source agreements for winget inventory and packages promises? [yes|no]" + }, + { + "type": "string", + "variable": "accept_package_agreements", + "namespace": "package_method_winget", + "bundle": "package_method_winget", + "label": "Accept Package Agreements", + "question": "Would you like to accept package agreements for winget inventory and packages promises? [yes|no]" + } + ] + }, "promise-type-ansible": { "description": "Promise type to manage systemd services.", "subdirectory": "promise-types/ansible", diff --git a/management/package-method-winget/README b/management/package-method-winget/README new file mode 100644 index 0000000..3299275 --- /dev/null +++ b/management/package-method-winget/README @@ -0,0 +1,30 @@ +package-method-winget +--- + +This module enables a `winget` package method used with policy like: + +```cf3 +packages: + windows:: + "Microsoft.WindowsTerminal" + package_method => winget, + package_policy => "add"; +``` + +Note that this module uses both the `winget` command as well as the PowerShell WinGet Client cmdlets as they make it easier to gather software inventory of currently installed packages. + +`winget` should be installed on most newer systems by default. + +In order for `winget` to be installed and Microsoft.WinGet.Client (the cmdlets module) ps1 scripts must be run which requires PowerShell Execution policy Unrestricted for LocalMachine. +The policy by default will not make this change. +You must opt-in by defining the class `winget_installed_allow_powershell_execution_changes` either in Groups/Host data or Augments in the namespace `data`: + +```json +{ + "classes": { + "data:winget_installed_allow_powershell_execution_changes": ["any"] + } +} +``` + +As of 2024-08-30 testing Windows AWS instances, none of them have `winget` installed. diff --git a/management/package-method-winget/install-winget-cli.ps1 b/management/package-method-winget/install-winget-cli.ps1 new file mode 100644 index 0000000..3e002c6 --- /dev/null +++ b/management/package-method-winget/install-winget-cli.ps1 @@ -0,0 +1,6 @@ +# instructions from https://github.com/microsoft/winget-cli +# WinGet.Client needs NuGet +Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted +Install-Module -Name Microsoft.WinGet.Client +Import-Module -Name Microsoft.WinGet.Client diff --git a/management/package-method-winget/install-winget.ps1 b/management/package-method-winget/install-winget.ps1 new file mode 100644 index 0000000..1a778fe --- /dev/null +++ b/management/package-method-winget/install-winget.ps1 @@ -0,0 +1,9 @@ +# from https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget +$progressPreference = 'silentlyContinue' +Write-Information "Downloading WinGet and its dependencies..." +Invoke-WebRequest -Uri https://aka.ms/getwinget -OutFile Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle +Invoke-WebRequest -Uri https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx -OutFile Microsoft.VCLibs.x64.14.00.Desktop.appx +Invoke-WebRequest -Uri https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx -OutFile Microsoft.UI.Xaml.2.8.x64.appx +Add-AppxPackage Microsoft.VCLibs.x64.14.00.Desktop.appx +Add-AppxPackage Microsoft.UI.Xaml.2.8.x64.appx +Add-AppxPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf new file mode 100644 index 0000000..69a03dd --- /dev/null +++ b/management/package-method-winget/package-method-winget.cf @@ -0,0 +1,67 @@ +# switch to module specific namespace to avoid name collisions +body file control +{ + namespace => "package_method_winget"; +} + +# package_method_winget bundle's purpose is to look at inputs/data for acceptance of source and package agreements +# these MUST be agreed to in order for this package method to work properly. +bundle agent package_method_winget +{ + vars: + # for group/host data, look in data:bundle.setting + # for cfbs inputs, look in bundle:bundle.setting as all three (namespace, bundle, variable) are required by cfbs + "var_accept_package_agreements" string => ifelse( + isvariable("data:package_method_winget.accept_package_agreements"), + "${data:package_method_winget.accept_package_agreements}", + "${package_method_winget:package_method_winget.accept_package_agreements}"); + "var_accept_source_agreements" string => ifelse( + isvariable("data:package_method_winget.accept_source_agreements"), + "${data:package_method_winget.accept_source_agreements}", + "${package_method_winget:package_method_winget.accept_source_agreements}"); + + classes: + "accept_source_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${var_accept_source_agreements}"), + scope => "namespace"; + "accept_package_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${var_accept_package_agreements}"), + scope => "namespace"; + + reports: + windows:: + "Please specify if you wish to --accept-source-agreements when using winget package method promises with the cfbs inputs or the variable package_method_winget:package_method_winget.accept_source_agreements having the value yes or no" + if => "!accept_source_agreements"; + "Please specify if you wish to --accept-package-agreements when using winget package method promises with cfbs inputs or the variable package_method_winget:package_method_winget.accept_package_agreements having the value yes or no" + if => "!accept_package_agreements"; +} + +# switch back to default namespace for ease of use in policy, e.g. refer to this package method as just `winget` instead of `package_method_winget:winget` in packages promises. +body file control +{ + namespace => "default"; +} +# NOTE: CASE MATTERS in package names, e.g. Docker.DockerDesktop will match but docker.dockerdesktop won't and so will re-install each agent run! +# TODO: fix case sensitive package names? ^^^ +body package_method winget +{ + package_changes => "bulk"; + package_name_convention => "$(name)"; + package_delete_convention => "$(name)"; + + package_installed_regex => ".*"; + # Note that package_list_name_regex does not allow for commas inside of package names as parsing that from ConvertTo-Csv below would be too complex + package_list_name_regex => '^"([^"]*)",.*'; + package_list_version_regex => '.*,"([^"]*)".*'; + + + # Here we use the Get-WinGetPackage Cmdlet because it is easier to produce easily parsed information that way + package_list_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Get-WinGetPackage | Select Id,InstalledVersion | ConvertTo-Csv "; + package_delete_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Uninstall-WinGetPackage "; + + # Here we use winget instead of PowerShell Cmdlets because we can provide the --accept-source-agreements and --accept-package-agreements this way which gets around dialog prompts + package_method_winget:accept_source_agreements.package_method_winget:accept_package_agreements:: + package_add_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"winget install --accept-source-agreements --accept-package-agreements "; + !package_method_winget:accept_source_agreements|!package_method_winget:accept_package_agreements:: + # the package name is appended to the end of this command, so we try here to make a command which conveys information only + package_add_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Write-Host You must set some vars for package-method-winget to work;exit 1; rem Trying to add package:"; +} + diff --git a/management/package-method-winget/test.sh b/management/package-method-winget/test.sh new file mode 100644 index 0000000..37447f0 --- /dev/null +++ b/management/package-method-winget/test.sh @@ -0,0 +1,11 @@ +cf-remote spawn --platform debian-12-arm64 --name hub --count 1 --role hub +cf-remote spawn --platform windows-2008 --role client --count 1 --name win2008 +cf-remote spawn --platform windows-2012 --role client --count 1 --name win2012 +cf-remote spawn --platform windows-2016 --role client --count 1 --name win2016 +cf-remote spawn --platform windows-2019 --role client --count 1 --name win2019 + +clients=win2012 win2008 win2016 win2019 +parallel cf-remote install --clients ::: $clients +for client in $clients; do + cf-remote run -H $client cf-agent -IB 172.31.18.178 # hub ip, internal, not public? +done diff --git a/management/package-method-winget/winget-installed.cf b/management/package-method-winget/winget-installed.cf new file mode 100644 index 0000000..1e38f0d --- /dev/null +++ b/management/package-method-winget/winget-installed.cf @@ -0,0 +1,30 @@ +bundle agent winget_installed +{ + classes: + windows:: +# TODO, I ~might~ need to make this "mandatory" in order for automated things to work, e.g. --accept-source-arguments --accept-package-arguments +# TODO make 'winget update --accept-source-arguments' a boolean cfbs input + "winget_not_installed" expression => not(returnszero("winget -v | out-null", "powershell")); + "winget_cli_not_installed" expression => not(returnszero("Get-WinGetPackage | out-null", "powershell")); + + commands: + windows.!winget_installed.execution_policy_ok:: + "powershell.exe -File '${this.promise_dirname}/install-winget.ps1'" + contain => powershell; + windows.!winget_cli_installed.execution_policy_ok:: + "powershell.exe -File '${this.promise_dirname}/install-winget-cli.ps1'" + contain => powershell; + + methods: + windows.(winget_not_installed|winget_cli_not_installed).data:winget_installed_allow_powershell_execution_changes:: + "powershell_execution_policy_set" usebundle => powershell_execution_policy_set("LocalMachine", "Unrestricted"), + classes => if_ok("execution_policy_ok"); + + reports: + windows.winget_not_installed:: + "In order for package-module-winget to function properly, winget must be installed."; + windows.winget_cli_not_installed:: + "In order for package-module-winget to function properly, winget-cli must be installed and imported."; + windows.(winget_not_installed|winget_cli_not_installed).!data:winget_installed_allow_powershell_execution_changes:: + "package-module-winget needs winget and/or winget-cli installed. Opt-in for this to be automated by this policy by settings the class winget_installed_allow_powershell_execution_changes in Groups/Hosts data or in augments as data:winget_installed_allow_powershell_execution_changes"; +} diff --git a/management/powershell-execution-policy/powershell-execution-policy.cf b/management/powershell-execution-policy/powershell-execution-policy.cf new file mode 100644 index 0000000..4f65904 --- /dev/null +++ b/management/powershell-execution-policy/powershell-execution-policy.cf @@ -0,0 +1,32 @@ +# Technically this is PowerShell Execution Policy, not Windows specific (TODO: s/windows/powershell/) +# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4 +bundle agent powershell_execution_policy_inventory +{ + vars: + windows:: + "execution_policy_csv_file" string => "${sys.statedir}${const.dirsep}powershell_execution_policy_list_cache.csv"; + "execution_policy_list_cache_command" string => "Get-ExecutionPolicy -list | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${execution_policy_csv_file}'"; + "csv" data => readcsv("${execution_policy_csv_file}"); + "i" slist => getindices("csv"); + "execution_policy_${csv[${i}][0]}" string => "${csv[${i}][0]}:${csv[${i}][1]}", + meta => { "inventory", "attribute_name=PowerShell Execution Policy" }; + + commands: + windows:: + "${execution_policy_list_cache_command}" + contain => powershell; +} + +# see link below for valid values for scope and policy +# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.4 +# This bundle runs a powershell command: Set-ExecutionPolicy -ExecutionPolicy -Scope +bundle agent powershell_execution_policy_set(scope, desired_policy) +{ + classes: + "policy_not_ok" expression => returnszero("if((Get-ExecutionPolicy ${scope}) -ne '${desired_policy}'){exit 0}else{exit 1}", "powershell"); + + commands: + windows.policy_not_ok:: + "Set-ExecutionPolicy -ExecutionPolicy ${desired_policy} -Scope ${scope}" + contain => powershell; +}