Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added package-module-winget and needed dependency powershell-execution-policy #101

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cfbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,52 @@
"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:winget_installed"
],
"input": [
craigcomstock marked this conversation as resolved.
Show resolved Hide resolved
{
"type": "string",
"variable": "accept_source_agreements",
"namespace": "data",
"bundle": "package_method_winget",
"label": "Accept Source Agreements",
"question": "Would you like to accept source agreements for winget packages promises? [yes|no]"
},
{
"type": "string",
"variable": "accept_package_agreements",
"namespace": "data",
"bundle": "package_method_winget",
"label": "Accept Package Agreements",
"question": "Would you like to accept package agreements for winget packages promises? [yes|no]"
},
{
"type": "string",
"variable": "allow_powershell_execution_policy_change",
"namespace": "data",
"bundle": "winget_installed",
"label": "Allow necessary PowerShell Execution Policy change: LocalMachine set to Unrestricted in order to install winget and cmdlets",
"question": "Would you like to allow this module to change PowerShell Execution Policy to LocalMachine:Unrestricted? [yes|no]"
}
]
},
"promise-type-ansible": {
"description": "Promise type to manage systemd services.",
"subdirectory": "promise-types/ansible",
Expand Down
52 changes: 52 additions & 0 deletions management/package-method-winget/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# package-method-winget

## Usage
This module enables a `winget` package method used with policy like:

```cf3
packages:
windows::
"Microsoft.WindowsTerminal"
package_method => winget,
package_policy => "add";
```

## Opt-in for accepting source and package agreements (Required)

In order for winget to operate properly you must opt-in to accepting source and packaging agreements.
Without this acceptance the package method will fail with a message like:

```console
info: Installing Microsoft.WindowsTerminal...
info: Q:powershell.exe -Comm ...:You must set some vars for package-method-winget to work
error: Finished command related to promiser 'Microsoft.WindowsTerminal' -- an error occurred, returned 1
error: Bulk package schedule execution failed somewhere - unknown outcome for 'Microsoft.WindowsTerminal'
```

Acceptance can be given either via cfbs inputs, augments, group or host specific data.

Set the value of `yes` in the following variables:
- `data:package_method_winget.accept_source_agreements`
- `data:package_method_winget.accept_package_agreements`

## Installation
This module uses both the `winget` command as well as the `Microsoft.WinGet.Client` PowerShell module as that makes it easier to gather the list of currently installed packages.

`winget` should be installed on most newer desktop systems by default.
Server images often do not have `winget` installed.

In order for `winget` and `Microsoft.WinGet.Client` to be installed, 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 setting the variable `winget_installed.allow_powershell_execution_policy_change` to the value of `yes`.
This can be set in Host/Group data or via Augments in the `data` namespace.

```json
{
"variables": {
"data:winget_installed.allow_powershell_execution_policy_change": {
"value": "yes"
}
}
}
```

6 changes: 6 additions & 0 deletions management/package-method-winget/install-winget-cli.ps1
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions management/package-method-winget/install-winget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# from https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget
# TODO, find a way to install the "latest" instead of these hard-coded versions
# as-is if newer versions are already installed this script should still succeed and serve it's purpose
$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
55 changes: 55 additions & 0 deletions management/package-method-winget/package-method-winget.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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
# an example of output of the package_list_command below is
#
# "Microsoft.WindowsTerminal","1.21.2361.0"
#
# so the matches below simply grab the thing before or after the comma separating Id and InstalledVersion fields.
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:";
}

# 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
{
classes:
"accept_source_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${data:package_method_winget.accept_source_agreements}"),
scope => "namespace";
"accept_package_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${data:package_method_winget.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 data: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 data:package_method_winget.accept_package_agreements having the value yes or no"
if => "!accept_package_agreements";
}

35 changes: 35 additions & 0 deletions management/package-method-winget/winget-installed.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
body file control
{
namespace => "winget_installed";
}

bundle agent winget_installed
{
classes:
windows::
"winget_not_installed" expression => not(returnszero("winget -v | out-null", "powershell"));
"winget_cli_not_installed" expression => not(returnszero("Get-WinGetPackage | out-null", "powershell"));
"allow_powershell_execution_policy_change" expression => regcmp("^[yY][eE]?[sS]?", "${data:winget_installed.allow_powershell_execution_policy_change}"),
scope => "namespace";

commands:
windows.!winget_installed.execution_policy_ok::
"powershell.exe -File '${this.promise_dirname}/install-winget.ps1'"
contain => default:powershell;
windows.!winget_cli_installed.execution_policy_ok::
"powershell.exe -File '${this.promise_dirname}/install-winget-cli.ps1'"
contain => default:powershell;

methods:
windows.(winget_not_installed|winget_cli_not_installed).allow_powershell_execution_policy_change::
"powershell_execution_policy_set" usebundle => default:powershell_execution_policy_set("LocalMachine", "Unrestricted"),
classes => default: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).!allow_powershell_execution_policy_change::
"package-module-winget needs winget and/or winget-cli installed. Opt-in for this to be automated by this policy by setting the variable data:winget_installed.allow_powershell_execution_policy_change to 'yes'. This can be accomplished via cfbs inputs, group/host data or augments.";
}
11 changes: 11 additions & 0 deletions management/powershell-execution-policy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# powershell-execution-policy

This module inventories and allows to set the state of the various `scopes` for PowerShell Execution Policy.

See the [Set-ExecutionPolicy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.4) documentation for details about scope and state values.

## Example

```cf3
"set_localmachine_unrestricted" usebundle => default:powershell_execution_policy_set("LocalMachine", "Unrestricted");
```
Original file line number Diff line number Diff line change
@@ -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 <policy> -Scope <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;
}