From fbf506dd03e12c905e87a9133e37a992191cb3d4 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 8 Feb 2023 16:23:13 +0100 Subject: [PATCH 001/146] Removed version number from README It creates extra overhead to have to update this in more places. --- promise-types/systemd/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/promise-types/systemd/README.md b/promise-types/systemd/README.md index ed85e7d7..6163b906 100644 --- a/promise-types/systemd/README.md +++ b/promise-types/systemd/README.md @@ -3,7 +3,6 @@ ## Synopsis * *Name*: `systemd` -* *Version*: `0.1.2` * *Description*: Create and manage services using systemd ## Requirements From 4f79c07facd3b3c3c349fe15c7cc2b48ac4a418f Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 8 Feb 2023 12:01:39 +0100 Subject: [PATCH 002/146] Added the conditional-installer module Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 67 ++++++++++++++++++++---- security/conditional-installer/README.md | 25 +++++++++ security/conditional-installer/main.cf | 60 +++++++++++++++++++++ 3 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 security/conditional-installer/README.md create mode 100644 security/conditional-installer/main.cf diff --git a/cfbs.json b/cfbs.json index 8f35b92e..83173cc6 100644 --- a/cfbs.json +++ b/cfbs.json @@ -13,6 +13,55 @@ "subdirectory": "reporting/client-initiated-reporting", "steps": ["json def.json def.json"] }, + "conditional-installer": { + "description": "Allows you to specify packages you want installed and conditions for where you want them installed, as well as a list of packages you generally want uninstalled.", + "subdirectory": "security/conditional-installer", + "input": [ + { + "type": "string", + "variable": "packages_to_uninstall", + "namespace": "conditional_installer", + "bundle": "conditional_installer", + "label": "Uninstall", + "question": "Which package(s) would you like to be uninstalled?" + }, + { + "type": "list", + "variable": "packages_to_install", + "namespace": "conditional_installer", + "bundle": "conditional_installer", + "label": "Install", + "subtype": [ + { + "key": "name", + "type": "string", + "label": "Package(s)", + "question": "Package(s) to install" + }, + { + "key": "condition", + "type": "string", + "label": "Condition", + "question": "Condition for where to install" + }, + { + "key": "why", + "type": "string", + "label": "Why", + "question": "Why?", + "default": "Unknown" + } + ], + "while": "Do you want to specify more packages to be installed?" + } + ], + "steps": [ + "copy main.cf services/cfbs/modules/conditional-installer/main.cf", + "input conditional-installer/input.json def.json", + "bundles conditional_installer:main", + "policy_files services/cfbs/modules/conditional-installer/main.cf" + ] + }, "demo": { "description": "Enables convenient and insecure settings for demoing CFEngine.", "subdirectory": "management/demo", @@ -106,21 +155,21 @@ "question": "Package name" }, { - "key": "why", - "type": "string", - "label": "Why", - "question": "Why should this package be uninstalled?", - "default": "Unknown" + "key": "why", + "type": "string", + "label": "Why", + "question": "Why should this package be uninstalled?", + "default": "Unknown" } ], "while": "Do you want to specify more packages to be uninstalled?" } ], "steps": [ - "copy uninstall-packages.cf services/cfbs/modules/uninstall-packages/uninstall-packages.cf", - "input uninstall-packages/input.json def.json", - "bundles uninstall_packages:uninstall_packages", - "policy_files services/cfbs/modules/uninstall-packages/uninstall-packages.cf" + "copy uninstall-packages.cf services/cfbs/modules/uninstall-packages/uninstall-packages.cf", + "input uninstall-packages/input.json def.json", + "bundles uninstall_packages:uninstall_packages", + "policy_files services/cfbs/modules/uninstall-packages/uninstall-packages.cf" ] }, "uninstall-rsh-server": { diff --git a/security/conditional-installer/README.md b/security/conditional-installer/README.md new file mode 100644 index 00000000..1c9cc394 --- /dev/null +++ b/security/conditional-installer/README.md @@ -0,0 +1,25 @@ +This is an experimental module for both uninstalling and installing packages based on conditions (CFEngine class expressions). +Users can specify a list of packages which should be uninstalled by default, and some packages which should be installed under certain conditions. +With this logic, you can have a long list of packages which are generally not allowed, and then for some of them, specify the exact scenarios where they are allowed. +This module uses the system package manager (via packages promises) to both install and uninstall packages. + +**Example:** For security reasons, you want to uninstall `talk`, `samba` and `apache2` in your infrastructure. +On your webservers, which have the `webserver` CFEngine class, you want Apache to be installed. +In this module, you put `talk,samba,apache2` in the list of packages to uninstall, and in the list of packages to install you put `apache2` with the condition `webserver`. +The module will install `apache2` on your webservers and uninstall it everywhere else. +`talk` and `samba` will be uninstalled everywhere. +As always with CFEngine, if the state is already correct, if the packages are already installed / not installed, no actions will be performed. + +**Hint:** Package names are based on your system's package manager. +In the list of packages to uninstall, it is convenient to just name all the variants of the name, for example `httpd` for RHEL and `apache2` for Debian-based systems. +For the packages to install you have to be more careful; when package names are different use the platform as part of the condition and the correct package name for that platform. +For our example above, it could be install `apache2` with the condition `webserver&debian`, and another entry to install `httpd` with the condition `webserver&redhat`. + +**Note:** This module is experimental, and things might change. +Specifically the module input currently accepts strings with comma separated package names. +We might change this to lists of strings and also try out different ways to specify input. +Feel free to play with it and give us feedback, but maybe don't use it for important infrastructure, yet. + +**Warning:** If using the same packages in both lists of packages to install and uninstall, be careful with how you specify the condition. +In general, you want the condition to be very stable (not varying over time) - when the condition is true the package will be installed and when false, the package is uninstalled. +If you use time-based classes or other conditions which vary, you can end up in a situation where you install and uninstall the package over and over again. diff --git a/security/conditional-installer/main.cf b/security/conditional-installer/main.cf new file mode 100644 index 00000000..15600577 --- /dev/null +++ b/security/conditional-installer/main.cf @@ -0,0 +1,60 @@ +body file control +{ + namespace => "conditional_installer"; +} + +bundle agent main +{ + classes: + "enabled" + if => and(isvariable(packages_to_install), isvariable(packages_to_uninstall)); + + vars: + # Mock data, should be based on input: + # "packages_to_uninstall" + # string => 'talk,talk-server,telnet,telnet-server'; +# + # "packages_to_install" + # data => '[ + # {"packages": "talk,talk-server", "condition": "any", "why": ""}, + # {"packages": "telnet", "condition": "telnet_test_machine", "why": ""} + # ]'; + enabled:: + # Determine packages to install: + "i" + slist => getindices(packages_to_install); + "_packages_to_install_lists[$(i)]" + slist => string_split("$(packages_to_install[$(i)][packages])", ",", 100), + if => "$(packages_to_install[$(i)][condition])", + comment => "$(packages_to_install[$(i)][why])"; + "_packages_to_install" + slist => getvalues(_packages_to_install_lists); + + # Determine packages to uninstall: + "_packages_to_uninstall_unfiltered" + slist => string_split("$(packages_to_uninstall)", ",", 100); + + "_packages_to_uninstall" + slist => difference( + _packages_to_uninstall_unfiltered, + _packages_to_install + ); + + packages: + enabled:: + "$(_packages_to_uninstall)" + policy => "absent"; + "$(_packages_to_install)" + policy => "present"; +} + +body file control +{ + namespace => "default"; +} + +bundle agent __main__ +{ + methods: + "conditional_installer:main"; +} From 1959c04f519db1ae810378c2789cbc6fa2f45b55 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Feb 2023 16:26:57 +0100 Subject: [PATCH 003/146] Fixed indentation --- security/conditional-installer/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/conditional-installer/main.cf b/security/conditional-installer/main.cf index 15600577..3c981d98 100644 --- a/security/conditional-installer/main.cf +++ b/security/conditional-installer/main.cf @@ -13,7 +13,7 @@ bundle agent main # Mock data, should be based on input: # "packages_to_uninstall" # string => 'talk,talk-server,telnet,telnet-server'; -# + # # "packages_to_install" # data => '[ # {"packages": "talk,talk-server", "condition": "any", "why": ""}, From 37672a73b7ee165b48859b74030a9c4b95057083 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 16 Oct 2023 09:18:47 -0500 Subject: [PATCH 004/146] Fixed bundle name for conditional_installer input data The policy looks for these variables in conditional_installer:main, not conditional_installer:conditional_installer. Ticket: ENT-10745 Changelog: Title --- cfbs.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfbs.json b/cfbs.json index 83173cc6..f4bafb41 100644 --- a/cfbs.json +++ b/cfbs.json @@ -21,7 +21,7 @@ "type": "string", "variable": "packages_to_uninstall", "namespace": "conditional_installer", - "bundle": "conditional_installer", + "bundle": "main", "label": "Uninstall", "question": "Which package(s) would you like to be uninstalled?" }, @@ -29,7 +29,7 @@ "type": "list", "variable": "packages_to_install", "namespace": "conditional_installer", - "bundle": "conditional_installer", + "bundle": "main", "label": "Install", "subtype": [ { From e6fca877f93101f0f90a48efe3c9348381199a8a Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 16 Oct 2023 09:21:50 -0500 Subject: [PATCH 005/146] Cleaned up trailing whitespace --- security/conditional-installer/main.cf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security/conditional-installer/main.cf b/security/conditional-installer/main.cf index 3c981d98..8493954a 100644 --- a/security/conditional-installer/main.cf +++ b/security/conditional-installer/main.cf @@ -13,7 +13,7 @@ bundle agent main # Mock data, should be based on input: # "packages_to_uninstall" # string => 'talk,talk-server,telnet,telnet-server'; - # + # # "packages_to_install" # data => '[ # {"packages": "talk,talk-server", "condition": "any", "why": ""}, @@ -33,7 +33,7 @@ bundle agent main # Determine packages to uninstall: "_packages_to_uninstall_unfiltered" slist => string_split("$(packages_to_uninstall)", ",", 100); - + "_packages_to_uninstall" slist => difference( _packages_to_uninstall_unfiltered, From 54363bcdab152d344e84fb32fcc7db5f6d944f33 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 16 Oct 2023 09:18:47 -0500 Subject: [PATCH 006/146] Fixed conditional_installer variables The policy looks for these variables in 'conditional_installer:main', not 'conditional_installer:conditional_installer'. And the policy looks for 'packages', not 'name' as the key holing the comma separated list of packages that should be installed for a given condition. Ticket: ENT-10745 Changelog: Title Fixed key name of packages for conditional_installer installed packages --- cfbs.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cfbs.json b/cfbs.json index 83173cc6..97865f5c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -21,7 +21,7 @@ "type": "string", "variable": "packages_to_uninstall", "namespace": "conditional_installer", - "bundle": "conditional_installer", + "bundle": "main", "label": "Uninstall", "question": "Which package(s) would you like to be uninstalled?" }, @@ -29,11 +29,11 @@ "type": "list", "variable": "packages_to_install", "namespace": "conditional_installer", - "bundle": "conditional_installer", + "bundle": "main", "label": "Install", "subtype": [ { - "key": "name", + "key": "packages", "type": "string", "label": "Package(s)", "question": "Package(s) to install" From 32a7eb60106ee9fc5444335532ea812df1a65a98 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 16 Oct 2023 12:46:38 -0500 Subject: [PATCH 007/146] conditional-installer: Fixed WARNING for module input This change fixes a warning about using a deprecated build step for input. WARNING: Deprecated 'input' build step behavior - it should be: 'input ./input.json def.json' --- cfbs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfbs.json b/cfbs.json index 97865f5c..1b45298c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -57,7 +57,7 @@ ], "steps": [ "copy main.cf services/cfbs/modules/conditional-installer/main.cf", - "input conditional-installer/input.json def.json", + "input ./input.json def.json", "bundles conditional_installer:main", "policy_files services/cfbs/modules/conditional-installer/main.cf" ] From 42f5d04d28a5f434bed2883ea1c59261fd1f4cb9 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 16 Oct 2023 09:21:50 -0500 Subject: [PATCH 008/146] Cleaned up trailing whitespace --- security/conditional-installer/main.cf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security/conditional-installer/main.cf b/security/conditional-installer/main.cf index 3c981d98..8493954a 100644 --- a/security/conditional-installer/main.cf +++ b/security/conditional-installer/main.cf @@ -13,7 +13,7 @@ bundle agent main # Mock data, should be based on input: # "packages_to_uninstall" # string => 'talk,talk-server,telnet,telnet-server'; - # + # # "packages_to_install" # data => '[ # {"packages": "talk,talk-server", "condition": "any", "why": ""}, @@ -33,7 +33,7 @@ bundle agent main # Determine packages to uninstall: "_packages_to_uninstall_unfiltered" slist => string_split("$(packages_to_uninstall)", ",", 100); - + "_packages_to_uninstall" slist => difference( _packages_to_uninstall_unfiltered, From e5a43450cf272fe84d4727a33d431c98ffb58bbc Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 1 Nov 2023 14:55:30 -0500 Subject: [PATCH 009/146] Added modules for autorun-inputs and autorun-bundles This simply mirrors the existing autorun bundle, but enables inputs and bundles independently. This is useful in the context of migrating a policy set where you have a mix of autorun policy that is present in services/autorun as well as outside that directory. This enables you to add your custom policy file tagged for autorun outside services/autorun and have it depend on autorun-bundles. --- cfbs.json | 10 ++++++++++ management/autorun-bundles/README.md | 3 +++ management/autorun-bundles/def.json | 5 +++++ management/autorun-inputs/README.md | 3 +++ management/autorun-inputs/def.json | 5 +++++ 5 files changed, 26 insertions(+) create mode 100644 management/autorun-bundles/README.md create mode 100644 management/autorun-bundles/def.json create mode 100644 management/autorun-inputs/README.md create mode 100644 management/autorun-inputs/def.json diff --git a/cfbs.json b/cfbs.json index 1b45298c..54198b89 100644 --- a/cfbs.json +++ b/cfbs.json @@ -8,6 +8,16 @@ "subdirectory": "management/autorun", "steps": ["json def.json def.json"] }, + "autorun-bundles": { + "description": "Enable automatic execution of bundles tagged 'autorun'.", + "subdirectory": "management/autorun-bundles", + "steps": ["json def.json def.json"] + }, + "autorun-inputs": { + "description": "Enable automatic loading of policy files in 'services/autorun/'.", + "subdirectory": "management/autorun-inputs", + "steps": ["json def.json def.json"] + }, "client-initiated-reporting": { "description": "Enable client initiated reporting and disable pull collection.", "subdirectory": "reporting/client-initiated-reporting", diff --git a/management/autorun-bundles/README.md b/management/autorun-bundles/README.md new file mode 100644 index 00000000..004710c7 --- /dev/null +++ b/management/autorun-bundles/README.md @@ -0,0 +1,3 @@ +# Enable autorun bundles + +Simple module to enable automatic execution of bundles tagged with `autorun`, using `def.json`. diff --git a/management/autorun-bundles/def.json b/management/autorun-bundles/def.json new file mode 100644 index 00000000..045031b7 --- /dev/null +++ b/management/autorun-bundles/def.json @@ -0,0 +1,5 @@ +{ + "classes": { + "services_autorun_bundles": ["any"] + } +} diff --git a/management/autorun-inputs/README.md b/management/autorun-inputs/README.md new file mode 100644 index 00000000..4e851b73 --- /dev/null +++ b/management/autorun-inputs/README.md @@ -0,0 +1,3 @@ +# Enable autorun inputs + +Simple module to enable automatic loading of policy files that reside in `services/autorun/`, using `def.json`. diff --git a/management/autorun-inputs/def.json b/management/autorun-inputs/def.json new file mode 100644 index 00000000..90faea3a --- /dev/null +++ b/management/autorun-inputs/def.json @@ -0,0 +1,5 @@ +{ + "classes": { + "services_autorun_inputs": ["any"] + } +} From 6d3e42128d9607e6f01d886441ec7a8f5f1333d3 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 23 Nov 2023 16:06:57 +0100 Subject: [PATCH 010/146] Added allow-all-hosts module Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 5 +++++ management/allow-all-hosts/README.md | 33 ++++++++++++++++++++++++++++ management/allow-all-hosts/def.json | 6 +++++ 3 files changed, 44 insertions(+) create mode 100644 management/allow-all-hosts/README.md create mode 100644 management/allow-all-hosts/def.json diff --git a/cfbs.json b/cfbs.json index 54198b89..14fda088 100644 --- a/cfbs.json +++ b/cfbs.json @@ -3,6 +3,11 @@ "description": "Repository of modules written by the CFEngine team.", "type": "modules", "provides": { + "allow-all-hosts": { + "description": "Allows all hosts / IP addresses to connect and fetch policy.", + "subdirectory": "management/allow-all-hosts", + "steps": ["json def.json def.json"] + }, "autorun": { "description": "Enable autorun functionality.", "subdirectory": "management/autorun", diff --git a/management/allow-all-hosts/README.md b/management/allow-all-hosts/README.md new file mode 100644 index 00000000..96e571bd --- /dev/null +++ b/management/allow-all-hosts/README.md @@ -0,0 +1,33 @@ +By default, CFEngine only accepts incoming connections from IP addresses on the same network (`/16` subnet). +This module changes the setting to allow all IP addresses. + +**Warning:** This module is intended to make testing / demonstrations easier. +It should **not** be used for production setups. + +## Details + +Internally, this changes the value of the `default:def.control_server_allowconnects` and `default:def.acl` variables. +The 2 variables have the same default values, but control slightly different things: + +* `default:def.control_server_allowconnects` - Works similar to a firewall. Controls which IP addresses are allowed to connect to the `cf-serverd`. +* `default:def.acl` - Controls access to files on the server, i.e. which IP addresses are allowed to fetch files, most notably the policy set from `/var/cfengine/masterfiles` on the hub. + +Advanced users might want to configure each of these variables individually, or even customize specific access for specific folders. + +You can achieve the same by editing `/var/cfengine/masterfiles/def.json`: + +``` +{ + "variables": { + "default:def.control_server_allowconnects": ["0.0.0.0/0", "::/0"], + "default:def.acl": ["0.0.0.0/0", "::/0"] + } +} +``` + +You can also edit these variables using the CMDB feature in Mission Portal. + +**Tip:** You can omit the first variable, it defaults to the value of `default:def.acl`, when not specified. + +**Note:** The variables and defaults mentioned here are for the default CFEngine policy set (MPF). +If you are only using the CFEngine binaries, not the default policy, these variables don't do anything special. diff --git a/management/allow-all-hosts/def.json b/management/allow-all-hosts/def.json new file mode 100644 index 00000000..a9da4255 --- /dev/null +++ b/management/allow-all-hosts/def.json @@ -0,0 +1,6 @@ +{ + "variables": { + "default:def.control_server_allowconnects": ["0.0.0.0/0", "::/0"], + "default:def.acl": ["0.0.0.0/0", "::/0"] + } +} From 154153f9b3090132c3880a8f07317eaada8f1233 Mon Sep 17 00:00:00 2001 From: Ole Petter Date: Mon, 8 Jan 2024 16:11:47 +0100 Subject: [PATCH 011/146] CI: Add a simple GitHub action to check cfbs.json with cfbs This adds a simple GitHub action and runs it on all opened and reopened pull requests. It verifies the `cfbs.json` with cfbs: * status * validate * pretty Ticket: CFE-4306 Signed-off-by: Ole Petter --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..820a3eef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: Continuous Integration + +on: + pull_request: + types: [opened, reopened] + +jobs: + unit_tests: + name: Run Unit Tests + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install dependencies + run: sudo apt-get update -y && sudo apt-get install -y python + - name: Install cfbs + run: pip install cfbs + - name: Check the status with cfbs + run: cfbs status + - name: Validate with cfbs + run: cfbs validate + - name: Check the formatting + run: cfbs --check pretty ./cfbs.json From 6a16ced0286b92c0e6a14e60ca62517928622602 Mon Sep 17 00:00:00 2001 From: Ole Petter Date: Mon, 8 Jan 2024 16:22:25 +0100 Subject: [PATCH 012/146] Run cfbs pretty on cfbs.json The file was not formatted according to cfbs pretty. Signed-off-by: Ole Petter --- cfbs.json | 86 +++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/cfbs.json b/cfbs.json index 14fda088..0bfe7bcb 100644 --- a/cfbs.json +++ b/cfbs.json @@ -31,6 +31,12 @@ "conditional-installer": { "description": "Allows you to specify packages you want installed and conditions for where you want them installed, as well as a list of packages you generally want uninstalled.", "subdirectory": "security/conditional-installer", + "steps": [ + "copy main.cf services/cfbs/modules/conditional-installer/main.cf", + "input ./input.json def.json", + "bundles conditional_installer:main", + "policy_files services/cfbs/modules/conditional-installer/main.cf" + ], "input": [ { "type": "string", @@ -69,12 +75,24 @@ ], "while": "Do you want to specify more packages to be installed?" } - ], + ] + }, + "delete-home-dotrhosts": { + "description": "Ensure that ~/.rhosts files are not present, as they present a security risk.", + "subdirectory": "security/delete-home-dotrhosts", "steps": [ - "copy main.cf services/cfbs/modules/conditional-installer/main.cf", - "input ./input.json def.json", - "bundles conditional_installer:main", - "policy_files services/cfbs/modules/conditional-installer/main.cf" + "copy policy/main.cf services/cfbs/delete-home-dotrhosts/", + "policy_files services/cfbs/delete-home-dotrhosts/", + "bundles delete_home_dotrhosts:main" + ] + }, + "delete-home-dotshosts": { + "description": "Ensure that ~/.shosts files are not present, as they present a security risk.", + "subdirectory": "security/delete-home-dotshots", + "steps": [ + "copy policy/main.cf services/cfbs/delete-home-dotshots/", + "policy_files services/cfbs/delete-home-dotshots/", + "bundles delete_home_dotshosts:main" ] }, "demo": { @@ -97,6 +115,16 @@ "subdirectory": "management/every-minute", "steps": ["json def.json def.json"] }, + "inventory-unshadowed-users": { + "description": "Adds reporting data (inventory) on local users in /etc/passwd not using /etc/shadow for their password.", + "subdirectory": "security/inventory-unshadowed-users", + "dependencies": ["library-parsed-local-users"], + "steps": [ + "copy policy/main.cf services/cfbs/inventory-unshadowed-users/", + "policy_files services/cfbs/inventory-unshadowed-users/", + "bundles inventory_unshadowed_users:main" + ] + }, "library-for-promise-types-in-bash": { "description": "Library enabling promise types implemented in bash.", "subdirectory": "libraries/bash", @@ -118,8 +146,8 @@ }, "promise-type-ansible": { "description": "Promise type to manage systemd services.", - "dependencies": ["library-for-promise-types-in-python"], "subdirectory": "promise-types/ansible", + "dependencies": ["library-for-promise-types-in-python"], "steps": [ "copy ansible_promise.py modules/promises/", "append enable.cf services/init.cf" @@ -127,8 +155,8 @@ }, "promise-type-git": { "description": "Promise type to manage git repos.", - "dependencies": ["library-for-promise-types-in-python"], "subdirectory": "promise-types/git", + "dependencies": ["library-for-promise-types-in-python"], "steps": [ "copy git.py modules/promises/", "append enable.cf services/init.cf" @@ -136,8 +164,8 @@ }, "promise-type-groups": { "description": "Experimental promise type to manage local user groups.", - "dependencies": ["library-for-promise-types-in-python"], "subdirectory": "promise-types/groups", + "dependencies": ["library-for-promise-types-in-python"], "steps": [ "copy groups.py modules/promises/", "append enable.cf services/init.cf" @@ -145,8 +173,8 @@ }, "promise-type-systemd": { "description": "Promise type to manage systemd services.", - "dependencies": ["library-for-promise-types-in-python"], "subdirectory": "promise-types/systemd", + "dependencies": ["library-for-promise-types-in-python"], "steps": [ "copy systemd.py modules/promises/", "append enable.cf services/init.cf" @@ -155,6 +183,12 @@ "uninstall-packages": { "description": "Allows you to specify a list of packages you want uninstalled on your hosts.", "subdirectory": "security/uninstall-packages", + "steps": [ + "copy uninstall-packages.cf services/cfbs/modules/uninstall-packages/uninstall-packages.cf", + "input uninstall-packages/input.json def.json", + "bundles uninstall_packages:uninstall_packages", + "policy_files services/cfbs/modules/uninstall-packages/uninstall-packages.cf" + ], "input": [ { "type": "list", @@ -179,12 +213,6 @@ ], "while": "Do you want to specify more packages to be uninstalled?" } - ], - "steps": [ - "copy uninstall-packages.cf services/cfbs/modules/uninstall-packages/uninstall-packages.cf", - "input uninstall-packages/input.json def.json", - "bundles uninstall_packages:uninstall_packages", - "policy_files services/cfbs/modules/uninstall-packages/uninstall-packages.cf" ] }, "uninstall-rsh-server": { @@ -195,34 +223,6 @@ "bundles uninstall_rsh_server", "policy_files services/cfbs/modules/uninstall-rsh-server/uninstall-rsh-server.cf" ] - }, - "inventory-unshadowed-users": { - "description": "Adds reporting data (inventory) on local users in /etc/passwd not using /etc/shadow for their password.", - "dependencies": ["library-parsed-local-users"], - "subdirectory": "security/inventory-unshadowed-users", - "steps": [ - "copy policy/main.cf services/cfbs/inventory-unshadowed-users/", - "policy_files services/cfbs/inventory-unshadowed-users/", - "bundles inventory_unshadowed_users:main" - ] - }, - "delete-home-dotrhosts": { - "description": "Ensure that ~/.rhosts files are not present, as they present a security risk.", - "subdirectory": "security/delete-home-dotrhosts", - "steps": [ - "copy policy/main.cf services/cfbs/delete-home-dotrhosts/", - "policy_files services/cfbs/delete-home-dotrhosts/", - "bundles delete_home_dotrhosts:main" - ] - }, - "delete-home-dotshosts": { - "description": "Ensure that ~/.shosts files are not present, as they present a security risk.", - "subdirectory": "security/delete-home-dotshots", - "steps": [ - "copy policy/main.cf services/cfbs/delete-home-dotshots/", - "policy_files services/cfbs/delete-home-dotshots/", - "bundles delete_home_dotshosts:main" - ] } } } From c919ccdbdd8d8941772accaf215a623ea90f12e8 Mon Sep 17 00:00:00 2001 From: Ole Petter Date: Mon, 8 Jan 2024 16:24:12 +0100 Subject: [PATCH 013/146] fix the spelling of the module value in cfbs.json The key `type`, takes `module`, and not `module_s_`. Signed-off-by: Ole Petter --- cfbs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfbs.json b/cfbs.json index 0bfe7bcb..1beb3bb8 100644 --- a/cfbs.json +++ b/cfbs.json @@ -1,7 +1,7 @@ { "name": "modules", "description": "Repository of modules written by the CFEngine team.", - "type": "modules", + "type": "module", "provides": { "allow-all-hosts": { "description": "Allows all hosts / IP addresses to connect and fetch policy.", From 349c52bdee4e206843ec16cdf494eba4dd567568 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 28 Feb 2024 17:13:30 +0100 Subject: [PATCH 014/146] Python promise modules: Renamed meta parameter to metadata This already confused Ole Petter and myself, so let's use another word that is not identical to 2 existing concepts in CFEngine. (`meta` the promise type and `meta` the promise attribute). Signed-off-by: Ole Herman Schumacher Elgesem --- examples/git-using-lib/git_using_lib.py | 4 ++-- examples/gpg/gpg.py | 4 ++-- examples/rss/rss.py | 4 ++-- examples/site-up/site_up.py | 4 ++-- libraries/python/cfengine.py | 16 ++++++++-------- promise-types/ansible/ansible_promise.py | 4 ++-- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 4 ++-- promise-types/http/http_promise_type.py | 4 ++-- promise-types/iptables/iptables.py | 4 ++-- promise-types/systemd/systemd.py | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/git-using-lib/git_using_lib.py b/examples/git-using-lib/git_using_lib.py index b6e203f1..d2033bb5 100644 --- a/examples/git-using-lib/git_using_lib.py +++ b/examples/git-using-lib/git_using_lib.py @@ -6,7 +6,7 @@ class GitPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super().__init__("git_promise_module", "0.0.3", **kwargs) - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): if not promiser.startswith("/"): raise ValidationError(f"File path '{promiser}' must be absolute") for name, value in attributes.items(): @@ -15,7 +15,7 @@ def validate_promise(self, promiser, attributes, meta): if name == "repo" and type(value) is not str: raise ValidationError(f"'repo' must be string for git promise types") - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): folder = promiser url = attributes["repo"] diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index 0b002c91..86c68a5b 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -103,7 +103,7 @@ def gpg_key_present(self, homedir, user_id): proc.communicate() self.log_error(f"Timed out querying for gpg key '{user_id}'") - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): if not promiser.startswith("/"): raise ValidationError( f"Promiser '{promiser}' for 'gpg_keys' promise must be an absolute path" @@ -113,7 +113,7 @@ def validate_promise(self, promiser, attributes, meta): f"Required attribute 'keylist' missing for 'gpg_keys' promise" ) - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): keylist_json = self.clean_storejson_output(attributes["keylist"]) self.log_verbose(f"keylist_json is '{keylist_json}'") diff --git a/examples/rss/rss.py b/examples/rss/rss.py index c95b007f..0ef6a4c8 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -8,7 +8,7 @@ def __init__(self): super().__init__("rss_promise_module", "0.0.3") - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): # check promiser type if type(promiser) is not str: raise ValidationError("invalid type for promiser: expected string") @@ -43,7 +43,7 @@ def validate_promise(self, promiser, attributes, meta): raise ValidationError(f"Invalid value '{select}' for attribute select: must be newest, oldest or random") - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): # get attriute feed feed = attributes['feed'] diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index 5341aa0d..0cdd4478 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -9,11 +9,11 @@ class SiteUpPromiseTypeModule(PromiseModule): def __init__(self): super().__init__("site_up_promise_module", "0.0.2") - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): if not self.is_url_valid(promiser): raise ValidationError(f"URL '{promiser}' is invalid") - def evaluate_promise(self, url, attributes, meta): + def evaluate_promise(self, url, attributes, metadata): ssl_ctx = ssl.create_default_context() if ( "skip_ssl_verification" in attributes diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index 8f45447b..b8a86607 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -326,10 +326,10 @@ def _handle_init(self): _put_response(self._response, self._out, self._record_file) def _handle_validate(self, promiser, attributes, request): - meta = {"promise_type": request.get("promise_type")} + metadata = {"promise_type": request.get("promise_type")} try: - self.validate_attributes(promiser, attributes, meta) - returned = self.validate_promise(promiser, attributes, meta) + self.validate_attributes(promiser, attributes, metadata) + returned = self.validate_promise(promiser, attributes, metadata) if returned is None: # Good, expected self._result = Result.VALID @@ -370,9 +370,9 @@ def _handle_validate(self, promiser, attributes, request): def _handle_evaluate(self, promiser, attributes, request): self._result_classes = None - meta = {"promise_type": request.get("promise_type")} + metadata = {"promise_type": request.get("promise_type")} try: - results = self.evaluate_promise(promiser, attributes, meta) + results = self.evaluate_promise(promiser, attributes, metadata) # evaluate_promise should return either a result or a (result, result_classes) pair if type(results) == str: @@ -448,16 +448,16 @@ def prepare_promiser_and_attributes(self, promiser, attributes): """Override if you want to modify promiser or attributes before validate or evaluate""" return (promiser, attributes) - def validate_attributes(self, promiser, attributes, meta): + def validate_attributes(self, promiser, attributes, metadata): """Override this if you want to prevent automatic validation""" return self._validate_attributes(promiser, attributes) - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): """Must override this or use validation through self.add_attribute()""" if not self._has_validation_attributes: raise NotImplementedError("Promise module must implement validate_promise") - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): raise NotImplementedError("Promise module must implement evaluate_promise") def protocol_terminate(self): diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 2fb55df0..f4b6a333 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -98,12 +98,12 @@ def prepare_promiser_and_attributes(self, promiser, attributes): safe_promiser = promiser.replace(",", "_") return (safe_promiser, attributes) - def validate_promise(self, promiser: str, attributes: Dict, meta: Dict): + def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): if not ANSIBLE_AVAILABLE: raise ValidationError("Ansible Python module not available") def evaluate_promise( - self, safe_promiser: str, attributes: Dict, meta: Dict + self, safe_promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: model = self.create_attribute_object(safe_promiser, attributes) diff --git a/promise-types/git/git.py b/promise-types/git/git.py index e472948f..f0ce1a2b 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -42,7 +42,7 @@ def depth_must_be_zero_or_more(v): self.add_attribute("update", bool, default=True) self.add_attribute("version", str, default="HEAD") - def evaluate_promise(self, promiser: str, attributes: Dict, meta: Dict): + def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): safe_promiser = promiser.replace(",", "_") attributes.setdefault("destination", promiser) model = self.create_attribute_object(promiser, attributes) diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index e11b6455..d5358fba 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -10,7 +10,7 @@ def __init__(self): self._name_regex = re.compile(r"^[a-z_][a-z0-9_-]*[$]?$") self._name_maxlen = 32 - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): # check promiser value if self._name_regex.match(promiser) is None: self.log_warning( @@ -115,7 +115,7 @@ def validate_promise(self, promiser, attributes, meta): % (duplicates, promiser) ) - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): # keep track of any repairs or failed repairs failed_repairs = 0 repairs = 0 diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index 3f2cdea8..efefb7bb 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -23,7 +23,7 @@ class HTTPPromiseModule(PromiseModule): def __init__(self, name="http_promise_module", version="2.0.0", **kwargs): super().__init__(name, version, **kwargs) - def validate_promise(self, promiser, attributes, meta): + def validate_promise(self, promiser, attributes, metadata): if "url" in attributes: url = attributes["url"] if type(url) != str: @@ -91,7 +91,7 @@ def target_fh(self, file_info): yield open(os.devnull, "wb") - def evaluate_promise(self, promiser, attributes, meta): + def evaluate_promise(self, promiser, attributes, metadata): url = attributes.get("url", promiser) method = attributes.get("method", "GET") headers = attributes.get("headers", dict()) diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index aab84c31..701fa4f6 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -159,7 +159,7 @@ def must_be_non_negative(v): self.add_attribute("rules", dict) self.add_attribute("executable", str, default="iptables") - def validate_promise(self, promiser: str, attributes: dict, meta: dict): + def validate_promise(self, promiser: str, attributes: dict, metadata: dict): command = attributes["command"] denied_attrs = self._collect_denied_attributes_of_command(command, attributes) @@ -182,7 +182,7 @@ def validate_promise(self, promiser: str, attributes: dict, meta: dict): if command != "flush" and attributes.get("chain") == "ALL": raise ValidationError("Chain 'ALL' is only available for command 'flush'") - def evaluate_promise(self, promiser: str, attributes: dict, meta: dict): + def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): safe_promiser = promiser.replace(",", "_") model = Model( diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index 6c5f4223..dc8c2d19 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -81,7 +81,7 @@ def prepare_promiser_and_attributes(self, promiser, attributes): return (safe_promiser, attributes) def evaluate_promise( - self, safe_promiser: str, attributes: Dict, meta: Dict + self, safe_promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: model = self.create_attribute_object(safe_promiser, attributes) # get the status of the service From 2b34466f5a7ab58dfd53a96064ba939a76319301 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 28 Feb 2024 17:27:00 +0100 Subject: [PATCH 015/146] Cleaned up README files relating to the updated promise types Removed version numbers from inside READMEs, since nobody has been updating them. Version numbers are already in the index and also in the python file for each module. Removed other unnecessary / duplicated information which just makes the module pages on build.cfengine.com look slightly worse. Tried making things a bit more consistent and easy to read / understand. Added a bit more information and links for the python promise type library. Tixed some small typos. Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/README.md | 16 +++++++++ libraries/python/README.org | 7 ---- promise-types/ansible/README.md | 60 ++++++++++++++------------------ promise-types/git/README.md | 12 ++----- promise-types/groups/README.md | 14 +++----- promise-types/http/README.org | 44 ++++++++++------------- promise-types/iptables/README.md | 12 ++----- promise-types/systemd/README.md | 12 ++----- 8 files changed, 73 insertions(+), 104 deletions(-) create mode 100644 libraries/python/README.md delete mode 100644 libraries/python/README.org diff --git a/libraries/python/README.md b/libraries/python/README.md new file mode 100644 index 00000000..5fa9ecac --- /dev/null +++ b/libraries/python/README.md @@ -0,0 +1,16 @@ +A library for CFEngine custom promise types implemented in Python. +It provides the `PromiseModule` base class and many helper values and mechanisms that facilitates creation of custom promises. + +If you'd like to develop new promise types (or other modules) for CFEngine, we recommend finishing our getting started tutorial: + +https://docs.cfengine.com/docs/3.21/getting-started.html + +(The last part shows implementing your first module). + +If you feel comfortable with using CFEngine and writing Python code already, there is a template project you can just copy and start editing: + +https://github.com/cfengine/promise-type-template + +For more detailed information about how custom promise types work, see our documentation: + +https://docs.cfengine.com/docs/3.21/reference-promise-types-custom.html diff --git a/libraries/python/README.org b/libraries/python/README.org deleted file mode 100644 index ba9cb344..00000000 --- a/libraries/python/README.org +++ /dev/null @@ -1,7 +0,0 @@ -* CFEngine Python promise types library - -A library for CFEngine custom promise types implemented in Python. It provides -the =PromiseModule= base class and many helper values and mechanisms that -facilitates creation of custom promises. - -See [[https://github.com/cfengine/modules/blob/master/examples/git-using-lib/git_using_lib.py][examples/git-using-lib]] for an example of how this library can be used. diff --git a/promise-types/ansible/README.md b/promise-types/ansible/README.md index 0615eb56..c6e3cbaf 100644 --- a/promise-types/ansible/README.md +++ b/promise-types/ansible/README.md @@ -1,35 +1,6 @@ -# ansible promise module +The `ansible` promise type allows you to run Ansible playbooks from within CFEngine policy. -## Synopsis - -* *Name*: `ansible` -* *Version*: `0.1.1` -* *Description*: Run Ansible playbooks - -## Requirements - -* Ansible >= 2.8.0 - -## Attributes - -| Name | Type | Description| Mandatory | Default | -| --- | --- | --- | --- | --- | -| `playbook` | `string` | Absolute path of the Ansible playbook | No | Promiser | -| `inventory` | `string` | Absolute path of the inventory file | No | - | -| `limit` | `slist` | List of host names to target | No | `{"localhost"}` | -| `tags` | `slist` | List of tags to play | No | `{}` | -| `become` | `boolean` | Set the `become` option | No | `False` | -| `become_method` | `string` | Set the `become_method` option | No | `"sudo"` | -| `become_user` | `string` | Set the `become_user` option | No | `root` | -| `connection` | `string` | Set the `connection` option; possible values: `local`, `ssh` | No | `local` | -| `forks` | `int` | Set the `forks` option | No | `1` | -| `private_key_file` | `string` | Absolute path of the SSH private key to use | No | - | -| `remote_user` | `string` | Set the `remote_user` option | No | `root` | - -## Examples - -Play the `/northern.tech/playbook.yaml` playbook locally, using `/northern.tech/inventory.yaml`, and limiting -the execution to the `helloworld` tag: +For example, you can play the `/northern.tech/playbook.yaml` playbook locally, using `/northern.tech/inventory.yaml`, and limiting the execution to the `helloworld` tag: ```cfengine3 bundle agent main @@ -42,6 +13,28 @@ bundle agent main } ``` +## Requirements + +* Ansible >= 2.8.0 + +## Attributes + +| Name | Type | Description | Mandatory | Default | +| ------------------ | --------- | ------------------------------------------------------------ | --------- | --------------- | +| `playbook` | `string` | Absolute path of the Ansible playbook | No | Promiser | +| `inventory` | `string` | Absolute path of the inventory file | No | - | +| `limit` | `slist` | List of host names to target | No | `{"localhost"}` | +| `tags` | `slist` | List of tags to play | No | `{}` | +| `become` | `boolean` | Set the `become` option | No | `False` | +| `become_method` | `string` | Set the `become_method` option | No | `"sudo"` | +| `become_user` | `string` | Set the `become_user` option | No | `root` | +| `connection` | `string` | Set the `connection` option; possible values: `local`, `ssh` | No | `local` | +| `forks` | `int` | Set the `forks` option | No | `1` | +| `private_key_file` | `string` | Absolute path of the SSH private key to use | No | - | +| `remote_user` | `string` | Set the `remote_user` option | No | `root` | + +## Examples + This promise can run ansible over ssh targeting multiple hosts, for example: ```cfengine3 @@ -64,9 +57,8 @@ bundle agent main ## Authors -This software was created by the team at [Northern.tech AS](https://northern.tech), with many contributions from the community. Thanks everyone! - -[CFEngine](https://cfengine.com) is sponsored by [Northern.tech AS](https://northern.tech) +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! ## Contribute diff --git a/promise-types/git/README.md b/promise-types/git/README.md index f15964f8..529d7539 100644 --- a/promise-types/git/README.md +++ b/promise-types/git/README.md @@ -1,9 +1,4 @@ -# git promise type - -## Synopsis - -* *Name*: `git` -* *Description*: Manage git checkouts of repositories to deploy files or software. +The `git` promise type enables writing concise policy for cloning a git repo and keeping it updated. ## Requirements @@ -67,9 +62,8 @@ bundle agent main ## Authors -This software was created by the team at [Northern.tech AS](https://northern.tech), with many contributions from the community. Thanks everyone! - -[CFEngine](https://cfengine.com) is sponsored by [Northern.tech AS](https://northern.tech) +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! ## Contribute diff --git a/promise-types/groups/README.md b/promise-types/groups/README.md index d82d0eb0..b2637c77 100644 --- a/promise-types/groups/README.md +++ b/promise-types/groups/README.md @@ -1,11 +1,6 @@ -# groups promise type +The `groups` promise type helps managing local groups, letting you ensure some users are a part of a group, or excluded from a group, etc. -## Synopsis - -* *Name*: `groups` -* *Version*: `0.1.3` -* *Description*: Manage local groups. -* *Note*: This is an experimental version of a promise type, and may be changed in the future. +**Note**: This is an experimental version of a promise type, and may be changed in the future. ## Requirements @@ -82,9 +77,8 @@ bundle agent main ## Authors -This software was created by the team at [Northern.tech AS](https://northern.tech), with many contributions from the community. Thanks everyone! - -[CFEngine](https://cfengine.com) is sponsored by [Northern.tech AS](https://northern.tech) +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! ## Contribute diff --git a/promise-types/http/README.org b/promise-types/http/README.org index cbfbb553..8feab959 100644 --- a/promise-types/http/README.org +++ b/promise-types/http/README.org @@ -1,22 +1,24 @@ -* http promise type +The =http= promise type lets you perform HTTP(S) requests from policy. -** Synopsis +** Example -- /Name/: =http= -- /Version/: =1.0.0= -- /Description/: Perform HTTP(S) requests from policy. +Here is a simple example of downloading an SVG file (with a HTTP GET request): -** Promiser and attributes - -The promiser can either be a URL of the request or, if the /url/ attribute is -used (see below), it can be an arbitrary string. All attributes are optional if -the URL is specified in the promiser. +#+BEGIN_SRC cfengine3 +bundle agent __main__ +{ + http: + "https://cfengine.com/images/cfengine-logo.svg" + file => "/var/cfengine/cfengine-logo.svg", + if => not(fileexists("/var/cfengine/cfengine-logo.svg")); +} +#+END_SRC *** Attributes | Name | Type | Description | Mandatory | Default | |------------+-----------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------+------------| -| =url= | =string= | URL or the request (starting with "http://" or "https://") | No | /promiser/ | +| =url= | =string= | URL of the request (starting with "http://" or "https://") | No | /promiser/ | | =file= | =string= | File system path for where to save the response (body). | No | - | | =method= | =string= | HTTP method of the request (GET, POST, PUT, DELETE or PATCH) | No | - | | =headers= | =string=, =slist= or =data= | headers to send with the request (=data= with key-value pairs, =slist= with colon-separated key-value pairs, or =string= with key:value pairs) on separate lines | No | - | @@ -25,6 +27,10 @@ the URL is specified in the promiser. *Note:* if no =file= attribute is provided the request will be sent to =/dev/null=. This is useful for something like a REST API endpoint where all you care about is an OK response (http code between 200 and 300) that results in the promise being =KEPT= +*Note:* The promiser can either be a URL of the request or, if the /url/ attribute is +used, it can be an arbitrary string. All attributes are optional if +the URL is specified in the promiser. + ** Result classes :PROPERTIES: :CUSTOM_ID: result-classes @@ -45,25 +51,11 @@ defined in case the response code is not an OK code. *The promiser is canonified in all the above classes.* -** Examples - -#+BEGIN_SRC cfengine3 -bundle agent __main__ -{ - http: - "https://cfengine.com/images/cfengine-logo.svg" - file => "/var/cfengine/cfengine-logo.svg", - if => not(fileexists("/var/cfengine/cfengine-logo.svg")); -} -#+END_SRC - ** Authors -This software was created by the team at [[https://northern.tech][Northern.tech AS]], with many +This software was created by the team at [[https://northern.tech][Northern.tech]], with many contributions from the community. Thanks everyone! -[[https://cfengine.com][CFEngine]] is sponsored by [[https://northern.tech][Northern.tech AS]] - ** Contribute Feel free to open pull requests to expand this documentation, add features or diff --git a/promise-types/iptables/README.md b/promise-types/iptables/README.md index d1493408..5c916c1a 100644 --- a/promise-types/iptables/README.md +++ b/promise-types/iptables/README.md @@ -1,9 +1,4 @@ -# Iptables Promise Module - -## Synopsis -- *Name*: `iptables` -- *Version*: `0.1.2` -- *Description*: Manage network packet filter rules +Promise type for managing `iptables` firewall rules. ## Requirements - [`iptables`](https://manpages.ubuntu.com/manpages/precise/en/man8/iptables.8.html) (command line tool) @@ -221,9 +216,8 @@ All rules added by the iptables custom promise will have a comment signifing it ## Authors -This software was created by the team at [Northern.tech AS](https://northern.tech), with many contributions from the community. Thanks everyone! - -[CFEngine](https://cfengine.com) is sponsored by [Northern.tech AS](https://northern.tech) +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! ## Contribute diff --git a/promise-types/systemd/README.md b/promise-types/systemd/README.md index 6163b906..952c126f 100644 --- a/promise-types/systemd/README.md +++ b/promise-types/systemd/README.md @@ -1,9 +1,4 @@ -# systemd promise module - -## Synopsis - -* *Name*: `systemd` -* *Description*: Create and manage services using systemd +The `systemd` promise type lets you create and manage services using systemd. ## Requirements @@ -98,9 +93,8 @@ bundle agent main ## Authors -This software was created by the team at [Northern.tech AS](https://northern.tech), with many contributions from the community. Thanks everyone! - -[CFEngine](https://cfengine.com) is sponsored by [Northern.tech AS](https://northern.tech) +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! ## Contribute From 7292a9020513b7adcbab8cbcb00fb5136a2a8f04 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 28 Feb 2024 17:40:27 +0100 Subject: [PATCH 016/146] iptables.py: Autoformatting by black Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/iptables/iptables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 701fa4f6..c8a90aa7 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -281,7 +281,7 @@ def _iptables_policy(self, executable, table, chain, target): def _iptables_flush(self, executable, table, chain): args = [executable, "-t", table, "-F"] - if chain != 'ALL': + if chain != "ALL": args.append(chain) self._run(args) @@ -290,7 +290,7 @@ def _iptables_all_rules_of(self, executable, table, chain) -> List[str]: If chain is specified then only _one_ policy rule will be on top. """ args = [executable, "-t", table, "-S"] - if chain != 'ALL': + if chain != "ALL": args.append(chain) return self._run(args) From 6e97b86c66263c74a5f9bb4409558721b654d757 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 28 Feb 2024 17:39:18 +0100 Subject: [PATCH 017/146] Bumped version numbers of to be released modules inside python source code Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/ansible/ansible_promise.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 2 +- promise-types/systemd/systemd.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index f4b6a333..115ac775 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -73,7 +73,7 @@ def v2_playbook_on_stats(self, stats): class AnsiblePromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(AnsiblePromiseTypeModule, self).__init__( - "ansible_promise_module", "0.2.1", **kwargs + "ansible_promise_module", "0.2.2", **kwargs ) def must_be_absolute(v): diff --git a/promise-types/git/git.py b/promise-types/git/git.py index f0ce1a2b..1576748c 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -9,7 +9,7 @@ class GitPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(GitPromiseTypeModule, self).__init__( - "git_promise_module", "0.2.2", **kwargs + "git_promise_module", "0.2.3", **kwargs ) def destination_must_be_absolute(v): diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index d5358fba..111cf571 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -6,7 +6,7 @@ class GroupsPromiseTypeModule(PromiseModule): def __init__(self): - super().__init__("groups_promise_module", "0.2.3") + super().__init__("groups_promise_module", "0.2.4") self._name_regex = re.compile(r"^[a-z_][a-z0-9_-]*[$]?$") self._name_maxlen = 32 diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index efefb7bb..735e5f04 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -20,7 +20,7 @@ def __init__(self, target): class HTTPPromiseModule(PromiseModule): - def __init__(self, name="http_promise_module", version="2.0.0", **kwargs): + def __init__(self, name="http_promise_module", version="2.0.1", **kwargs): super().__init__(name, version, **kwargs) def validate_promise(self, promiser, attributes, metadata): diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index dc8c2d19..aae14130 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -22,7 +22,7 @@ class SystemdPromiseTypeStates(Enum): class SystemdPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(SystemdPromiseTypeModule, self).__init__( - "systemd_promise_module", "0.2.2", **kwargs + "systemd_promise_module", "0.2.3", **kwargs ) def state_must_be_valid(v): From 10101ee997cc2dbc3678a846d6c67329e588cc6f Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem <4048546+olehermanse@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:51:51 +0100 Subject: [PATCH 018/146] Updated git_using_lib.py example to match blog post See: https://github.com/cfengine/website/pull/972 --- examples/git-using-lib/git_using_lib.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/git-using-lib/git_using_lib.py b/examples/git-using-lib/git_using_lib.py index d2033bb5..7e1c8b7f 100644 --- a/examples/git-using-lib/git_using_lib.py +++ b/examples/git-using-lib/git_using_lib.py @@ -3,38 +3,33 @@ class GitPromiseTypeModule(PromiseModule): - def __init__(self, **kwargs): - super().__init__("git_promise_module", "0.0.3", **kwargs) + def __init__(self): + super().__init__("git_promise_module", "0.0.1") def validate_promise(self, promiser, attributes, metadata): if not promiser.startswith("/"): raise ValidationError(f"File path '{promiser}' must be absolute") - for name, value in attributes.items(): - if name != "repo": - raise ValidationError(f"Unknown attribute '{name}' for git promises") - if name == "repo" and type(value) is not str: - raise ValidationError(f"'repo' must be string for git promise types") + if "repository" not in attributes: + raise ValidationError(f"Attribute 'repository' is required") def evaluate_promise(self, promiser, attributes, metadata): + url = attributes["repository"] folder = promiser - url = attributes["repo"] - - safe_promiser = promiser.replace(",", "_") if os.path.exists(folder): - return (Result.KEPT, [f"{safe_promiser}_cloned_already"]) + self.log_verbose(f"'{folder}' already exists, nothing to do") + return Result.KEPT self.log_info(f"Cloning '{url}' -> '{folder}'...") os.system(f"git clone {url} {folder} 2>/dev/null") if os.path.exists(folder): self.log_info(f"Successfully cloned '{url}' -> '{folder}'") - return (Result.REPAIRED, [f"{safe_promiser}_cloned"]) + return Result.REPAIRED else: self.log_error(f"Failed to clone '{url}' -> '{folder}'") - return (Result.NOT_KEPT, [f"{safe_promiser}_clone_failed"]) + return Result.NOT_KEPT if __name__ == "__main__": - # GitPromiseTypeModule(record_file_path="/tmp/git_using_lib.log").start() GitPromiseTypeModule().start() From aa4b062fcc92fb17170d9eb1ec307ccfc6f6665b Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 Mar 2024 16:16:05 +0100 Subject: [PATCH 019/146] Adjusted site-up example to be aligned with latest blog post changes See: https://github.com/cfengine/website/pull/972 Signed-off-by: Ole Herman Schumacher Elgesem --- examples/site-up/site_up.cf | 14 ++++++-------- examples/site-up/site_up.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/site-up/site_up.cf b/examples/site-up/site_up.cf index c14777ce..d0c83a3d 100644 --- a/examples/site-up/site_up.cf +++ b/examples/site-up/site_up.cf @@ -6,16 +6,14 @@ promise agent site_up } bundle agent site_up_autorun -# @brief Example illustrating use of site_up promise type { meta: - - "tags" slist => { "autorun" }; + "tags" slist => { "autorun" }; site_up: - "http://172.28.128.10"; - "https://cfengine.com/"; - "https://cfengine2.com/"; - "https://unavailable.com" - skip_ssl_verification => "true"; + "http://172.28.128.10"; + "https://cfengine.com/"; + "https://cfengine2.com/"; + "https://unavailable.com" + skip_ssl_verification => "true"; } diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index 0cdd4478..2498b95c 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -7,7 +7,14 @@ class SiteUpPromiseTypeModule(PromiseModule): def __init__(self): - super().__init__("site_up_promise_module", "0.0.2") + super().__init__("site_up_promise_module", "0.0.3") + + def is_url_valid(self, url): + regex = re.compile( + r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)", + re.IGNORECASE, + ) + return re.match(regex, url) is not None def validate_promise(self, promiser, attributes, metadata): if not self.is_url_valid(promiser): @@ -24,7 +31,7 @@ def evaluate_promise(self, url, attributes, metadata): error = None try: code = urllib.request.urlopen(url, context=ssl_ctx).getcode() - self.log_info(f"Site '{url}' is UP!") + self.log_verbose(f"Site '{url}' is UP!") return Result.KEPT except urllib.error.HTTPError as e: # HTTPError exception returns response code and useful when handling exotic HTTP errors @@ -39,13 +46,6 @@ def evaluate_promise(self, url, attributes, metadata): self.log_error(error) return Result.NOT_KEPT - def is_url_valid(self, url): - regex = re.compile( - r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)", - re.IGNORECASE, - ) - return re.match(regex, url) is not None - if __name__ == "__main__": SiteUpPromiseTypeModule().start() From edca0396afc49e1d4ba55cbbaaf212eea90d5b9d Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 Mar 2024 17:09:15 +0100 Subject: [PATCH 020/146] Fixed repo vs repository attribute in gpg example promise type Signed-off-by: Ole Herman Schumacher Elgesem --- examples/gpg/password-store-sync.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gpg/password-store-sync.cf b/examples/gpg/password-store-sync.cf index 583264a7..99132cab 100644 --- a/examples/gpg/password-store-sync.cf +++ b/examples/gpg/password-store-sync.cf @@ -25,7 +25,7 @@ bundle agent main git: "/home/user/.password-store" - repo => "https://github.com/user/my-passwords"; + repository => "https://github.com/user/my-passwords"; gpg_keys: "/home/user/.gnupg" From 1a1c8603a536900a9438d1c4d269b54c507312d6 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 Mar 2024 17:09:33 +0100 Subject: [PATCH 021/146] Renamed url parameter to promiser for consistency Signed-off-by: Ole Herman Schumacher Elgesem --- examples/site-up/site_up.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index 2498b95c..022919a2 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -20,7 +20,8 @@ def validate_promise(self, promiser, attributes, metadata): if not self.is_url_valid(promiser): raise ValidationError(f"URL '{promiser}' is invalid") - def evaluate_promise(self, url, attributes, metadata): + def evaluate_promise(self, promiser, attributes, metadata): + url = promiser ssl_ctx = ssl.create_default_context() if ( "skip_ssl_verification" in attributes From d2a970d8eb4181da1d8b1d8362cf16637602b169 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 Mar 2024 18:18:11 +0100 Subject: [PATCH 022/146] Smaller consistency fixes to align examples with blog posts Signed-off-by: Ole Herman Schumacher Elgesem --- examples/gpg/gpg.py | 2 +- examples/site-up/site_up.cf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index 86c68a5b..d1f206b1 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -1,4 +1,4 @@ -"""Custom Promise Type module for adding missing gpg keys +"""Custom promise type module for adding missing gpg keys Given some GPG user id values (see man page for all possiblities, there are many) and the ascii value of a key, ensure that the key is present in the given GPG homedir. diff --git a/examples/site-up/site_up.cf b/examples/site-up/site_up.cf index d0c83a3d..6de97f10 100644 --- a/examples/site-up/site_up.cf +++ b/examples/site-up/site_up.cf @@ -1,8 +1,8 @@ promise agent site_up # @brief Define site_up promise type { - path => "/var/cfengine/modules/promises/site_up.py"; - interpreter => "/usr/bin/python3.8"; + path => "/var/cfengine/modules/promises/site_up.py"; + interpreter => "/usr/bin/python3.8"; } bundle agent site_up_autorun From 5fed175857e2fd6a65e1d0f74ab2dddd30653e2c Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 Mar 2024 23:15:44 +0100 Subject: [PATCH 023/146] Added new module to disable automatic key trust Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 5 + .../disable-automatic-key-trust/README.md | 91 +++++++++++++++++++ .../disable-automatic-key-trust/def.json | 1 + 3 files changed, 97 insertions(+) create mode 100644 management/disable-automatic-key-trust/README.md create mode 100644 management/disable-automatic-key-trust/def.json diff --git a/cfbs.json b/cfbs.json index 1beb3bb8..24a275e7 100644 --- a/cfbs.json +++ b/cfbs.json @@ -101,6 +101,11 @@ "dependencies": ["autorun", "every-minute"], "steps": ["json def.json def.json"] }, + "disable-automatic-key-trust": { + "description": "Makes the hub / policy server stop accepting new keys automatically.", + "subdirectory": "management/disable-automatic-key-trust", + "steps": ["json def.json def.json"] + }, "enable-aslr": { "description": "Ensure that Address space layout randomization (ASLR) is enabled on the system.", "subdirectory": "security/enable-aslr", diff --git a/management/disable-automatic-key-trust/README.md b/management/disable-automatic-key-trust/README.md new file mode 100644 index 00000000..2ae510b2 --- /dev/null +++ b/management/disable-automatic-key-trust/README.md @@ -0,0 +1,91 @@ +CFEngine provides a way to automatically trust keys from other hosts, which is enabled for all hosts on the same `/16` subnet as the hub by default. +Once your hosts are bootstrapped, or if you are using another way to establish trust (distribute keys) it is recommended to disable the automatic trust mechanism. +This module helps you do that, it is equivalent to editing the augments file (`/var/cfengine/masterfiles/def.json`) to: + +```json +{ + "variables": { + "default:def.trustkeysfrom": [] + } +} +``` + +After disabling automatic trust, take a look at: + +- The [allow-hosts](https://build.cfengine.com/modules/allow-hosts/) module for specifying which hosts should be allowed to connect / fetch policy. +- The [secure bootstrap documentation](https://docs.cfengine.com/docs/3.21/getting-started-installation-secure-bootstrap.html) for more information on how to establish trust (distribute keys). + +## Details + +To connect / bootstrap a new client to a CFEngine hub, three things need to happen: + +1. It must be **allowed to connect** over the network. +2. Its cryptographic **key must be trusted** by the hub. +3. It must be **allowed to access** the policy files it wants to download. + +In CFEngine, these 3 things are controlled by the variables `def.control_server_allowconnects`, `def.trustkeysfrom` and `def.acl` (respectively). + +By default, `def.acl` is set to the `/16` subnet of the hub's IP address. +If not overriden, the two other variables are set to default to the value as `def.acl`. + +This means that, by default, any host which is on the same `/16` subnet as the hub is allowed to connect, automatically bootstrap and access the policy files. +This configuration is intented to make it easy to get started / test, but should be edited to a more secure configuration in a production environment. + +By default, CFEngine only accepts incoming connections from IP addresses on the same network (`/16` subnet). +This module changes the setting to allow all IP addresses. + +**Note:** The variables and defaults mentioned here are for the default CFEngine policy set (MPF). +If you are only using the CFEngine binaries, not the default policy, these variables don't do anything special. + +For more information on this subject, see the [secure bootstrap section of our documentation](https://docs.cfengine.com/docs/3.21/getting-started-installation-secure-bootstrap.html). + +## Examples + +To better illustrate how this works, and what options are available, I've included some examples of what you could put in your `def.json` file below. + +Allow connections from anywhere but don't trust any new keys: + +```json +{ + "variables": { + "default:def.acl": ["0.0.0.0/0", "::/0"], + "default:def.control_server_allowconnects": ["0.0.0.0/0", "::/0"], + "default:def.trustkeysfrom": [] + } +} +``` + +If you have set up just a couple of hosts, and want to only allow those, this is easy: + +```json +{ + "variables": { + "default:def.acl": ["1.2.3.4", "4.3.2.1"] + } +} +``` + +(The IP addresses `1.2.3.4` and `4.3.2.1` are just examples here, replace them with the actual IP addresses of your hosts.) + +Because of the defaults mentioned above, this is equivalent to: + +```json +{ + "variables": { + "default:def.acl": ["1.2.3.4", "4.3.2.1"], + "default:def.control_server_allowconnects": ["1.2.3.4", "4.3.2.1"], + "default:def.trustkeysfrom": ["1.2.3.4", "4.3.2.1"] + } +} +``` + +Finally, if you want those only those 2 hosts to communicate, and only with those keys they already have, you can do this: + +```json +{ + "variables": { + "default:def.acl": ["1.2.3.4", "4.3.2.1"], + "default:def.trustkeysfrom": [] + } +} +``` diff --git a/management/disable-automatic-key-trust/def.json b/management/disable-automatic-key-trust/def.json new file mode 100644 index 00000000..6dde1534 --- /dev/null +++ b/management/disable-automatic-key-trust/def.json @@ -0,0 +1 @@ +{ "variables": {"default:def.trustkeysfrom": []} } From 81432cb32dda9c89b922c8bd2be01433431e4e17 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem <4048546+olehermanse@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:38:16 +0100 Subject: [PATCH 024/146] Fixed grammar --- management/disable-automatic-key-trust/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/disable-automatic-key-trust/README.md b/management/disable-automatic-key-trust/README.md index 2ae510b2..9b12632d 100644 --- a/management/disable-automatic-key-trust/README.md +++ b/management/disable-automatic-key-trust/README.md @@ -79,7 +79,7 @@ Because of the defaults mentioned above, this is equivalent to: } ``` -Finally, if you want those only those 2 hosts to communicate, and only with those keys they already have, you can do this: +Finally, if you want only those 2 hosts to communicate, and only with those keys they already have, you can do this: ```json { From 09b0a68d6cc16190f348878284a83bcfcddfffc0 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 4 Apr 2024 13:12:25 -0500 Subject: [PATCH 025/146] Added module to disable recommendations from the MPF Ticket: ENT-11525 Changelog: Title --- cfbs.json | 5 +++++ management/disable-recommendations/README.md | 20 ++++++++++++++++++++ management/disable-recommendations/def.json | 8 ++++++++ 3 files changed, 33 insertions(+) create mode 100644 management/disable-recommendations/README.md create mode 100644 management/disable-recommendations/def.json diff --git a/cfbs.json b/cfbs.json index 24a275e7..555f71af 100644 --- a/cfbs.json +++ b/cfbs.json @@ -23,6 +23,11 @@ "subdirectory": "management/autorun-inputs", "steps": ["json def.json def.json"] }, + "disable-recommendations": { + "description": "Disable all recommendations emitted from the Masterfiles Policy Framework (MPF).", + "subdirectory": "management/disable-recommendations", + "steps": ["json def.json def.json"] + }, "client-initiated-reporting": { "description": "Enable client initiated reporting and disable pull collection.", "subdirectory": "reporting/client-initiated-reporting", diff --git a/management/disable-recommendations/README.md b/management/disable-recommendations/README.md new file mode 100644 index 00000000..2ed92639 --- /dev/null +++ b/management/disable-recommendations/README.md @@ -0,0 +1,20 @@ +The Masterfiles Policy Framework (MPF) emits recommendations for various settings given the context and role of a host. + +For example, when federated reporting is enabled on an Enterprise hub, there is a recommendation to install gnu-parallel. + +``` +R: CFEngine recommends installing gnu parallel on federated reporting superhubs. +``` + +These can be useful, but you may want to disable the functionality to quiet your policy runs. This module facilitates disabling all recommendations by defining `default:cfengine_recommendations_disabled`, it is equivalent to editing the augments file (`/var/cfengine/masterfiles/def.json`) to: + +```json +{ + "classes": { + "default:cfengine_recommendations_disabled": { + "class_expressions": [ "any::" ], + "comment": "We disabled all recommendations emitted by the MPF to quiet policy output." + } + } +} +``` diff --git a/management/disable-recommendations/def.json b/management/disable-recommendations/def.json new file mode 100644 index 00000000..715b0b42 --- /dev/null +++ b/management/disable-recommendations/def.json @@ -0,0 +1,8 @@ +{ + "classes": { + "default:cfengine_recommendations_disabled": { + "class_expressions": [ "any::" ], + "comment": "We disabled all recommendations emitted by the MPF to quiet policy output." + } + } +} From cba9e72b3018685bf6fe266e4caf57608329c3d1 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Fri, 5 Apr 2024 14:46:08 -0500 Subject: [PATCH 026/146] Update management/disable-recommendations/README.md Co-authored-by: Vratislav Podzimek --- management/disable-recommendations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/disable-recommendations/README.md b/management/disable-recommendations/README.md index 2ed92639..6f051caf 100644 --- a/management/disable-recommendations/README.md +++ b/management/disable-recommendations/README.md @@ -6,7 +6,7 @@ For example, when federated reporting is enabled on an Enterprise hub, there is R: CFEngine recommends installing gnu parallel on federated reporting superhubs. ``` -These can be useful, but you may want to disable the functionality to quiet your policy runs. This module facilitates disabling all recommendations by defining `default:cfengine_recommendations_disabled`, it is equivalent to editing the augments file (`/var/cfengine/masterfiles/def.json`) to: +These can be useful, but you may want to disable the functionality to quiet your policy runs. This module facilitates disabling all recommendations by defining the `default:cfengine_recommendations_disabled` class. Thus it is an equivalent to editing the augments file (`/var/cfengine/masterfiles/def.json`) to: ```json { From 3b9744c7173969a045f507d848acbf0fe5b78e6b Mon Sep 17 00:00:00 2001 From: Bas van der Vlies Date: Mon, 17 Jun 2024 17:22:00 +0200 Subject: [PATCH 027/146] git promise tyoe used a wrong command line option --- promise-types/git/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 1576748c..0a6fecc0 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -160,7 +160,7 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): # checkout the branch, if different from the current one output = self._git( model, - [model.executable, "rev-parse", "--abbrev-re", "HEAD".format()], + [model.executable, "rev-parse", "--abbrev-ref", "HEAD".format()], cwd=model.destination, ) detached = False From 754b639697eff92237abad390d1b644ccedb37a9 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Wed, 19 Jun 2024 16:04:11 +0200 Subject: [PATCH 028/146] Always send important log messages from Python-based promise types These messages inform about changes being done for repaired promises or errors reported for failed promises, etc. and the agent requires some whenever there's repaired/failed promise by a custom promise type module. Ticket: CFE-4406 Changelog: None --- libraries/python/cfengine.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index b8a86607..c26dbaf3 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -44,7 +44,13 @@ def _would_log(level_set, msg_level): # uknown level, assume it would be logged return True - return _LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set] + # info: log messages are special because they report changes done in promise + # evaluation which is important not only for showing to the user, but also + # for auditing/changelog and all modules are required to send info: messages + # for all REPAIRED promises. A similar logic applies to errors and warnings, + # IOW, anything at or above the info level. + return ((_LOG_LEVELS[msg_level] <= _LOG_LEVELS["info"]) or + (_LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set])) def _cfengine_type(typing): From 2404c7d037ef9bd18fc772d2daa4e2c360d6d4a4 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Wed, 19 Jun 2024 16:30:43 +0200 Subject: [PATCH 029/146] Rename _would_log() in the Python library The name no longer fits. --- libraries/python/cfengine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index c26dbaf3..e1dcf2a9 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -39,7 +39,7 @@ def _put_response(data, file, record_file=None): record_file.write("> \n") -def _would_log(level_set, msg_level): +def _should_send_log(level_set, msg_level): if msg_level not in _LOG_LEVELS: # uknown level, assume it would be logged return True @@ -404,7 +404,7 @@ def _handle_terminate(self): sys.exit(0) def _log(self, level, message): - if self._log_level is not None and not _would_log(self._log_level, level): + if self._log_level is not None and not _should_send_log(self._log_level, level): return # Message can be str or an object which implements __str__() From 284cdb60b01599c387ff57b9adb70ced9a2a2fe9 Mon Sep 17 00:00:00 2001 From: Bas van der Vlies Date: Tue, 6 Aug 2024 13:37:56 +0200 Subject: [PATCH 030/146] git promise type must just continue if we have unicode error closes https://github.com/cfengine/modules/issues/99 --- promise-types/git/git.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 0a6fecc0..1ccab387 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -236,16 +236,21 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): def _git(self, model: object, args: List[str], cwd: Optional[str] = None) -> str: self.log_verbose("Run: {cmd}".format(cmd=" ".join(args))) - output = ( - subprocess.check_output( - args, - env=self._git_envvars(model), - cwd=cwd, - stderr=subprocess.PIPE, + try: + output = ( + subprocess.check_output( + args, + env=self._git_envvars(model), + cwd=cwd, + stderr=subprocess.PIPE, + ) + .strip() + .decode("utf-8") ) - .strip() - .decode("utf-8") - ) + except UnicodeDecodeError: + output = "Could not decode output, just continue" + pass + output != "" and self.log_verbose(output) return output From eb8af73228a70374efc7755632dd8ef17ea65675 Mon Sep 17 00:00:00 2001 From: Bas van der Vlies Date: Mon, 12 Aug 2024 17:54:19 +0200 Subject: [PATCH 031/146] use `-shorstat` option this will prevent unidcode errors in the `diff` output and will detect changes --- promise-types/git/git.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 1ccab387..0766b081 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -192,6 +192,7 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): [ model.executable, "diff", + "--shortstat", "..{remote}/{version}".format( remote=model.remote, version=model.version ), @@ -236,21 +237,16 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): def _git(self, model: object, args: List[str], cwd: Optional[str] = None) -> str: self.log_verbose("Run: {cmd}".format(cmd=" ".join(args))) - try: - output = ( - subprocess.check_output( - args, - env=self._git_envvars(model), - cwd=cwd, - stderr=subprocess.PIPE, - ) - .strip() - .decode("utf-8") + output = ( + subprocess.check_output( + args, + env=self._git_envvars(model), + cwd=cwd, + stderr=subprocess.PIPE, ) - except UnicodeDecodeError: - output = "Could not decode output, just continue" - pass - + .strip() + .decode("utf-8") + ) output != "" and self.log_verbose(output) return output From bc46bc97f53a212645b9f4c16331d6277dd18faa Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 2 Sep 2024 12:53:07 +0200 Subject: [PATCH 032/146] promise-type-python: Prepared for 0.2.5 release TODO: We are not able to keep these version numbers in sync, I think we should add some "macro" like @@BUILD_INSERT_MODULE_VERSION@@; https://northerntech.atlassian.net/browse/CFE-4437 Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/git/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 0766b081..4123045c 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -9,7 +9,7 @@ class GitPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(GitPromiseTypeModule, self).__init__( - "git_promise_module", "0.2.3", **kwargs + "git_promise_module", "0.2.5", **kwargs ) def destination_must_be_absolute(v): From 25fc72cbdac42fef63709d3e55c43e5d38899eac Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 29 Aug 2024 15:24:08 -0500 Subject: [PATCH 033/146] Added package-module-winget and needed dependency powershell-execution-policy Ticket: ENT-12144 --- cfbs.json | 46 ++++++++++++++++ management/package-method-winget/README.md | 52 ++++++++++++++++++ .../install-winget-cli.ps1 | 6 ++ .../package-method-winget/install-winget.ps1 | 11 ++++ .../package-method-winget.cf | 55 +++++++++++++++++++ .../package-method-winget/winget-installed.cf | 35 ++++++++++++ .../powershell-execution-policy/README.md | 11 ++++ .../powershell-execution-policy.cf | 32 +++++++++++ 8 files changed, 248 insertions(+) create mode 100644 management/package-method-winget/README.md 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/winget-installed.cf create mode 100644 management/powershell-execution-policy/README.md create mode 100644 management/powershell-execution-policy/powershell-execution-policy.cf diff --git a/cfbs.json b/cfbs.json index 555f71af..e1f12499 100644 --- a/cfbs.json +++ b/cfbs.json @@ -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": [ + { + "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", diff --git a/management/package-method-winget/README.md b/management/package-method-winget/README.md new file mode 100644 index 00000000..c9fd99ec --- /dev/null +++ b/management/package-method-winget/README.md @@ -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" + } + } +} +``` + 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 00000000..3e002c69 --- /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 00000000..f7e7a59e --- /dev/null +++ b/management/package-method-winget/install-winget.ps1 @@ -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 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 00000000..8c3db90d --- /dev/null +++ b/management/package-method-winget/package-method-winget.cf @@ -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"; +} + diff --git a/management/package-method-winget/winget-installed.cf b/management/package-method-winget/winget-installed.cf new file mode 100644 index 00000000..a14363ed --- /dev/null +++ b/management/package-method-winget/winget-installed.cf @@ -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."; +} diff --git a/management/powershell-execution-policy/README.md b/management/powershell-execution-policy/README.md new file mode 100644 index 00000000..3fdd9e11 --- /dev/null +++ b/management/powershell-execution-policy/README.md @@ -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"); +``` 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 00000000..4f65904b --- /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; +} From 7b5664234eb738ecf1462c5a5f77dbb44d2ab21a Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Mon, 30 Sep 2024 15:51:00 -0500 Subject: [PATCH 034/146] Removed cfengine_internal_purge_policies as it is no longer needed This class's behavior became the default in 3.18 and 3.18 is EOL so all supported versions have the desired behavior as the default. --- management/demo/def.json | 1 - 1 file changed, 1 deletion(-) diff --git a/management/demo/def.json b/management/demo/def.json index 0735d827..ea609c9e 100644 --- a/management/demo/def.json +++ b/management/demo/def.json @@ -1,7 +1,6 @@ { "classes": { "mpf_augments_control_enabled": ["any"], - "cfengine_internal_purge_policies": ["any"], "cfengine_mp_fr_dependencies_auto_install": ["any"] }, "vars": { From 0146e6bef8131d68d1a06b71b4c12b147983cbc4 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 3 Oct 2024 16:45:54 -0500 Subject: [PATCH 035/146] Added windows-capability module --- cfbs.json | 9 +++ .../windows-capability/windows-capability.cf | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 management/windows-capability/windows-capability.cf diff --git a/cfbs.json b/cfbs.json index e1f12499..5a31dbf7 100644 --- a/cfbs.json +++ b/cfbs.json @@ -279,6 +279,15 @@ "bundles uninstall_rsh_server", "policy_files services/cfbs/modules/uninstall-rsh-server/uninstall-rsh-server.cf" ] + }, + "windows-capability": { + "description": "Manage and inventory Windows Capabilities.", + "subdirectory": "management/windows-capability", + "steps": [ + "directory ./ services/cfbs/windows-capability/", + "policy_files services/cfbs/windows-capability/", + "bundles windows_capability" + ] } } } diff --git a/management/windows-capability/windows-capability.cf b/management/windows-capability/windows-capability.cf new file mode 100644 index 00000000..739bb231 --- /dev/null +++ b/management/windows-capability/windows-capability.cf @@ -0,0 +1,70 @@ +bundle agent windows_capability_installed(capability_name) +{ + methods: + "Installed" usebundle => windows_capability:_windows_capability_state("${capability_name}", "Installed"); +} +bundle agent windows_optionial_capability_notpresent(capability_name) +{ + methods: + "NotPresent" usebundle => windows_capability:_windows_capability_state("${capability_name}", "NotPresent"); +} + +bundle agent windows_capability +{ + methods: + windows.!data:disable_windows_capability_inventory:: + "windows_capability:_inventory"; +} + +body file control +{ + namespace => "windows_capability"; +} + +# https://learn.microsoft.com/en-us/powershell/module/dism/get-windowscapability?view=windowsserver2022-ps +# https://learn.microsoft.com/en-us/powershell/module/dism/remove-windowscapability?view=windowsserver2022-ps +# https://learn.microsoft.com/en-us/powershell/module/dism/add-windowscapability?view=windowsserver2022-ps +bundle agent _inventory +{ + vars: + windows:: + "cache_file" string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; + "command" string => "Get-WindowsCapability -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; + + "csv" data => readcsv("${cache_file}"); + "i" slist => getindices("csv"); + "${this.namespace}[${i}]" string => "${csv[${i}][0]}:${csv[${i}][1]}", + meta => { "inventory", "attribute_name=Windows Capability" }; + + files: + "${cache_file}" + file_select => default:days_old(1), + delete => default:tidy; + + commands: + windows:: + "${command}" + if => not(fileexists("${cache_file}")), + contain => default:powershell; +} + + +bundle agent _windows_capability_state(capability_name, desired_state) +{ + vars: + "operation" string => ifelse(strcmp("${desired_state}", "Installed"), "Add", "Remove"); + classes: + windows:: + "state_not_ok" expression => returnszero("if((Get-WindowsCapability -Online -Name ${capability_name} | select-object -expandproperty state) -ne '${desired_state}'){exit 0}else{exit 1}", "powershell"); + + commands: + windows.state_not_ok:: + "${operation}-WindowsCapability -Online -Name ${capability_name}" + classes => default:results("bundle", "state"), + contain => default:powershell; + + files: + windows.state_repaired:: + "${_inventory.cache_file}" + delete => default:tidy; +} From c86d1649c267602a06a51384c887bd17104cb0df Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 3 Oct 2024 16:46:06 -0500 Subject: [PATCH 036/146] Added windows-optional-feature module --- cfbs.json | 9 +++ .../windows-optional-feature.cf | 73 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 management/windows-optional-feature/windows-optional-feature.cf diff --git a/cfbs.json b/cfbs.json index 5a31dbf7..65a1690c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -288,6 +288,15 @@ "policy_files services/cfbs/windows-capability/", "bundles windows_capability" ] + }, + "windows-optional-feature": { + "description": "Manage and inventory Windows Optional Features.", + "subdirectory": "management/windows-optional-feature", + "steps": [ + "directory ./ services/cfbs/windows-optional-feature/", + "policy_files services/cfbs/windows-optional-feature/", + "bundles windows_optional_feature" + ] } } } diff --git a/management/windows-optional-feature/windows-optional-feature.cf b/management/windows-optional-feature/windows-optional-feature.cf new file mode 100644 index 00000000..27aec9b5 --- /dev/null +++ b/management/windows-optional-feature/windows-optional-feature.cf @@ -0,0 +1,73 @@ +# https://learn.microsoft.com/en-us/powershell/module/dism/get-windowsoptionalfeature?view=windowsserver2022-ps +# https://learn.microsoft.com/en-us/powershell/module/dism/disable-windowsoptionalfeature?view=windowsserver2022-ps +# https://learn.microsoft.com/en-us/powershell/module/dism/enable-windowsoptionalfeature?view=windowsserver2022-ps +# https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v#enable-hyper-v-using-powershell + +bundle agent windows_optional_feature_enabled( feature_name ) +{ + methods: + "Enabled" usebundle => windows_optional_feature:_promise_state("${feature_name}", "Enabled"); +} + +bundle agent windows_optional_feature_disabled( feature_name ) +{ + methods: + "Disabled" usebundle => windows_optional_feature:_promise_state("${feature_name}", "Disabled"); +} + +bundle agent windows_optional_feature +{ + methods: + windows.!data:disable_windows_optional_feature_inventory:: + "windows_optional_feature:_inventory"; +} + +body file control +{ + namespace => "windows_optional_feature"; +} + +bundle agent _inventory +{ + vars: + windows:: + "cache_file" string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; + "command" string => "Get-WindowsOptionalFeature -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; + "csv" data => readcsv("${cache_file}"); + "i" slist => getindices("csv"); + "${this.namespace}[${i}]" string => "${csv[${i}][0]}:${csv[${i}][1]}", + meta => { "inventory", "attribute_name=Windows Optional Features" }; + + files: + "${cache_file}" + file_select => default:days_old(1), + delete => default:tidy; + + commands: + windows:: + "${command}" + if => not(fileexists("${cache_file}")), + contain => default:powershell; +} + +bundle agent _promise_state(feature_name, desired_state) +{ + vars: + "operation" string => ifelse(strcmp("${desired_state}", "Enabled"), "Enable", "Disable"); + classes: + windows:: + "state_not_ok" expression => returnszero( "if((Get-WindowsOptionalFeature -Online -FeatureName ${feature_name} | select-object -expandproperty state) -ne '${state_name}'){exit 0}else{exit 1}", "powershell"); + commands: + windows.state_not_ok:: +# -All enables parent features if need be + "${operation}-WindowsOptionalFeature -Online -FeatureName ${feature_name} -NoRestart -All" + contain => default:powershell, + classes => default:results("bundle", "state"); +# -NoRestart so that we don't wait forever for a Yes reply from a user +# then if we ran this command due to needing to, restart below. + + files: + windows.state_repaired:: + "${_inventory.cache_file}" + delete => default:tidy; +} From bd14392fe738ae0ba01ece24bb5759f960554c2a Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 3 Oct 2024 16:46:20 -0500 Subject: [PATCH 037/146] Added windows-openssh-server module --- cfbs.json | 9 +++++++ software/windows/windows-openssh-server.cf | 29 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 software/windows/windows-openssh-server.cf diff --git a/cfbs.json b/cfbs.json index 65a1690c..43cba024 100644 --- a/cfbs.json +++ b/cfbs.json @@ -289,6 +289,15 @@ "bundles windows_capability" ] }, + "windows-openssh-server": { + "description": "Optionally install Windows OpenSSH Server.", + "subdirectory": "software/windows", + "steps": [ + "copy windows-openssh-server.cf services/cfbs/software/windows/windows-openssh-server.cf", + "policy_files services/cfbs/software/windows/windows-openssh-server.cf", + "bundles windows_openssh_server" + ] + }, "windows-optional-feature": { "description": "Manage and inventory Windows Optional Features.", "subdirectory": "management/windows-optional-feature", diff --git a/software/windows/windows-openssh-server.cf b/software/windows/windows-openssh-server.cf new file mode 100644 index 00000000..ca8a02a4 --- /dev/null +++ b/software/windows/windows-openssh-server.cf @@ -0,0 +1,29 @@ +bundle agent windows_openssh_server +{ + methods: + data:openssh_server_installed:: + "openssh_server_installed"; +} + +bundle agent windows_openssh_server_installed +{ + methods: + windows:: + "Add OpenSSH.Server Capability" + # NOTE: this version "0.0.1.0" is not the actual version but rather a static number that is mysterious to me. + # the actual installed version will be the "latest" + usebundle => windows_capability_installed("OpenSSH.Server~~~~0.0.1.0"), + classes => classes_generic("openssh"); + + services: + windows.openssh_ok:: + "sshd" + service_policy => "start", + service_method => windows_openssh_service_method; +} + +body service_method windows_openssh_service_method +{ + service_type => "windows"; + service_autostart_policy => "boot_time"; +} From 58fe064b55e94f9d6e3ac791140f68f1ed0ad0d3 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Wed, 23 Oct 2024 11:43:01 -0500 Subject: [PATCH 038/146] Added setting of HOME environment variable for git promise type There was an issue where due to the lack of the $HOME env var the git commands were not picking up my .gitconfig and .git-credentials files. This change enables a way to setup authentication for private repositories. .gitconfig could be: [credential] helper = store and .git-credentials could be something of the form: https://:@gitlab.com --- promise-types/git/README.md | 27 +++++++++++++++++++++++++++ promise-types/git/git.py | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/promise-types/git/README.md b/promise-types/git/README.md index 529d7539..e493152a 100644 --- a/promise-types/git/README.md +++ b/promise-types/git/README.md @@ -60,6 +60,33 @@ bundle agent main } ``` +## Authentication + +This module will set the `HOME` environment variable if it is not set already based on the user running `cf-agent`, typically `root`. + +In order to add authentication you can use [gitcredentials](https://git-scm.com/docs/gitcredentials). + +An example of this usage would be to have two files in `$HOME`: `.gitconfig` and `.git-credentials`. + +- `.gitconfig` +```sh +[credential] + helper = store +``` + +- `.git-credentials` + +Using the `store` helper places the username and password in plaintext in `$HOME/.git-credentials`. + +Here is an explanation of that file [storage format](https://git-scm.com/docs/git-credential-store#_storage_format): + +> The `.git-credentials` file is stored in plaintext. Each credential is stored on its own line as a URL like: + +```text +https://user:pass@example.com +``` +> No other kinds of lines (e.g. empty lines or comment lines) are allowed in the file, even though some may be silently ignored. Do not view or edit the file with editors. + ## Authors This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 4123045c..b14354af 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -1,5 +1,6 @@ import os import subprocess +from pathlib import Path from typing import Dict, List, Optional @@ -255,6 +256,9 @@ def _git_envvars(self, model: object): env["GIT_SSH_COMMAND"] = model.ssh_executable if model.ssh_options: env["GIT_SSH_COMMAND"] += " " + model.ssh_options + if not 'HOME' in env: + # git should have a HOME env var to retrieve .gitconfig, .git-credentials, etc + env["HOME"] = str(Path.home()) return env From 3d6e49a68c4d1ca2ad59228d67a79e80f3416210 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Tue, 17 Dec 2024 12:40:34 -0600 Subject: [PATCH 039/146] Added inventory-windows-services module Companion to inventory-systemd for Windows. Ticket: ENT-12537 --- cfbs.json | 9 ++++++++ inventory/inventory-windows-services.cf | 28 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 inventory/inventory-windows-services.cf diff --git a/cfbs.json b/cfbs.json index 43cba024..21bbd610 100644 --- a/cfbs.json +++ b/cfbs.json @@ -135,6 +135,15 @@ "bundles inventory_unshadowed_users:main" ] }, + "inventory-windows-services": { + "description": "Inventory running Windows services.", + "subdirectory": "inventory", + "steps": [ + "copy inventory-windows-services.cf services/cfbs/inventory-windows-services/", + "policy_files services/cfbs/inventory-windows-services/", + "bundles inventory_windows_services_running" + ] + }, "library-for-promise-types-in-bash": { "description": "Library enabling promise types implemented in bash.", "subdirectory": "libraries/bash", diff --git a/inventory/inventory-windows-services.cf b/inventory/inventory-windows-services.cf new file mode 100644 index 00000000..ca7b25f8 --- /dev/null +++ b/inventory/inventory-windows-services.cf @@ -0,0 +1,28 @@ +bundle agent inventory_windows_services_running +# @brief Inventory Windows services that are running +# @inventory Windows services running +{ + vars: + windows:: + "_cache" string => "$(sys.statedir)/windows-services-running.json"; + + "_data" data => readjson("$(_cache)"); + "_indices" slist => getindices(_data); + + "i[$(_indices)]" + string => "$(_data[$(_indices)][DisplayName])", + meta => { "inventory", "attribute_name=Windows services running"}; + + + commands: + windows:: + "Get-Service | Where-Object {$_.Status -eq 'Running'} | ConvertTo-Json | Set-Content -Path '${_cache}'" + contain => powershell; + +} + +bundle agent __main__ +{ + methods: + "inventory_windows_services_running"; +} From 4fc92316b9deb16c8a6f80d0d0c1a80d3c372176 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 9 Jan 2025 13:47:19 +0100 Subject: [PATCH 040/146] Improved the README for the autorun module Signed-off-by: Ole Herman Schumacher Elgesem --- management/autorun/README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/management/autorun/README.md b/management/autorun/README.md index e96fc850..329f5fe7 100644 --- a/management/autorun/README.md +++ b/management/autorun/README.md @@ -1,3 +1,26 @@ -# Enable autorun +This module enables autorun functionality so you can add policy files to `services/autorun` and tag bundles with `autorun` causing them to be automatically discovered and run. -Simple module to enable autorun functionality, using def.json. +A policy file you add to `services/autorun` could look like this: + +```cfengine3 +bundle agent my_example +{ + meta: + "tags" + slist => { "autorun" }; + reports: + "Hello, world!" +} +``` + +With autorun enabled, the policy file would be parsed and the bundle evaluated without editing any of your existing files / policy. + +It uses the augments file to achieve this, it is the same as editing your `def.json` file like this: + +```json +{ + "classes": { + "services_autorun": ["any"] + } +} +``` From 1d721a6f1d02cf8adf45340b4ac6c8ce7974af70 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem <4048546+olehermanse@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:54:21 +0100 Subject: [PATCH 041/146] Code review suggestion Co-authored-by: Lars Erik Wik <53906608+larsewi@users.noreply.github.com> --- management/autorun/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/autorun/README.md b/management/autorun/README.md index 329f5fe7..5f143853 100644 --- a/management/autorun/README.md +++ b/management/autorun/README.md @@ -13,7 +13,7 @@ bundle agent my_example } ``` -With autorun enabled, the policy file would be parsed and the bundle evaluated without editing any of your existing files / policy. +With autorun enabled, the policy file would be parsed, and the bundle evaluated without editing any of your existing files / policy. It uses the augments file to achieve this, it is the same as editing your `def.json` file like this: From bdda7ef14890b554303fe35e061460b25997f6e9 Mon Sep 17 00:00:00 2001 From: jakub-nt <175944085+jakub-nt@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:58:23 +0100 Subject: [PATCH 042/146] Bump Ubuntu version in workflow Signed-off-by: jakub-nt <175944085+jakub-nt@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 820a3eef..6bf8f7bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,13 @@ on: jobs: unit_tests: name: Run Unit Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Install dependencies - run: sudo apt-get update -y && sudo apt-get install -y python + run: sudo apt-get update -y && sudo apt-get install -y python3 - name: Install cfbs run: pip install cfbs - name: Check the status with cfbs From 5ac6ddc255db0dbb2b32f8b039ee2940e014edcc Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Fri, 11 Apr 2025 16:21:45 -0500 Subject: [PATCH 043/146] Fixed wrong bundle name in windows-openssh-server module This caused openssh server to never be installed. --- software/windows/windows-openssh-server.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/windows/windows-openssh-server.cf b/software/windows/windows-openssh-server.cf index ca8a02a4..25f8f875 100644 --- a/software/windows/windows-openssh-server.cf +++ b/software/windows/windows-openssh-server.cf @@ -2,7 +2,7 @@ bundle agent windows_openssh_server { methods: data:openssh_server_installed:: - "openssh_server_installed"; + "windows_openssh_server_installed"; } bundle agent windows_openssh_server_installed From 3dc5aae8195e26d7bf9af1c1c3a6f191d35e7356 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Thu, 12 Jun 2025 16:46:25 +0200 Subject: [PATCH 044/146] Added symlinks promise type Ticket: CFE-4541 Signed-off-by: Victor Moene --- promise-types/symlinks/README.md | 53 +++++++++++++ promise-types/symlinks/enable.cf | 6 ++ promise-types/symlinks/example.cf | 15 ++++ promise-types/symlinks/symlinks.py | 117 +++++++++++++++++++++++++++++ promise-types/symlinks/test.cf | 116 ++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 promise-types/symlinks/README.md create mode 100644 promise-types/symlinks/enable.cf create mode 100644 promise-types/symlinks/example.cf create mode 100644 promise-types/symlinks/symlinks.py create mode 100644 promise-types/symlinks/test.cf diff --git a/promise-types/symlinks/README.md b/promise-types/symlinks/README.md new file mode 100644 index 00000000..66cb3c73 --- /dev/null +++ b/promise-types/symlinks/README.md @@ -0,0 +1,53 @@ +The `symlink` promise type enables concise policy for symbolic links. + +## Attributes + +| Name | Type | Description | Default | +|---------------|---------------|-----------------------------------------------------------|---------------| +| `file` | `string` | Path to file. Cannot be used together with `directory`. | - | +| `directory` | `string` | Path to directory. Cannot be used together with `file`. | - | + +## Examples + +To create a symlink to the directory `/tmp/my-dir` with the name `/tmp/my-link`, we can do: + +```cfengine3 +bundle agent main +{ + symlinks: + "/tmp/my-link" + directory => "/tmp/my-dir"; +} +``` + +In similar fashion, to create a symlink to the file `/tmp/my-dir` with the name `/tmp/my-link`, we can do: + +```cfengine3 +bundle agent main +{ + symlinks: + "/tmp/my-link" + file => "/tmp/my-file"; +} +``` + +If the path to the file/directory given in the promise is not an absolute, doesn't exist or its type doesn't correspond with the promise's attribute ("file" or "directory"), then the promise will fail. + +Trying to symlink to a file/directory where the link name is the same as an existing file/directory will also make the promise fail. + +Already exisiting symlinks with incorrect target will be corrected according to the policy. + + +## Authors + +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! + +## Contribute + +Feel free to open pull requests to expand this documentation, add features, or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. diff --git a/promise-types/symlinks/enable.cf b/promise-types/symlinks/enable.cf new file mode 100644 index 00000000..39b15f07 --- /dev/null +++ b/promise-types/symlinks/enable.cf @@ -0,0 +1,6 @@ +promise agent symlinks +# @brief Define symlinks promise type +{ + path => "$(sys.workdir)/modules/promises/symlinks.py"; + interpreter => "/usr/bin/python3"; +} diff --git a/promise-types/symlinks/example.cf b/promise-types/symlinks/example.cf new file mode 100644 index 00000000..0fafc2be --- /dev/null +++ b/promise-types/symlinks/example.cf @@ -0,0 +1,15 @@ +promise agent symlinks +# @brief Define symlinks promise type +{ + path => "$(sys.workdir)/modules/promises/symlinks.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent main +{ + symlinks: + "/tmp/myfilelink" + file => "tmp/myfile"; + "/tmp/mydirlink" + directory => "tmp/mydirectory"; +} diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py new file mode 100644 index 00000000..2803c1c9 --- /dev/null +++ b/promise-types/symlinks/symlinks.py @@ -0,0 +1,117 @@ +import os +from cfengine import PromiseModule, ValidationError, Result + + +class SymlinksPromiseTypeModule(PromiseModule): + + def __init__(self, **kwargs): + super(SymlinksPromiseTypeModule, self).__init__( + name="symlinks_promise_module", + version="0.0.1", + **kwargs, + ) + + def is_absolute_dir(v): + if not os.path.isabs(v): + raise ValidationError("must be an absolute path, not '{v}'".format(v=v)) + if not os.path.exists(v): + raise ValidationError("directory must exists") + if not os.path.isdir(v): + raise ValidationError("must be a dir") + + def is_absolute_file(v): + if not os.path.isabs(v): + raise ValidationError("must be an absolute path, not '{v}'".format(v=v)) + if not os.path.exists(v): + raise ValidationError("file must exists") + if not os.path.isfile(v): + raise ValidationError("must be a file") + + self.add_attribute("directory", str, validator=is_absolute_dir) + self.add_attribute("file", str, validator=is_absolute_file) + + def validate_promise(self, promiser, attributes, metadata): + model = self.create_attribute_object(promiser, attributes) + + if not model.file and not model.directory: + raise ValidationError("missing 'file' or 'directory' attribute") + + if model.file and model.directory: + raise ValidationError("must specify either 'file' or 'directory', not both") + + def evaluate_promise(self, promiser, attributes, metadata): + model = self.create_attribute_object(promiser, attributes) + link_target = model.file if model.file else model.directory + + try: + os.symlink(link_target, promiser, target_is_directory=bool(model.directory)) + self.log_info("Created symlink '{}' -> '{}'".format(promiser, link_target)) + return Result.REPAIRED + except FileExistsError: + + if not os.path.islink(promiser): + self.log_error("Symlink '{}' is already a path".format(promiser)) + return Result.NOT_KEPT + + if os.path.realpath(promiser) != link_target: + self.log_warning( + "Symlink '{}' already exists but has wrong target '{}'".format( + promiser, os.path.realpath(promiser) + ) + ) + try: + os.unlink(promiser) + except FileNotFoundError: + self.log_error( + "'{}' is already unlinked from its old target".format(promiser) + ) + return Result.NOT_KEPT + except Exception: + self.log_error( + "'{}' has wrong target but couldn't be unlinked: {}".format( + promiser, e + ) + ) + return Result.NOT_KEPT + try: + os.symlink( + link_target, promiser, target_is_directory=bool(model.directory) + ) + except FileExistsError: + self.log_error( + "Couldn't symlink '{}' to '{}'. A symlink already exists".format( + link_target, promiser + ) + ) + return Result.NOT_KEPT + except FileNotFoundError: + self.log_error("'{}' doesn't exist".format(link_target)) + return Result.NOT_KEPT + except Exception as e: + self.log_error( + "Couldn't symlink '{}' to '{}': {}".format( + link_target, promiser, e + ) + ) + return Result.NOT_KEPT + + self.log_info( + "Corrected symlink '{}' -> '{}'".format(promiser, link_target) + ) + return Result.REPAIRED + + return Result.KEPT + + except FileNotFoundError: + self.log_error("'{}' doesn't exist".format(promiser)) + return Result.NOT_KEPT + + except Exception as e: + self.log_error( + "Couldn't symlink '{}' to '{}': {}".format(link_target, promiser, e) + ) + return Result.NOT_KEPT + + +if __name__ == "__main__": + SymlinksPromiseTypeModule().start() diff --git a/promise-types/symlinks/test.cf b/promise-types/symlinks/test.cf new file mode 100644 index 00000000..468ac0d9 --- /dev/null +++ b/promise-types/symlinks/test.cf @@ -0,0 +1,116 @@ +body common control +{ + inputs => { "$(sys.libdir)/stdlib.cf" }; + version => "1.0"; + bundlesequence => { "init", "test", "check", "cleanup"}; +} + +####################################################### + +bundle agent init +{ + files: + "/tmp/my-file" + create => "true"; + "/tmp/my-dir/." + create => "true"; + "/tmp/other-dir/." + create => "true"; + "/tmp/replaced-link" + link_from => ln_s("/tmp/other-dir"); + "/tmp/already-existing-link" + link_from => ln_s("/tmp/other-dir"); +} + +####################################################### + +promise agent symlinks +{ + path => "$(this.promise_dirname)/symlinks.py"; + interpreter => "/usr/bin/python3"; +} + +body classes outcome(arg) +{ + promise_kept => { "$(arg)_kept" }; + promise_repaired => { "$(arg)_repaired" }; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-4541" } + string => "Test the symlinks promise module"; + + symlinks: + "/tmp/file-link" + file => "/tmp/my-file", + classes => outcome("created_file"); + "/tmp/dir-link" + directory => "/tmp/my-dir", + classes => outcome("created_dir"); + "/tmp/replaced-link" + directory => "/tmp/my-dir", + classes => outcome("corrected"); + "/tmp/already-existing-link" + directory => "/tmp/other-dir", + classes => outcome("didnothing"); + +} + +####################################################### + +bundle agent check +{ + + vars: + "my_file_stat" + string => filestat("/tmp/file-link", "linktarget"); + "my_dir_stat" + string => filestat("/tmp/dir-link", "linktarget"); + "replaced_link_stat" + string => filestat("/tmp/replaced-link", "linktarget"); + "already_existing_link_stat" + string => filestat("/tmp/already-existing-link", "linktarget"); + + classes: + "ok" + expression => and ( + strcmp("$(my_file_stat)", "/tmp/my-file"), + strcmp("$(my_dir_stat)", "/tmp/my-dir"), + strcmp("$(replaced_link_stat)", "/tmp/my-dir"), + strcmp("$(already_existing_link_stat)", "/tmp/other-dir"), + "created_file_repaired", + "created_dir_repaired", + "corrected_repaired", + "didnothing_kept" + ); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + +} + +# ####################################################### + +bundle agent cleanup +{ + files: + "/tmp/file-link" + delete => tidy; + "/tmp/dir-link" + delete => tidy; + "/tmp/my-file" + delete => tidy; + "/tmp/my-dir/." + delete => tidy; + "/tmp/other-dir/." + delete => tidy; + "/tmp/replaced-link" + delete => tidy; + "/tmp/already-existing-link" + delete => tidy; +} From 40b990367b528972c41f826fa554b8dd4a415f47 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 24 Jun 2025 16:44:42 +0200 Subject: [PATCH 045/146] Added json promise type Signed-off-by: Victor Moene --- promise-types/json/README.md | 155 +++++++++++++++++ promise-types/json/enable.cf | 6 + promise-types/json/example.cf | 13 ++ promise-types/json/json_promise_type.py | 152 +++++++++++++++++ promise-types/json/test.cf | 161 ++++++++++++++++++ .../json/tests/to_append.expected.json | 22 +++ .../json/tests/to_modify.expected.json | 3 + promise-types/json/tests/to_modify.start.json | 3 + .../json/tests/to_overwrite.expected.json | 7 + .../json/tests/to_overwrite.start.json | 3 + 10 files changed, 525 insertions(+) create mode 100644 promise-types/json/README.md create mode 100644 promise-types/json/enable.cf create mode 100644 promise-types/json/example.cf create mode 100644 promise-types/json/json_promise_type.py create mode 100644 promise-types/json/test.cf create mode 100644 promise-types/json/tests/to_append.expected.json create mode 100644 promise-types/json/tests/to_modify.expected.json create mode 100644 promise-types/json/tests/to_modify.start.json create mode 100644 promise-types/json/tests/to_overwrite.expected.json create mode 100644 promise-types/json/tests/to_overwrite.start.json diff --git a/promise-types/json/README.md b/promise-types/json/README.md new file mode 100644 index 00000000..5d69c70c --- /dev/null +++ b/promise-types/json/README.md @@ -0,0 +1,155 @@ +Promise type for manipulating `json` files + +## Attributes + +| Name | Type | Description | +|---------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| `object` | `data container` | json object type. It can also be json arrays | +| `array` | `slist`, `rlist`, `ilist`, `data array` | json array type. `slist`, `rlist` and `ilist` will only create string arrays. To create array of other types, use `data array` | +| `string` | `string` | json string type | +| `number` | `real`, `int` | json number type | +| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | + +## Examples + +### Write to a whole file + +To write to a json file, you can do: + +```cfengine3 +bundle agent main +{ + json: + "/tmp/newfile.json" + array => '["hello", "world"]'; +} +``` + +The resulting `/tmp/newfile.json` will only contain the array: + +```json +["hello", "world"] +``` + +If the `/tmp/newfile.json` doesn't exist, it will be created. If it exists and contains some data, they will be overwritten. + +### Write to a specific field + +Given a json file `/tmp/oldfile.json`, +```json +{ + "foo": "bar" +} +``` + +we can modify/append a field by doing: + +```cfengine3 +bundle agent main +{ + json: + "/tmp/oldfile.json:greeting" + array => '["hello", "world"]'; +} +``` + +And the content of `/tmp/oldfile.json` will become: + +```json +{ + "foo": "bar", + "greeting": ["hello", "world"] +} +``` + +If the field doesn't exist, it is appended. If it already exists, its data will be overwritten. + +### Writing types + +In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data container` type in the policy. + +To see what happens if we use + +```cfengine3 +bundle agent main +{ + vars: + "json_data" + data => '[1.2, true, "hello!"]'; + + "real_list" + rlist => {"1.2", "2.3"}; + "bool_list" + slist => {"true", "false"}; + + json: + "/tmp/example_1.json:json_data" + array => "$(json_data)"; + + "/tmp/example_2.json:real_list" + array => "$(real_list)"; + "/tmp/example_2.json:bool_list" + array => "$(bool_list)"; +} +``` + +We can compare the content of `/tmp/example_1.json` and `/tmp/example_2.json`: + +```json +{ + "json_data": [1.2, true, "hello!"] +} +``` + +```json +{ + "real_list": ["1.2", "2.3"], + "bool_list": ["true", "false"] +} +``` + +As we can see, using slist, rlist or ilist to write arrays will always result in array of strings. If we want more complex arrays using containg number, true, false or null, then we need to use the `data container` type. + +## Not implemented yet + +The copy attribute allows to copy the content of a json file into another json file. For example, `/tmp/oldfile.json` contains the following: + +```json +{ + "hello": "world" +} +``` + +We can copy it into the `/tmp/newfile.json` in the field `"oldfile"` by doing: + +```cfengine3 +bundle agent main +{ + json: + "/tmp/newfile.json:oldfile" + copy => "/tmp/oldfile.json"; +} +``` + +```json +{ + "oldfile": { + "hello": "world" + } +} +``` + + +## Authors + +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! + +## Contribute + +Feel free to open pull requests to expand this documentation, add features, or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. diff --git a/promise-types/json/enable.cf b/promise-types/json/enable.cf new file mode 100644 index 00000000..cccfe944 --- /dev/null +++ b/promise-types/json/enable.cf @@ -0,0 +1,6 @@ +promise agent json +# @brief Define json promise type +{ + path => "$(sys.workdir)/modules/promises/json_promise_type.py"; + interpreter => "/usr/bin/python3"; +} diff --git a/promise-types/json/example.cf b/promise-types/json/example.cf new file mode 100644 index 00000000..91d3a84f --- /dev/null +++ b/promise-types/json/example.cf @@ -0,0 +1,13 @@ +promise agent json +# @brief Define json promise type +{ + path => "$(sys.workdir)/modules/promises/json_promise_type.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent main +{ + json: + "/tmp/myusers.json:name" + string => "John" +} diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py new file mode 100644 index 00000000..95f1969b --- /dev/null +++ b/promise-types/json/json_promise_type.py @@ -0,0 +1,152 @@ +import os +import json + +from cfengine import PromiseModule, ValidationError, Result, AttributeObject + + +def is_number(num): + try: + float(num) + return True + except ValueError: + return False + + +def is_json_serializable(string): + try: + json.loads(string) + return True + except json.JSONDecodeError: + return False + + +class JsonPromiseTypeModule(PromiseModule): + + def __init__(self, **kwargs): + super(JsonPromiseTypeModule, self).__init__( + name="json_promise_module", version="0.0.1", **kwargs + ) + + self.types = ["object", "array", "string", "number", "primitive"] + self.valid_attributes = ( + self.types + ) # for now, the only valid attributes are the types. + + def create_attribute_object(self, attributes): + data = {t: None for t in self.valid_attributes} + for attr, val in attributes.items(): + data[attr] = val + return AttributeObject(data) + + def validate_promise(self, promiser, attributes, metadata): + + for attr in attributes: + if attr not in self.valid_attributes: + raise ValidationError("Unknown attribute '{}'".format(attr)) + + present_types = [t for t in self.types if t in attributes] + if present_types == 0: + raise ValidationError( + "The promiser '{}' is missing a type attribute. The possible types are {}".format( + promiser, str(self.types) + ) + ) + elif len(present_types) > 1: + raise ValidationError( + "The attributes {} cannot be together".format(str(self.types)) + ) + + filename, _, _ = promiser.partition(":") + if os.path.exists(filename) and not os.path.isfile(filename): + raise ValidationError( + "'{}' already exists and is not a file".format(filename) + ) + + if not filename.endswith(".json"): + raise ValidationError("'{}' is not a json file") + + model = self.create_attribute_object(attributes) + if ( + model.object + and isinstance(model.object, str) + and not is_json_serializable(model.object) + ): + raise ValidationError( + "'{}' is not a valid data container".format(model.object) + ) + + if model.array: + if isinstance(model.array, str): + if not is_json_serializable(model.array): + raise ValidationError( + "'{}' is not a valid list".format(model.array) + ) + + if not isinstance(json.loads(model.array), list): + raise ValidationError( + "'{}' is not a valid data".format(model.array) + ) + + elif not isinstance(model.array, list): + raise ValidationError( + "'{}' is not a valid data array".format(model.array) + ) + + if model.number and not is_number(model.number): + raise ValidationError( + "'{}' is not a valid int or real".format(model.number) + ) + + if model.primitive and model.primitive not in ["true", "false", "null"]: + raise ValidationError( + "expected 'true', 'false' or 'null' but got '{}".format(model.primitive) + ) + + def evaluate_promise(self, promiser, attributes, metadata): + model = self.create_attribute_object(attributes) + filename, _, field = promiser.partition(":") + + # type conversion + + datatype = next(t for t in self.types if t in attributes) + + match datatype: + case "object" | "array": + data = ( + json.loads(attributes[datatype]) + if isinstance(attributes[datatype], str) + else attributes[datatype] + ) + case "number": + data = float(model.number) if "." in model.number else int(model.number) + case "primitive": + data = None if model.primitive == "null" else model.primitive == "true" + case _: # strings + data = attributes[datatype] + + # json manipulation + + try: + with open(filename, "r+") as f: + content = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + content = {} + + if field: + if field in content and content[field] == data: + Result.KEPT + content[field] = data + else: + if content == data: + Result.KEPT + content = data + + with open(filename, "w") as f: + json.dump(content, f, indent=4) + + self.log_info("Updated '{}'".format(filename)) + Result.REPAIRED + + +if __name__ == "__main__": + JsonPromiseTypeModule().start() diff --git a/promise-types/json/test.cf b/promise-types/json/test.cf new file mode 100644 index 00000000..ecb5fcac --- /dev/null +++ b/promise-types/json/test.cf @@ -0,0 +1,161 @@ +body common control +{ + inputs => { "$(sys.libdir)/stdlib.cf" }; + version => "1.0"; + bundlesequence => { "init", "test", "check", "cleanup"}; +} + +####################################################### + +bundle agent init +{ + vars: + "to_overwrite" + data => readjson("$(this.promise_dirname)/tests/to_overwrite.start.json", 100k); + "to_modify" + data => readjson("$(this.promise_dirname)/tests/to_modify.start.json", 100k); + + files: + "$(this.promise_dirname)/tests/to_overwrite.json" + create => "true", + content => "$(to_overwrite)"; + "$(this.promise_dirname)/tests/to_modify.json" + create => "true", + content => "$(to_modify)"; + "$(this.promise_dirname)/tests/to_append_1.json" + create => "true"; +} + +####################################################### + +promise agent json +{ + path => "$(this.promise_dirname)/json_promise_type.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent test +{ + vars: + "objects" + data => '{ "bar": [1, 2, 3] }'; + "int_arrays" + data => '[1,2,3]'; + "arrays" + ilist => { "1", "2" }; # slist == rlist == ilist -> lists of string + "numbers" + int => "1"; + "strings" + string => "hello"; + "bools" + string => "true"; + "nulls" + string => "null"; + + + json: + "$(this.promise_dirname)/tests/to_overwrite.json" + object => "@(objects)"; + + "$(this.promise_dirname)/tests/to_modify.json:Hello" + string => "$(strings)"; + + "$(this.promise_dirname)/tests/to_append_1.json:a" + object => "@(objects)"; + "$(this.promise_dirname)/tests/to_append_1.json:b" + object => "@(int_arrays)"; + "$(this.promise_dirname)/tests/to_append_1.json:c" + array => "@(int_arrays)"; + "$(this.promise_dirname)/tests/to_append_1.json:d" + string => "$(strings)"; + "$(this.promise_dirname)/tests/to_append_1.json:e" + number => "$(numbers)"; + "$(this.promise_dirname)/tests/to_append_1.json:f" + primitive => "$(bools)"; + "$(this.promise_dirname)/tests/to_append_1.json:g" + primitive => "$(nulls)"; + + "$(this.promise_dirname)/tests/to_append_2.json:a" + object => '{ "bar": [1, 2, 3] }'; + "$(this.promise_dirname)/tests/to_append_2.json:b" + object => '[1,2,3]'; + "$(this.promise_dirname)/tests/to_append_2.json:c" + array => '[1,2,3]'; + "$(this.promise_dirname)/tests/to_append_2.json:d" + string => "hello"; + "$(this.promise_dirname)/tests/to_append_2.json:e" + number => "1"; + "$(this.promise_dirname)/tests/to_append_2.json:f" + primitive => "true"; + "$(this.promise_dirname)/tests/to_append_2.json:g" + primitive => "null"; + +} + +####################################################### + +bundle agent check +{ + vars: + "to_overwrite_content" + data => readjson("$(this.promise_dirname)/tests/to_overwrite.json", 100k); + "to_modify_content" + data => readjson("$(this.promise_dirname)/tests/to_modify.json", 100k); + "to_append_1_content" + data => readjson("$(this.promise_dirname)/tests/to_append_1.json", 100k); + "to_append_2_content" + data => readjson("$(this.promise_dirname)/tests/to_append_2.json", 100k); + + "to_overwrite_content_true" + data => readjson("$(this.promise_dirname)/tests/to_overwrite.expected.json", 100k); + "to_modify_content_true" + data => readjson("$(this.promise_dirname)/tests/to_modify.expected.json", 100k); + "to_append_content_true" + data => readjson("$(this.promise_dirname)/tests/to_append.expected.json", 100k); + + "to_overwrite_content_indices" + slist => getindices("to_overwrite_content"); + "to_modify_content_indices" + slist => getindices("to_modify_content"); + "to_append_1_content_indices" + slist => getindices("to_append_1_content"); + "to_append_2_content_indices" + slist => getindices("to_append_2_content"); + + "to_overwrite_content_true_indices" + slist => getindices("to_overwrite_content_true"); + "to_modify_content_true_indices" + slist => getindices("to_modify_content_true"); + "to_append_content_true_indices" + slist => getindices("to_append_content_true"); + + classes: + "ok" + expression => and ( + strcmp("$(to_overwrite_content[$(to_overwrite_content_indices)])", "$(to_overwrite_content_true[$(to_overwrite_content_true_indices)])"), + strcmp("$(to_modify_content[$(to_modify_content_indices)])", "$(to_modify_content_true[$(to_modify_content_true_indices)])"), + strcmp("$(to_append_1_content[$(to_append_1_content_indices)])", "$(to_append_2_content[$(to_append_2_content_indices)])"), + strcmp("$(to_append_1_content[$(to_append_1_content_indices)])", "$(to_append_content_true[$(to_append_content_true_indices)])") + ); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} + +# ####################################################### + +bundle agent cleanup +{ + files: + "$(this.promise_dirname)/tests/to_overwrite.json" + delete => tidy; + "$(this.promise_dirname)/tests/to_modify.json" + delete => tidy; + "$(this.promise_dirname)/tests/to_append_1.json" + delete => tidy; + "$(this.promise_dirname)/tests/to_append_2.json" + delete => tidy; +} diff --git a/promise-types/json/tests/to_append.expected.json b/promise-types/json/tests/to_append.expected.json new file mode 100644 index 00000000..8c84adc3 --- /dev/null +++ b/promise-types/json/tests/to_append.expected.json @@ -0,0 +1,22 @@ +{ + "a": { + "bar": [ + 1, + 2, + 3 + ] + }, + "b": [ + 1, + 2, + 3 + ], + "c": [ + "1", + "2" + ], + "d": "hello", + "e": 1, + "f": true, + "g": null +} diff --git a/promise-types/json/tests/to_modify.expected.json b/promise-types/json/tests/to_modify.expected.json new file mode 100644 index 00000000..1ded6095 --- /dev/null +++ b/promise-types/json/tests/to_modify.expected.json @@ -0,0 +1,3 @@ +{ + "Hello": "hello" +} diff --git a/promise-types/json/tests/to_modify.start.json b/promise-types/json/tests/to_modify.start.json new file mode 100644 index 00000000..02bb47e1 --- /dev/null +++ b/promise-types/json/tests/to_modify.start.json @@ -0,0 +1,3 @@ +{ + "Hello": "World" +} diff --git a/promise-types/json/tests/to_overwrite.expected.json b/promise-types/json/tests/to_overwrite.expected.json new file mode 100644 index 00000000..7117d3f7 --- /dev/null +++ b/promise-types/json/tests/to_overwrite.expected.json @@ -0,0 +1,7 @@ +{ + "bar": [ + 1, + 2, + 3 + ] +} diff --git a/promise-types/json/tests/to_overwrite.start.json b/promise-types/json/tests/to_overwrite.start.json new file mode 100644 index 00000000..02bb47e1 --- /dev/null +++ b/promise-types/json/tests/to_overwrite.start.json @@ -0,0 +1,3 @@ +{ + "Hello": "World" +} From 0f4660c61e4f5c8425a411b154fd24e9a0e125a1 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 30 Jun 2025 11:03:21 +0200 Subject: [PATCH 046/146] Fixed json promise type Signed-off-by: Victor Moene --- promise-types/json/README.md | 12 ++-- promise-types/json/json_promise_type.py | 84 +++++++++++++++---------- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/promise-types/json/README.md b/promise-types/json/README.md index 5d69c70c..1e4c8208 100644 --- a/promise-types/json/README.md +++ b/promise-types/json/README.md @@ -5,7 +5,7 @@ Promise type for manipulating `json` files | Name | Type | Description | |---------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| | `object` | `data container` | json object type. It can also be json arrays | -| `array` | `slist`, `rlist`, `ilist`, `data array` | json array type. `slist`, `rlist` and `ilist` will only create string arrays. To create array of other types, use `data array` | +| `array` | `data array` | json array type | | `string` | `string` | json string type | | `number` | `real`, `int` | json number type | | `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | @@ -64,9 +64,9 @@ And the content of `/tmp/oldfile.json` will become: If the field doesn't exist, it is appended. If it already exists, its data will be overwritten. -### Writing types +### Writing arrays -In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data container` type in the policy. +In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data` type in the policy. To see what happens if we use @@ -84,12 +84,12 @@ bundle agent main json: "/tmp/example_1.json:json_data" - array => "$(json_data)"; + array => "@(json_data)"; "/tmp/example_2.json:real_list" - array => "$(real_list)"; + array => "@(real_list)"; "/tmp/example_2.json:bool_list" - array => "$(bool_list)"; + array => "@(bool_list)"; } ``` diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 95f1969b..2d945d75 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -1,5 +1,7 @@ import os import json +import tempfile +import shutil from cfengine import PromiseModule, ValidationError, Result, AttributeObject @@ -48,22 +50,23 @@ def validate_promise(self, promiser, attributes, metadata): if present_types == 0: raise ValidationError( "The promiser '{}' is missing a type attribute. The possible types are {}".format( - promiser, str(self.types) + promiser, ", ".join(["'{}'".format(t) for t in self.types]) ) ) elif len(present_types) > 1: raise ValidationError( - "The attributes {} cannot be together".format(str(self.types)) + "The attributes {} cannot be together".format( + ", ".join(["'{}'".format(t) for t in self.types]) + ) ) - filename, _, _ = promiser.partition(":") - if os.path.exists(filename) and not os.path.isfile(filename): - raise ValidationError( - "'{}' already exists and is not a file".format(filename) - ) + filename, colon, field = promiser.partition(":") - if not filename.endswith(".json"): - raise ValidationError("'{}' is not a json file") + if not filename: + raise ValidationError("Invalid syntax: missing file name") + + if colon and not field: + raise ValidationError("Invalid syntax: field specified but empty") model = self.create_attribute_object(attributes) if ( @@ -77,14 +80,16 @@ def validate_promise(self, promiser, attributes, metadata): if model.array: if isinstance(model.array, str): - if not is_json_serializable(model.array): + try: + array = json.loads(model.array) + + except: raise ValidationError( - "'{}' is not a valid list".format(model.array) + "'{}' cannot be serialized to a json array".format(model.array) ) - - if not isinstance(json.loads(model.array), list): + if not isinstance(array, list): raise ValidationError( - "'{}' is not a valid data".format(model.array) + "'{}' is not a valid data array".format(model.array) ) elif not isinstance(model.array, list): @@ -106,23 +111,18 @@ def evaluate_promise(self, promiser, attributes, metadata): model = self.create_attribute_object(attributes) filename, _, field = promiser.partition(":") + if os.path.exists(filename) and not os.path.isfile(filename): + self.log_error("'{}' already exists and is not a regular file".format(filename)) + return Result.NOT_KEPT + # type conversion datatype = next(t for t in self.types if t in attributes) - match datatype: - case "object" | "array": - data = ( - json.loads(attributes[datatype]) - if isinstance(attributes[datatype], str) - else attributes[datatype] - ) - case "number": - data = float(model.number) if "." in model.number else int(model.number) - case "primitive": - data = None if model.primitive == "null" else model.primitive == "true" - case _: # strings - data = attributes[datatype] + if isinstance(attributes[datatype], str) and not model.string: + data = json.loads(attributes[datatype]) + else: + data = attributes[datatype] # json manipulation @@ -131,21 +131,41 @@ def evaluate_promise(self, promiser, attributes, metadata): content = json.load(f) except (FileNotFoundError, json.JSONDecodeError): content = {} + except Exception as e: + self.log_error("Failed to read '{}': {}".format(filename, e)) + return Result.NOT_KEPT if field: + if not isinstance(content, dict): + content = {} + self.log_warning( + "Tried to access '{}' in '{}' when the content is not subscriptable. Overwriting the file...".format( + field, filename + ) + ) + if field in content and content[field] == data: - Result.KEPT + self.log_info("'{}' is already up to date") + return Result.KEPT content[field] = data else: if content == data: - Result.KEPT + self.log_info("'{}' is already up to date") + return Result.KEPT content = data - with open(filename, "w") as f: - json.dump(content, f, indent=4) + fd, tmp = tempfile.mkstemp() + json_bytes = json.dumps(content, indent=4).encode("utf-8") + written = os.write(fd, json_bytes) + os.close(fd) + shutil.move(tmp, filename) + + if (written != len(json_bytes)): + self.log_error("Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format(filename, written, len(json_bytes))) + return Result.NOT_KEPT self.log_info("Updated '{}'".format(filename)) - Result.REPAIRED + return Result.REPAIRED if __name__ == "__main__": From 0e69c388715d543714966dce9314e6aa226b6a0e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 13:40:17 +0200 Subject: [PATCH 047/146] JSON promise type: Reformatted README with Prettier Fixed some small whitespace / formatting mistakes Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/promise-types/json/README.md b/promise-types/json/README.md index 1e4c8208..035e298f 100644 --- a/promise-types/json/README.md +++ b/promise-types/json/README.md @@ -2,13 +2,13 @@ Promise type for manipulating `json` files ## Attributes -| Name | Type | Description | -|---------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| `object` | `data container` | json object type. It can also be json arrays | -| `array` | `data array` | json array type | -| `string` | `string` | json string type | -| `number` | `real`, `int` | json number type | -| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | +| Name | Type | Description | +| ----------- | ---------------- | ----------------------------------------------------------------------------- | +| `object` | `data container` | json object type. It can also be json arrays | +| `array` | `data array` | json array type | +| `string` | `string` | json string type | +| `number` | `real`, `int` | json number type | +| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | ## Examples @@ -35,7 +35,8 @@ If the `/tmp/newfile.json` doesn't exist, it will be created. If it exists and c ### Write to a specific field -Given a json file `/tmp/oldfile.json`, +Given a json file `/tmp/oldfile.json`, + ```json { "foo": "bar" @@ -68,15 +69,15 @@ If the field doesn't exist, it is appended. If it already exists, its data will In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data` type in the policy. -To see what happens if we use +To see what happens if we use ```cfengine3 bundle agent main { - vars: + vars: "json_data" data => '[1.2, true, "hello!"]'; - + "real_list" rlist => {"1.2", "2.3"}; "bool_list" @@ -85,7 +86,7 @@ bundle agent main json: "/tmp/example_1.json:json_data" array => "@(json_data)"; - + "/tmp/example_2.json:real_list" array => "@(real_list)"; "/tmp/example_2.json:bool_list" @@ -116,7 +117,7 @@ The copy attribute allows to copy the content of a json file into another json f ```json { - "hello": "world" + "hello": "world" } ``` @@ -139,7 +140,6 @@ bundle agent main } ``` - ## Authors This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. From 08cd750994c5bb06e248de8eb0f3762d48fc84a9 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 15:07:05 +0200 Subject: [PATCH 048/146] promise-type-json: Formatted with black Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/json_promise_type.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 2d945d75..918f6493 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -112,7 +112,9 @@ def evaluate_promise(self, promiser, attributes, metadata): filename, _, field = promiser.partition(":") if os.path.exists(filename) and not os.path.isfile(filename): - self.log_error("'{}' already exists and is not a regular file".format(filename)) + self.log_error( + "'{}' already exists and is not a regular file".format(filename) + ) return Result.NOT_KEPT # type conversion @@ -160,8 +162,12 @@ def evaluate_promise(self, promiser, attributes, metadata): os.close(fd) shutil.move(tmp, filename) - if (written != len(json_bytes)): - self.log_error("Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format(filename, written, len(json_bytes))) + if written != len(json_bytes): + self.log_error( + "Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format( + filename, written, len(json_bytes) + ) + ) return Result.NOT_KEPT self.log_info("Updated '{}'".format(filename)) From 7d67b93c331ab29340b1351c36e283891feba830 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 15:55:20 +0200 Subject: [PATCH 049/146] promise-type-json: Fixed already up to date log message Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/json_promise_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 918f6493..32bb4c7d 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -147,12 +147,12 @@ def evaluate_promise(self, promiser, attributes, metadata): ) if field in content and content[field] == data: - self.log_info("'{}' is already up to date") + self.log_info("'{}' is already up to date".format(promiser)) return Result.KEPT content[field] = data else: if content == data: - self.log_info("'{}' is already up to date") + self.log_info("'{}' is already up to date".format(promiser)) return Result.KEPT content = data From c0adf955f62ea3bfe4733c753e0dac8bc6342de5 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:01:09 +0200 Subject: [PATCH 050/146] CFEngine module library: Improvements to error handling Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index e1dcf2a9..a80d6f61 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -380,6 +380,8 @@ def _handle_evaluate(self, promiser, attributes, request): try: results = self.evaluate_promise(promiser, attributes, metadata) + assert results is not None # Most likely someone forgot to return something + # evaluate_promise should return either a result or a (result, result_classes) pair if type(results) == str: self._result = results @@ -389,7 +391,9 @@ def _handle_evaluate(self, promiser, attributes, request): self._result_classes = results[1] except Exception as e: self.log_critical( - "{error_type}: {error}".format(error_type=type(e).__name__, error=e) + "{error_type}: {error} (Bug in python promise type module, run with --debug for traceback)".format( + error_type=type(e).__name__, error=e + ) ) self._add_traceback_to_response() self._result = Result.ERROR From cb2d8a914fea38175516c9b418879a6748869887 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:01:37 +0200 Subject: [PATCH 051/146] CFEngine module library: Formatted with black Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index a80d6f61..e0696013 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -49,8 +49,9 @@ def _should_send_log(level_set, msg_level): # for auditing/changelog and all modules are required to send info: messages # for all REPAIRED promises. A similar logic applies to errors and warnings, # IOW, anything at or above the info level. - return ((_LOG_LEVELS[msg_level] <= _LOG_LEVELS["info"]) or - (_LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set])) + return (_LOG_LEVELS[msg_level] <= _LOG_LEVELS["info"]) or ( + _LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set] + ) def _cfengine_type(typing): @@ -71,12 +72,14 @@ class AttributeObject(object): def __init__(self, d): for key, value in d.items(): setattr(self, key, value) + def __repr__(self): return "{}({})".format( self.__class__.__qualname__, - ", ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()) + ", ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), ) + class ValidationError(Exception): def __init__(self, message): self.message = message From 509d979730e77e8b8e54a8908825230d03abfb76 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:03:18 +0200 Subject: [PATCH 052/146] CFEngine module library: Added docstring Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index e0696013..7e07e0f9 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -1,3 +1,12 @@ +""" +CFEngine module library + +This library can be used to implement CFEngine modules in python. +Currently, this is for implementing custom promise types, +but it might be expanded to other types of modules in the future, +for example custom functions. +""" + import sys import json import traceback From d7d3087dab70b5f676b71c92e4819d8ee46bde80 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:38:29 +0200 Subject: [PATCH 053/146] Renamed cfengine.py to cfengine_module_library.py Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 5 ++++- examples/git-using-lib/git_using_lib.py | 2 +- examples/gpg/gpg.py | 3 ++- examples/rss/rss.py | 2 +- examples/site-up/site_up.py | 2 +- libraries/python/{cfengine.py => cfengine_module_library.py} | 0 promise-types/ansible/ansible_promise.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 2 +- promise-types/iptables/iptables.py | 2 +- promise-types/json/json_promise_type.py | 2 +- promise-types/symlinks/symlinks.py | 2 +- promise-types/systemd/systemd.py | 2 +- 14 files changed, 17 insertions(+), 13 deletions(-) rename libraries/python/{cfengine.py => cfengine_module_library.py} (100%) diff --git a/cfbs.json b/cfbs.json index 21bbd610..e2641c01 100644 --- a/cfbs.json +++ b/cfbs.json @@ -152,7 +152,10 @@ "library-for-promise-types-in-python": { "description": "Library enabling promise types implemented in python.", "subdirectory": "libraries/python", - "steps": ["copy cfengine.py modules/promises/"] + "steps": [ + "copy cfengine_module_library.py modules/promises/cfengine_module_library.py", + "copy cfengine_module_library.py modules/promises/cfengine.py" + ] }, "maintainers-in-motd": { "description": "Add maintainer and purpose information from CMDB to /etc/motd", diff --git a/examples/git-using-lib/git_using_lib.py b/examples/git-using-lib/git_using_lib.py index 7e1c8b7f..07747b18 100644 --- a/examples/git-using-lib/git_using_lib.py +++ b/examples/git-using-lib/git_using_lib.py @@ -1,5 +1,5 @@ import os -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GitPromiseTypeModule(PromiseModule): diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index d1f206b1..333b992f 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -44,7 +44,8 @@ import json from subprocess import Popen, PIPE import sys -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result + class GpgKeysPromiseTypeModule(PromiseModule): def __init__(self): diff --git a/examples/rss/rss.py b/examples/rss/rss.py index 0ef6a4c8..1ece7fe3 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -1,6 +1,6 @@ import requests, html, re, os, random import xml.etree.ElementTree as ET -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class RssPromiseTypeModule(PromiseModule): diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index 022919a2..ec7775de 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -2,7 +2,7 @@ import ssl import urllib.request import urllib.error -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class SiteUpPromiseTypeModule(PromiseModule): diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine_module_library.py similarity index 100% rename from libraries/python/cfengine.py rename to libraries/python/cfengine_module_library.py diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 115ac775..ee9353bf 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -2,7 +2,7 @@ from typing import Dict, Tuple, List -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result try: from ansible import context diff --git a/promise-types/git/git.py b/promise-types/git/git.py index b14354af..cb8fecdb 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GitPromiseTypeModule(PromiseModule): diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index 111cf571..9c3f09c1 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -1,7 +1,7 @@ import re import json from subprocess import Popen, PIPE -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GroupsPromiseTypeModule(PromiseModule): diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index 735e5f04..f59ef115 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -8,7 +8,7 @@ import json from contextlib import contextmanager -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result _SUPPORTED_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"} diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index c8a90aa7..24958517 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -3,7 +3,7 @@ as they are in `man iptables` """ -from cfengine import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject from typing import Callable, List, Dict, Tuple from collections import namedtuple from itertools import takewhile, dropwhile diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 32bb4c7d..db48d48a 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -3,7 +3,7 @@ import tempfile import shutil -from cfengine import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject def is_number(num): diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index 2803c1c9..dcb9af8d 100644 --- a/promise-types/symlinks/symlinks.py +++ b/promise-types/symlinks/symlinks.py @@ -1,5 +1,5 @@ import os -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class SymlinksPromiseTypeModule(PromiseModule): diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index aae14130..e8219801 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -5,7 +5,7 @@ from enum import Enum from typing import Dict, List, Optional, Tuple -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result SYSTEMD_LIB_PATH = "/lib/systemd/system" From 18876d8e17a20409c7556c73681ad0c802ca8333 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:47:46 +0200 Subject: [PATCH 054/146] Reformatted all python files with black Signed-off-by: Ole Herman Schumacher Elgesem --- examples/rss/rss.py | 75 +++++++------ promise-types/git/git.py | 9 +- promise-types/http/http_promise_type.py | 142 +++++++++++++++++------- promise-types/iptables/iptables.py | 7 +- promise-types/json/json_promise_type.py | 7 +- 5 files changed, 167 insertions(+), 73 deletions(-) diff --git a/examples/rss/rss.py b/examples/rss/rss.py index 1ece7fe3..5de683fc 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -7,7 +7,6 @@ class RssPromiseTypeModule(PromiseModule): def __init__(self): super().__init__("rss_promise_module", "0.0.3") - def validate_promise(self, promiser, attributes, metadata): # check promiser type if type(promiser) is not str: @@ -15,37 +14,46 @@ def validate_promise(self, promiser, attributes, metadata): # check that promiser is a valid file path if not self._is_unix_file(promiser) and not self._is_win_file(promiser): - raise ValidationError(f"invalid value '{promiser}' for promiser: must be a filepath") + raise ValidationError( + f"invalid value '{promiser}' for promiser: must be a filepath" + ) # check that required attribute feed is present if "feed" not in attributes: raise ValidationError("Missing required attribute feed") # check that attribute feed has a valid type - feed = attributes['feed'] + feed = attributes["feed"] if type(feed) is not str: raise ValidationError("Invalid type for attribute feed: expected string") # check that attribute feed is a valid file path or url - if not (self._is_unix_file(feed) or self._is_win_file(feed) or self._is_url(feed)): - raise ValidationError(f"Invalid value '{feed}' for attribute feed: must be a file path or url") + if not ( + self._is_unix_file(feed) or self._is_win_file(feed) or self._is_url(feed) + ): + raise ValidationError( + f"Invalid value '{feed}' for attribute feed: must be a file path or url" + ) # additional checks if optional attribute select is present if "select" in attributes: - select = attributes['select'] + select = attributes["select"] # check that attribute select has a valid type if type(select) is not str: - raise ValidationError(f"Invalid type for attribute select: expected string") + raise ValidationError( + f"Invalid type for attribute select: expected string" + ) # check that attribute select has a valid value - if select != 'newest' and select != 'oldest' and select != 'random': - raise ValidationError(f"Invalid value '{select}' for attribute select: must be newest, oldest or random") - + if select != "newest" and select != "oldest" and select != "random": + raise ValidationError( + f"Invalid value '{select}' for attribute select: must be newest, oldest or random" + ) def evaluate_promise(self, promiser, attributes, metadata): # get attriute feed - feed = attributes['feed'] + feed = attributes["feed"] # fetch resource resource = self._get_resource(feed) @@ -65,7 +73,6 @@ def evaluate_promise(self, promiser, attributes, metadata): return result - def _get_resource(self, path): if self._is_url(path): # fetch from url @@ -73,73 +80,76 @@ def _get_resource(self, path): response = requests.get(path) if response.ok: return response.content - self.log_error(f"Failed to fetch feed from url '{path}'': status code '{response.status_code}'") + self.log_error( + f"Failed to fetch feed from url '{path}'': status code '{response.status_code}'" + ) return None # fetch from file try: self.log_verbose(f"Reading feed from file '{path}'") - with open(path, 'r', encoding='utf-8') as f: + with open(path, "r", encoding="utf-8") as f: resource = f.read() return resource except Exception as e: self.log_error(f"Failed to open file '{path}' for reading: {e}") return None - def _get_items(self, res, path): # extract descriptions in /channel/item try: self.log_verbose(f"Parsing feed '{path}'") items = [] root = ET.fromstring(res) - for item in root.findall('./channel/item'): + for item in root.findall("./channel/item"): for child in item: - if child.tag == 'description': + if child.tag == "description": items.append(child.text) return items except Exception as e: self.log_error(f"Failed to parse feed '{path}': {e}") return None - def _pick_item(self, items, attributes): # Pick newest item as default item = items[0] # Select item from feed if "select" in attributes: - select = attributes['select'] - if select == 'random': + select = attributes["select"] + if select == "random": self.log_verbose("Selecting random item from feed") item = random.choice(items) - elif select == 'oldest': + elif select == "oldest": self.log_verbose("Selecting oldest item from feed") - item = items[- 1] + item = items[-1] else: self.log_verbose("Selecting newest item from feed") else: self.log_verbose("Selecting newest item as default") return item - def _write_promiser(self, item, promiser): file_exist = os.path.isfile(promiser) if file_exist: try: - with open(promiser, 'r', encoding='utf-8') as f: + with open(promiser, "r", encoding="utf-8") as f: if f.read() == item: - self.log_verbose(f"File '{promiser}' exists and is up to date, no changes needed") + self.log_verbose( + f"File '{promiser}' exists and is up to date, no changes needed" + ) return Result.KEPT except Exception as e: self.log_error(f"Failed to open file '{promiser}' for reading: {e}") return Result.NOT_KEPT try: - with open(promiser, 'w', encoding='utf-8') as f: + with open(promiser, "w", encoding="utf-8") as f: if file_exist: - self.log_info(f"File '{promiser}' exists but contents differ, updating content") + self.log_info( + f"File '{promiser}' exists but contents differ, updating content" + ) else: self.log_info(f"File '{promiser}' does not exist, creating file") f.write(item) @@ -148,17 +158,20 @@ def _write_promiser(self, item, promiser): self.log_error(f"Failed to open file '{promiser}' for writing: {e}") return Result.NOT_KEPT - def _is_win_file(self, path): return re.search(r"^[a-zA-Z]:\\[\\\S|*\S]?.*$", path) != None - def _is_unix_file(self, path): return re.search(r"^(/[^/ ]*)+/?$", path) != None - def _is_url(self, path): - return re.search(r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", path) != None + return ( + re.search( + r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + path, + ) + != None + ) if __name__ == "__main__": diff --git a/promise-types/git/git.py b/promise-types/git/git.py index cb8fecdb..4880b4e1 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -161,7 +161,12 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): # checkout the branch, if different from the current one output = self._git( model, - [model.executable, "rev-parse", "--abbrev-ref", "HEAD".format()], + [ + model.executable, + "rev-parse", + "--abbrev-ref", + "HEAD".format(), + ], cwd=model.destination, ) detached = False @@ -256,7 +261,7 @@ def _git_envvars(self, model: object): env["GIT_SSH_COMMAND"] = model.ssh_executable if model.ssh_options: env["GIT_SSH_COMMAND"] += " " + model.ssh_options - if not 'HOME' in env: + if not "HOME" in env: # git should have a HOME env var to retrieve .gitconfig, .git-credentials, etc env["HOME"] = str(Path.home()) return env diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index f59ef115..094d9243 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -13,6 +13,7 @@ _SUPPORTED_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"} + class FileInfo: def __init__(self, target): self.target = target @@ -36,7 +37,9 @@ def validate_promise(self, promiser, attributes, metadata): if type(method) != str: raise ValidationError("'method' must be a string") if method not in _SUPPORTED_METHODS: - raise ValidationError("'method' must be one of %s" % ", ".join(_SUPPORTED_METHODS)) + raise ValidationError( + "'method' must be one of %s" % ", ".join(_SUPPORTED_METHODS) + ) if "headers" in attributes: headers = attributes["headers"] @@ -44,23 +47,35 @@ def validate_promise(self, promiser, attributes, metadata): if headers_type == str: headers_lines = headers.splitlines() if any(line.count(":") != 1 for line in headers_lines): - raise ValidationError("'headers' must be string with 'name: value' pairs on separate lines") + raise ValidationError( + "'headers' must be string with 'name: value' pairs on separate lines" + ) elif headers_type == list: if any(line.count(":") != 1 for line in headers): - raise ValidationError("'headers' must be a list of 'name: value' pairs") + raise ValidationError( + "'headers' must be a list of 'name: value' pairs" + ) elif headers_type == dict: # nothing to check for dict? pass else: - raise ValidationError("'headers' must be a string, an slist or a data container" + - " value with 'name: value' pairs") + raise ValidationError( + "'headers' must be a string, an slist or a data container" + + " value with 'name: value' pairs" + ) if "payload" in attributes: payload = attributes["payload"] if type(payload) not in (str, dict): - raise ValidationError("'payload' must be a string or a data container value") + raise ValidationError( + "'payload' must be a string or a data container value" + ) - if type(payload) == str and payload.startswith("@") and not os.path.isabs(payload[1:]): + if ( + type(payload) == str + and payload.startswith("@") + and not os.path.isabs(payload[1:]) + ): raise ValidationError("File-based payload must be an absolute path") if "file" in attributes: @@ -70,18 +85,25 @@ def validate_promise(self, promiser, attributes, metadata): if "insecure" in attributes: insecure = attributes["insecure"] - if type(insecure) != str or insecure not in ("true", "True", "false", "False"): - raise ValidationError("'insecure' must be either \"true\" or \"false\"") + if type(insecure) != str or insecure not in ( + "true", + "True", + "false", + "False", + ): + raise ValidationError('\'insecure\' must be either "true" or "false"') @contextmanager def target_fh(self, file_info): if file_info.target: dirname = os.path.dirname(file_info.target) os.makedirs(dirname, exist_ok=True) - temp_file = file_info.target+".cftemp" + temp_file = file_info.target + ".cftemp" with open(temp_file, "wb") as fh: yield fh - if not os.path.isfile(file_info.target) or not filecmp.cmp(temp_file, file_info.target): + if not os.path.isfile(file_info.target) or not filecmp.cmp( + temp_file, file_info.target + ): os.replace(temp_file, file_info.target) file_info.was_repaired = True else: @@ -90,7 +112,6 @@ def target_fh(self, file_info): # this is to do something like API requests where you don't care about the result other than response code yield open(os.devnull, "wb") - def evaluate_promise(self, promiser, attributes, metadata): url = attributes.get("url", promiser) method = attributes.get("method", "GET") @@ -100,24 +121,39 @@ def evaluate_promise(self, promiser, attributes, metadata): insecure = attributes.get("insecure", False) result = Result.KEPT - canonical_promiser = promiser.translate(str.maketrans({char: "_" for char in ("@", "/", ":", "?", "&", "%")})) + canonical_promiser = promiser.translate( + str.maketrans({char: "_" for char in ("@", "/", ":", "?", "&", "%")}) + ) if headers and type(headers) != dict: if type(headers) == str: - headers = {key: value for key, value in (line.split(":") for line in headers.splitlines())} + headers = { + key: value + for key, value in (line.split(":") for line in headers.splitlines()) + } elif type(headers) == list: - headers = {key: value for key, value in (line.split(":") for line in headers)} + headers = { + key: value for key, value in (line.split(":") for line in headers) + } if payload: if type(payload) == dict: try: payload = json.dumps(payload) except TypeError: - self.log_error("Failed to convert 'payload' to text representation for request '%s'" % url) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_payload_failed" % (canonical_promiser, method), - "%s_%s_payload_conversion_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to convert 'payload' to text representation for request '%s'" + % url + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_payload_failed" % (canonical_promiser, method), + "%s_%s_payload_conversion_failed" + % (canonical_promiser, method), + ], + ) if "Content-Type" not in headers: headers["Content-Type"] = "application/json" @@ -129,11 +165,18 @@ def evaluate_promise(self, promiser, attributes, metadata): # scope. Thank you, Python! payload = open(path, "rb") except OSError as e: - self.log_error("Failed to open payload file '%s' for request '%s': %s" % (path, url, e)) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_payload_failed" % (canonical_promiser, method), - "%s_%s_payload_file_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to open payload file '%s' for request '%s': %s" + % (path, url, e) + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_payload_failed" % (canonical_promiser, method), + "%s_%s_payload_file_failed" % (canonical_promiser, method), + ], + ) if "Content-Length" not in headers: headers["Content-Length"] = os.path.getsize(path) @@ -142,12 +185,14 @@ def evaluate_promise(self, promiser, attributes, metadata): if type(payload) == str: payload = payload.encode("utf-8") - request = urllib.request.Request(url=url, data=payload, method=method, headers=headers) + request = urllib.request.Request( + url=url, data=payload, method=method, headers=headers + ) SSL_context = None if insecure: # convert to a boolean - insecure = (insecure.lower() == "true") + insecure = insecure.lower() == "true" if insecure: SSL_context = ssl.SSLContext() SSL_context.verify_method = ssl.CERT_NONE @@ -155,8 +200,13 @@ def evaluate_promise(self, promiser, attributes, metadata): try: with urllib.request.urlopen(request, context=SSL_context) as url_req: if not (200 <= url_req.status < 300): - self.log_error("Request for '%s' failed with code %d" % (url, url_req.status)) - return (Result.NOT_KEPT, ["%s_%s_request_failed" % (canonical_promiser, method)]) + self.log_error( + "Request for '%s' failed with code %d" % (url, url_req.status) + ) + return ( + Result.NOT_KEPT, + ["%s_%s_request_failed" % (canonical_promiser, method)], + ) # TODO: log progress when url_req.headers["Content-length"] > REPORTING_THRESHOLD file_info = FileInfo(target) with self.target_fh(file_info) as target_file: @@ -169,22 +219,38 @@ def evaluate_promise(self, promiser, attributes, metadata): result = Result.REPAIRED except urllib.error.URLError as e: self.log_error("Failed to request '%s': %s" % (url, e)) - return (Result.NOT_KEPT, ["%s_%s_request_failed" % (canonical_promiser, method)]) + return ( + Result.NOT_KEPT, + ["%s_%s_request_failed" % (canonical_promiser, method)], + ) except OSError as e: - self.log_error("Failed to store '%s' response to '%s': %s" % (url, target, e)) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_file_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to store '%s' response to '%s': %s" % (url, target, e) + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_file_failed" % (canonical_promiser, method), + ], + ) if target: if result == Result.REPAIRED: - self.log_info("Saved request response from '%s' to '%s'" % (url, target)) + self.log_info( + "Saved request response from '%s' to '%s'" % (url, target) + ) else: - self.log_info("No changes in request response from '%s' to '%s'" % (url, target)) + self.log_info( + "No changes in request response from '%s' to '%s'" % (url, target) + ) else: - self.log_info("Successfully executed%s request to '%s'" % ((" " + method if method else ""), - url)) + self.log_info( + "Successfully executed%s request to '%s'" + % ((" " + method if method else ""), url) + ) return (result, ["%s_%s_request_done" % (canonical_promiser, method)]) + if __name__ == "__main__": HTTPPromiseModule().start() diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 24958517..a94b77cf 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -3,7 +3,12 @@ as they are in `man iptables` """ -from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) from typing import Callable, List, Dict, Tuple from collections import namedtuple from itertools import takewhile, dropwhile diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index db48d48a..02c968e9 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -3,7 +3,12 @@ import tempfile import shutil -from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) def is_number(num): From 43825b584610fec0a5caabcdd3fd1532353daf73 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:49:45 +0200 Subject: [PATCH 055/146] Set all promise type modules to version number 0.0.0 See: https://northerntech.atlassian.net/browse/CFE-4553 Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/ansible/ansible_promise.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 2 +- promise-types/iptables/iptables.py | 2 +- promise-types/json/json_promise_type.py | 2 +- promise-types/symlinks/symlinks.py | 2 +- promise-types/systemd/systemd.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index ee9353bf..5f2c1c59 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -73,7 +73,7 @@ def v2_playbook_on_stats(self, stats): class AnsiblePromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(AnsiblePromiseTypeModule, self).__init__( - "ansible_promise_module", "0.2.2", **kwargs + "ansible_promise_module", "0.0.0", **kwargs ) def must_be_absolute(v): diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 4880b4e1..3f5a0777 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -10,7 +10,7 @@ class GitPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(GitPromiseTypeModule, self).__init__( - "git_promise_module", "0.2.5", **kwargs + "git_promise_module", "0.0.0", **kwargs ) def destination_must_be_absolute(v): diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index 9c3f09c1..7d6bd998 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -6,7 +6,7 @@ class GroupsPromiseTypeModule(PromiseModule): def __init__(self): - super().__init__("groups_promise_module", "0.2.4") + super().__init__("groups_promise_module", "0.0.0") self._name_regex = re.compile(r"^[a-z_][a-z0-9_-]*[$]?$") self._name_maxlen = 32 diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index 094d9243..fe71dcee 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -21,7 +21,7 @@ def __init__(self, target): class HTTPPromiseModule(PromiseModule): - def __init__(self, name="http_promise_module", version="2.0.1", **kwargs): + def __init__(self, name="http_promise_module", version="0.0.0", **kwargs): super().__init__(name, version, **kwargs) def validate_promise(self, promiser, attributes, metadata): diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index a94b77cf..80018131 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -122,7 +122,7 @@ class IptablesPromiseTypeModule(PromiseModule): } def __init__(self, **kwargs): - super().__init__("iptables_promise_module", "0.2.2", **kwargs) + super().__init__("iptables_promise_module", "0.0.0", **kwargs) def must_be_one_of(items) -> Callable: def validator(v): diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 02c968e9..b72260c7 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -31,7 +31,7 @@ class JsonPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(JsonPromiseTypeModule, self).__init__( - name="json_promise_module", version="0.0.1", **kwargs + name="json_promise_module", version="0.0.0", **kwargs ) self.types = ["object", "array", "string", "number", "primitive"] diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index dcb9af8d..1b05864a 100644 --- a/promise-types/symlinks/symlinks.py +++ b/promise-types/symlinks/symlinks.py @@ -7,7 +7,7 @@ class SymlinksPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(SymlinksPromiseTypeModule, self).__init__( name="symlinks_promise_module", - version="0.0.1", + version="0.0.0", **kwargs, ) diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index e8219801..7b2821ad 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -22,7 +22,7 @@ class SystemdPromiseTypeStates(Enum): class SystemdPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(SystemdPromiseTypeModule, self).__init__( - "systemd_promise_module", "0.2.3", **kwargs + "systemd_promise_module", "0.0.0", **kwargs ) def state_must_be_valid(v): From 085d6ae0a5261e3b41fae2272f4703131d062637 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 14 Jul 2025 14:50:49 +0200 Subject: [PATCH 056/146] Prepared python module library for version replacement Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 4 +++- libraries/python/cfengine_module_library.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cfbs.json b/cfbs.json index e2641c01..e05a954c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -154,7 +154,9 @@ "subdirectory": "libraries/python", "steps": [ "copy cfengine_module_library.py modules/promises/cfengine_module_library.py", - "copy cfengine_module_library.py modules/promises/cfengine.py" + "replace_version 2 0.0.0 modules/promises/cfengine_module_library.py", + "copy cfengine_module_library.py modules/promises/cfengine.py", + "replace_version 2 0.0.0 modules/promises/cfengine.py" ] }, "maintainers-in-motd": { diff --git a/libraries/python/cfengine_module_library.py b/libraries/python/cfengine_module_library.py index 7e07e0f9..3e8ddb4d 100644 --- a/libraries/python/cfengine_module_library.py +++ b/libraries/python/cfengine_module_library.py @@ -5,6 +5,15 @@ Currently, this is for implementing custom promise types, but it might be expanded to other types of modules in the future, for example custom functions. + +This library is available as a module in CFEngine Build: + +https://build.cfengine.com/modules/library-for-promise-types-in-python/ + +CFEngine Build version: 0.0.0 + +(If the version number is all zeroes, you are looking at the source. It will +be replaced in a build step when you run cfbs build). """ import sys @@ -119,7 +128,7 @@ class Result: class PromiseModule: def __init__( - self, name="default_module_name", version="0.0.1", record_file_path=None + self, name="default_module_name", version="0.0.0", record_file_path=None ): self.name = name self.version = version From dd974ef6a05f7ad3762123e8f1e5730171990501 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Fri, 21 Nov 2025 08:09:18 -0600 Subject: [PATCH 057/146] Added windows-openssh-server README --- software/windows/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 software/windows/README.md diff --git a/software/windows/README.md b/software/windows/README.md new file mode 100644 index 00000000..5dd43258 --- /dev/null +++ b/software/windows/README.md @@ -0,0 +1,5 @@ +The `windows-openssh-server` bundle ensures that an openssh server is running on Windows hosts if the class data:openssh_server_installed is defined. + +You can define this in the Host Info page or CMDB. + +This bundle depends on and uses the [windows-capability](https://build.cfengine.com/modules/windows-capability/) module to accomplish the goal. From 31f849dc5e6e75a95a20e1ad0981c1a7474f7eb4 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 22 Jan 2026 15:46:59 -0600 Subject: [PATCH 058/146] Fixed ansible custom promise type ability to load modules This seems to be due to the promise type not calling init_plugin_loader() as mentioned in this issue: https://github.com/ansible/ansible/issues/83992 and solved in this commit https://github.com/ansible/ansible-documentation/pull/1524 This occurred with the upgrade from ansible 2.14 to 2.15 around May 2024. Ticket: ENT-13701 Changelog: title --- promise-types/ansible/ansible_promise.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 5f2c1c59..5f9e39a2 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -13,6 +13,7 @@ from ansible.parsing.dataloader import DataLoader from ansible.plugins.callback import CallbackBase from ansible.vars.manager import VariableManager + from ansible.plugins.loader import init_plugin_loader ANSIBLE_AVAILABLE = True except ImportError: @@ -165,4 +166,5 @@ def evaluate_promise( if __name__ == "__main__": + init_plugin_loader() AnsiblePromiseTypeModule().start() From 53d6f125f91de23b79a5bf4b345ef7bad2e72984 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 29 Jan 2026 11:25:19 -0600 Subject: [PATCH 059/146] Added package-method-ansible-galaxy-collection --- cfbs.json | 9 +++++++++ .../README.md | 18 ++++++++++++++++++ ...ackage-method-ansible-galaxy-collections.cf | 15 +++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 management/package-method-ansible-galaxy-collection/README.md create mode 100644 management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf diff --git a/cfbs.json b/cfbs.json index e05a954c..a9787f21 100644 --- a/cfbs.json +++ b/cfbs.json @@ -177,6 +177,15 @@ "bundles powershell_execution_policy_inventory" ] }, + "package-method-ansible-galaxy-collection": { + "description": "Package method for installing Ansible Galaxy Collections.", + "subdirectory": "management/package-method-ansible-galaxy-collection", + "dependencies": [], + "steps": [ + "directory ./ services/cfbs/modules/package-method-ansible-galaxy-collection/", + "policy_files services/cfbs/modules/package-method-ansible-galaxy-collection/" + ] + }, "package-method-winget": { "description": "Package method for Windows winget package manager.", "subdirectory": "management/package-method-winget", diff --git a/management/package-method-ansible-galaxy-collection/README.md b/management/package-method-ansible-galaxy-collection/README.md new file mode 100644 index 00000000..b42c11dd --- /dev/null +++ b/management/package-method-ansible-galaxy-collection/README.md @@ -0,0 +1,18 @@ +# package-method-ansible-galaxy-collection + +## Usage +This module enables a `ansible-galaxy-collection` package method used with policy like: + +```cf3 +packages: + "ansible.posix" + package_method => ansible_galaxy_collection, + package_policy => "add"; +``` + +Note that there is not a command option in ansible-galaxy to uninstall collections so that will result in no changes being made and a warning message. + +## Installation + +This module does not ensure that needed ansible packages are installed to provide the `ansible-galaxy` command. +Typically this is a package named `ansible` on Debian-based distributions or `ansible-core` on RedHat-based distributions. diff --git a/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf b/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf new file mode 100644 index 00000000..17200366 --- /dev/null +++ b/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf @@ -0,0 +1,15 @@ +body package_method ansible_galaxy_collection +{ + package_changes => "bulk"; + package_name_convention => "$(name)"; + package_delete_convention => "$(name)"; + + package_installed_regex => ".*"; + package_list_name_regex => '^"([^"]*)",.*'; + package_list_version_regex => '.*,"([^"]*)".*'; + + + package_list_command => "ansible-galaxy collection list"; + package_delete_command => 'echo "Deleting an Ansible Galaxy Collection is not supported"'; + package_add_command => "ansible-galaxy collection install "; +} From 91f9ee86d1f071f6f3eb0fce86c946e2fea76802 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Fri, 30 Jan 2026 10:21:54 -0600 Subject: [PATCH 060/146] Update management/package-method-ansible-galaxy-collection/README.md Co-authored-by: Nick Anderson --- management/package-method-ansible-galaxy-collection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/package-method-ansible-galaxy-collection/README.md b/management/package-method-ansible-galaxy-collection/README.md index b42c11dd..16737517 100644 --- a/management/package-method-ansible-galaxy-collection/README.md +++ b/management/package-method-ansible-galaxy-collection/README.md @@ -10,7 +10,7 @@ packages: package_policy => "add"; ``` -Note that there is not a command option in ansible-galaxy to uninstall collections so that will result in no changes being made and a warning message. +Note that there is not a command option in ansible-galaxy to uninstall collections so that will result in no changes being made and a warning message. Ansible documentation suggests [removing collections with rm](https://docs.ansible.com/projects/ansible/latest/collections_guide/collections_installing.html#removing-a-collection) ## Installation From 2c7058ed30a821e4abd85e57326a9c2fba4c7c51 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 12 Feb 2026 17:55:54 -0600 Subject: [PATCH 061/146] Added inventory-fde module for full disk encryption detection Detects mounted dm-crypt volumes (LUKS1, LUKS2, plain) on Linux by reading /sys/block/ and /proc/mounts. Reports whether all, some, or no non-virtual block device filesystems are encrypted. Ticket: ENT-13744 Changelog: title --- cfbs.json | 12 +- inventory/inventory-fde/README.md | 36 +++++ inventory/inventory-fde/inventory-fde.cf | 143 ++++++++++++++++++ .../inventory-windows-services.cf | 0 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 inventory/inventory-fde/README.md create mode 100644 inventory/inventory-fde/inventory-fde.cf rename inventory/{ => inventory-windows-services}/inventory-windows-services.cf (100%) diff --git a/cfbs.json b/cfbs.json index a9787f21..f8af42ba 100644 --- a/cfbs.json +++ b/cfbs.json @@ -137,13 +137,23 @@ }, "inventory-windows-services": { "description": "Inventory running Windows services.", - "subdirectory": "inventory", + "subdirectory": "inventory/inventory-windows-services", "steps": [ "copy inventory-windows-services.cf services/cfbs/inventory-windows-services/", "policy_files services/cfbs/inventory-windows-services/", "bundles inventory_windows_services_running" ] }, + "inventory-fde": { + "description": "Inventory full disk encryption status (LUKS, FileVault, BitLocker).", + "tags": ["inventory", "security"], + "subdirectory": "inventory/inventory-fde", + "steps": [ + "copy inventory-fde.cf services/cfbs/inventory-fde/", + "policy_files services/cfbs/inventory-fde/", + "bundles inventory_fde:main" + ] + }, "library-for-promise-types-in-bash": { "description": "Library enabling promise types implemented in bash.", "subdirectory": "libraries/bash", diff --git a/inventory/inventory-fde/README.md b/inventory/inventory-fde/README.md new file mode 100644 index 00000000..e9d765a1 --- /dev/null +++ b/inventory/inventory-fde/README.md @@ -0,0 +1,36 @@ +Full disk encryption (FDE) protects data at rest by encrypting entire block devices. +This module detects mounted volumes backed by dm-crypt (LUKS1, LUKS2, or plain dm-crypt) on Linux systems and reports whether all, some, or none of the non-virtual block device filesystems are encrypted. + +Detection is performed entirely through virtual filesystem reads (`/sys/block/` and `/proc/mounts`), with no dependency on external commands like `dmsetup` or `findmnt`. + +## How it works + +1. Enumerates device-mapper block devices from `/sys/block/dm-*` +2. Reads each device's DM subsystem UUID from `/sys/block/dm-N/dm/uuid` +3. Identifies crypt devices by the `CRYPT-` prefix in the UUID +4. Parses `/proc/mounts` to find all non-virtual block device mounts (excluding loop devices) +5. Classifies each mount as encrypted or unencrypted by checking if its device matches a crypt device path + +## Inventory + +- **Full disk encryption enabled** -- `yes` if all non-virtual block device filesystems are encrypted, `partial` if some are encrypted and some are not, `no` if none are encrypted. +- **Full disk encryption method** -- The encryption type(s) detected, e.g. `LUKS2`, `LUKS1`, `PLAIN`, or `none`. Multiple types are comma-separated if different methods are in use. +- **Full disk encryption volumes** -- List of mountpoints backed by encrypted devices. +- **Unencrypted volumes** -- List of mountpoints on non-virtual block devices that are not encrypted. + +## Example + +A system with LUKS2-encrypted root but unencrypted `/boot` and `/boot/efi`: + +``` +$ sudo cf-agent -Kf ./inventory-fde.cf --show-evaluated-vars=inventory_fde +Variable name Variable value Meta tags Comment +inventory_fde:main.fde_enabled partial source=promise,inventory,attribute_name=Full disk encryption enabled +inventory_fde:main.fde_method LUKS2 source=promise,inventory,attribute_name=Full disk encryption method +inventory_fde:main.fde_volumes {"/"} source=promise,inventory,attribute_name=Full disk encryption volumes +inventory_fde:main.unencrypted_volumes {"/boot","/boot/efi"} source=promise,inventory,attribute_name=Unencrypted volumes +``` + +## Platform + +- Linux only (requires `/sys/block/` and `/proc/mounts`) diff --git a/inventory/inventory-fde/inventory-fde.cf b/inventory/inventory-fde/inventory-fde.cf new file mode 100644 index 00000000..c7f3ec88 --- /dev/null +++ b/inventory/inventory-fde/inventory-fde.cf @@ -0,0 +1,143 @@ +body file control +{ + namespace => "inventory_fde"; +} + +bundle agent main +# @brief Inventory full disk encryption status +# @inventory Full disk encryption enabled - Whether all non-virtual mounted filesystems use dm-crypt encryption (yes, partial, or no). +# @inventory Full disk encryption method - The encryption type(s) in use, e.g. LUKS2, LUKS1, PLAIN, or none. +# @inventory Full disk encryption volumes - List of mountpoints backed by encrypted devices, e.g. /. +# @inventory Unencrypted volumes - List of mountpoints on non-virtual block devices that are not encrypted, e.g. /boot, /boot/efi. +{ + classes: + linux:: + # Flag each dm device that has a CRYPT uuid + "_dm_is_crypt_${_dm_devices}" + expression => regcmp("CRYPT-.*", "${_dm_uuid[${_dm_devices}]}"); + + # Classify each mount: real block device? (starts with /dev/, not a loop device) + "_is_real_block_${_mnt_idx}" + expression => regcmp("/dev/(?!loop)\S+", "${_mnt_data[${_mnt_idx}][0]}"); + + # Classify each real block mount: is device in the crypt paths list? + "_is_encrypted_${_mnt_idx}" + expression => regcmp("(${_crypt_paths_regex})", "${_mnt_data[${_mnt_idx}][0]}"), + if => canonify("_is_real_block_${_mnt_idx}"); + + # Summary classes + "_has_encrypted" + expression => isgreaterthan(length(_encrypted_mountpoints), 0); + "_has_unencrypted" + expression => isgreaterthan(length(_unencrypted_mountpoints), 0); + + vars: + linux:: + # Enumerate all device-mapper block devices + "_dm_devices" slist => lsdir("/sys/block", "dm-\d+", false); + + # Read the DM subsystem uuid and name for each dm device + "_dm_uuid[${_dm_devices}]" + string => readfile("/sys/block/${_dm_devices}/dm/uuid"), + if => fileexists("/sys/block/${_dm_devices}/dm/uuid"); + "_dm_name[${_dm_devices}]" + string => readfile("/sys/block/${_dm_devices}/dm/name"), + if => fileexists("/sys/block/${_dm_devices}/dm/name"); + + # Build list of crypt device paths (both /dev/mapper/ and /dev/) + "_crypt_mapper_path[${_dm_devices}]" + string => "/dev/mapper/${_dm_name[${_dm_devices}]}", + if => canonify("_dm_is_crypt_${_dm_devices}"); + "_crypt_dm_path[${_dm_devices}]" + string => "/dev/${_dm_devices}", + if => canonify("_dm_is_crypt_${_dm_devices}"); + "_all_crypt_paths" + slist => { getvalues(_crypt_mapper_path), getvalues(_crypt_dm_path) }; + + # Build a regex alternation of all crypt device paths for matching + "_crypt_paths_regex" + string => join("|", maplist(regex_replace("$(this)", "([./])", "\\\1", "g"), _all_crypt_paths)); + + # Extract the encryption type for crypt devices + # UUID format: CRYPT--- + "_dm_crypt_type[${_dm_devices}]" + string => regex_replace("${_dm_uuid[${_dm_devices}]}", "^CRYPT-([^-]+)-.*", "\1", ""), + if => canonify("_dm_is_crypt_${_dm_devices}"); + + # Parse /proc/mounts into indexed array + # Columns: 0=device, 1=mountpoint, 2=fstype, 3=options, 4=dump, 5=pass + "_n_mnt_lines" + int => readstringarrayidx("_mnt_data", "/proc/mounts", "\s*#[^\n]*", "\s+", inf, inf); + "_mnt_idx" slist => getindices(_mnt_data); + + # Collect all real block device mountpoints + "_all_real_mountpoint[${_mnt_idx}]" + string => "${_mnt_data[${_mnt_idx}][1]}", + if => canonify("_is_real_block_${_mnt_idx}"); + + # Collect encrypted mountpoints (subset of real block mounts) + "_encrypted_mountpoint[${_mnt_idx}]" + string => "${_mnt_data[${_mnt_idx}][1]}", + if => and( + canonify("_is_real_block_${_mnt_idx}"), + canonify("_is_encrypted_${_mnt_idx}") + ); + + # Derive unencrypted mountpoints as the difference + "_all_real_mountpoints" slist => getvalues(_all_real_mountpoint); + "_encrypted_mountpoints" slist => getvalues(_encrypted_mountpoint); + "_unencrypted_mountpoints" + slist => difference(_all_real_mountpoints, _encrypted_mountpoints); + + # Inventory: full encryption (encrypted volumes exist, no unencrypted ones) + _has_encrypted.!_has_unencrypted:: + "fde_enabled" + string => "yes", + meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + + # Inventory: partial encryption + _has_encrypted._has_unencrypted:: + "fde_enabled" + string => "partial", + meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + + # Inventory: no encryption + linux.!_has_encrypted:: + "fde_enabled" + string => "no", + meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + + # Method and volume details + _has_encrypted:: + "fde_method" + string => join(", ", unique(getvalues(_dm_crypt_type))), + meta => { "inventory", "attribute_name=Full disk encryption method" }; + "fde_volumes" + slist => unique(_encrypted_mountpoints), + meta => { "inventory", "attribute_name=Full disk encryption volumes" }; + + linux.!_has_encrypted:: + "fde_method" + string => "none", + meta => { "inventory", "attribute_name=Full disk encryption method" }; + + _has_unencrypted:: + "unencrypted_volumes" + slist => unique(_unencrypted_mountpoints), + meta => { "inventory", "attribute_name=Unencrypted volumes" }; + + reports: + !linux.verbose_mode:: + "$(this.promise_filename): $(this.namespace):$(this.bundle) is currently only instrumented for Linux. Please consider making a pull request or filing a ticket to request your specific platform."; +} + +body file control +{ + namespace => "default"; +} + +bundle agent __main__ +{ + methods: + "inventory_fde:main"; +} diff --git a/inventory/inventory-windows-services.cf b/inventory/inventory-windows-services/inventory-windows-services.cf similarity index 100% rename from inventory/inventory-windows-services.cf rename to inventory/inventory-windows-services/inventory-windows-services.cf From 486a05ecf197f49ccf22cb8db424b6228a7bab52 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Fri, 13 Feb 2026 16:22:52 -0600 Subject: [PATCH 062/146] Added cipher and keyslot inventory to inventory-fde Extended inventory-fde to report active dm-crypt cipher per volume and LUKS keyslot details (per-keyslot cipher and PBKDF algorithm). LUKS2 metadata is cached as JSON with a 24-hour TTL. Gracefully degrades when dmsetup or cryptsetup are absent. Tool paths are defined as variables for single-point configuration. Includes loopback test helper script and Mission Portal screenshot. --- cfbs.json | 4 +- inventory/inventory-fde/README.md | 32 ++- .../inventory-fde-mission-portal.png | Bin 0 -> 47983 bytes inventory/inventory-fde/inventory-fde.cf | 230 ++++++++++++++++-- .../inventory-fde/test-encrypted-volume.sh | 96 ++++++++ 5 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 inventory/inventory-fde/inventory-fde-mission-portal.png create mode 100755 inventory/inventory-fde/test-encrypted-volume.sh diff --git a/cfbs.json b/cfbs.json index f8af42ba..00c83667 100644 --- a/cfbs.json +++ b/cfbs.json @@ -149,8 +149,8 @@ "tags": ["inventory", "security"], "subdirectory": "inventory/inventory-fde", "steps": [ - "copy inventory-fde.cf services/cfbs/inventory-fde/", - "policy_files services/cfbs/inventory-fde/", + "copy inventory-fde.cf services/cfbs/modules/inventory-fde/inventory-fde.cf", + "policy_files services/cfbs/modules/inventory-fde/inventory-fde.cf", "bundles inventory_fde:main" ] }, diff --git a/inventory/inventory-fde/README.md b/inventory/inventory-fde/README.md index e9d765a1..f2c8a826 100644 --- a/inventory/inventory-fde/README.md +++ b/inventory/inventory-fde/README.md @@ -1,7 +1,8 @@ Full disk encryption (FDE) protects data at rest by encrypting entire block devices. This module detects mounted volumes backed by dm-crypt (LUKS1, LUKS2, or plain dm-crypt) on Linux systems and reports whether all, some, or none of the non-virtual block device filesystems are encrypted. -Detection is performed entirely through virtual filesystem reads (`/sys/block/` and `/proc/mounts`), with no dependency on external commands like `dmsetup` or `findmnt`. +Basic detection (encryption status, method, volume lists) is performed entirely through virtual filesystem reads (`/sys/block/` and `/proc/mounts`). +When `dmsetup` and `cryptsetup` are available, the module additionally reports the active cipher and LUKS keyslot details (per-keyslot cipher and PBKDF algorithm). ## How it works @@ -10,13 +11,19 @@ Detection is performed entirely through virtual filesystem reads (`/sys/block/` 3. Identifies crypt devices by the `CRYPT-` prefix in the UUID 4. Parses `/proc/mounts` to find all non-virtual block device mounts (excluding loop devices) 5. Classifies each mount as encrypted or unencrypted by checking if its device matches a crypt device path +6. If `dmsetup` is available, reads the active cipher from `dmsetup table` for each crypt device +7. If `cryptsetup` is available, reads LUKS keyslot metadata (cipher and PBKDF per slot) via `cryptsetup luksDump` ## Inventory -- **Full disk encryption enabled** -- `yes` if all non-virtual block device filesystems are encrypted, `partial` if some are encrypted and some are not, `no` if none are encrypted. -- **Full disk encryption method** -- The encryption type(s) detected, e.g. `LUKS2`, `LUKS1`, `PLAIN`, or `none`. Multiple types are comma-separated if different methods are in use. -- **Full disk encryption volumes** -- List of mountpoints backed by encrypted devices. -- **Unencrypted volumes** -- List of mountpoints on non-virtual block devices that are not encrypted. +- **Full disk encryption enabled** - `yes` if all non-virtual block device filesystems are encrypted, `partial` if some are encrypted and some are not, `no` if none are encrypted. +- **Full disk encryption methods** - The encryption type(s) detected, e.g. `LUKS2`, `LUKS1`, `PLAIN`. Empty list when no encryption is found. +- **Full disk encryption volumes** - List of mountpoints backed by encrypted devices. +- **Unencrypted volumes** - List of mountpoints on non-virtual block devices that are not encrypted. +- **Full disk encryption volume ciphers** - The active dm-crypt cipher per volume, e.g. `/ : aes-xts-plain64`. Requires `dmsetup`. +- **Full disk encryption keyslot info** - LUKS keyslot cipher and PBKDF per volume, e.g. `/ : 0:aes-xts-plain64/argon2id`. Requires `cryptsetup`. Not available for plain dm-crypt (no keyslots). + +[![Inventory in Mission Portal](inventory-fde-mission-portal.png)](inventory-fde-mission-portal.png) ## Example @@ -26,11 +33,24 @@ A system with LUKS2-encrypted root but unencrypted `/boot` and `/boot/efi`: $ sudo cf-agent -Kf ./inventory-fde.cf --show-evaluated-vars=inventory_fde Variable name Variable value Meta tags Comment inventory_fde:main.fde_enabled partial source=promise,inventory,attribute_name=Full disk encryption enabled -inventory_fde:main.fde_method LUKS2 source=promise,inventory,attribute_name=Full disk encryption method +inventory_fde:main.fde_method {"LUKS2"} source=promise,inventory,attribute_name=Full disk encryption methods inventory_fde:main.fde_volumes {"/"} source=promise,inventory,attribute_name=Full disk encryption volumes inventory_fde:main.unencrypted_volumes {"/boot","/boot/efi"} source=promise,inventory,attribute_name=Unencrypted volumes +inventory_fde:main.fde_volume_cipher {"/ : aes-xts-plain64"} source=promise,inventory,attribute_name=Full disk encryption volume ciphers +inventory_fde:main.fde_keyslot_info {"/ : 0:aes-xts-plain64/argon2id"} source=promise,inventory,attribute_name=Full disk encryption keyslot info +``` + +## Testing + +A helper script is included to create and tear down a LUKS2 test volume on a loopback device: + +``` +sudo ./test-encrypted-volume.sh setup # Create and mount test volume +sudo cf-agent -KIf ./inventory-fde.cf --show-evaluated-vars=inventory_fde +sudo ./test-encrypted-volume.sh teardown # Clean up ``` ## Platform - Linux only (requires `/sys/block/` and `/proc/mounts`) +- Cipher and keyslot inventory requires `dmsetup` and/or `cryptsetup` (typically available on systems with dm-crypt) diff --git a/inventory/inventory-fde/inventory-fde-mission-portal.png b/inventory/inventory-fde/inventory-fde-mission-portal.png new file mode 100644 index 0000000000000000000000000000000000000000..89f8ebc7b2a3fa0e983898fb22c6772339a678a5 GIT binary patch literal 47983 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 z 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 j2;-lv-`D@&RrEs0?MSHb%CVXKtwB;$R-{TuFW~ "inventory_fde"; } +# Duplicated from the CFEngine standard library so this module can be parsed +# and tested standalone without loading the full masterfiles. +# _tidy: lib/files.cf body delete tidy +# _in_shell: lib/commands.cf body contain in_shell + +body delete _tidy +{ + dirlinks => "delete"; + rmdirs => "true"; +} + +body contain _in_shell +{ + useshell => "useshell"; +} + bundle agent main # @brief Inventory full disk encryption status # @inventory Full disk encryption enabled - Whether all non-virtual mounted filesystems use dm-crypt encryption (yes, partial, or no). -# @inventory Full disk encryption method - The encryption type(s) in use, e.g. LUKS2, LUKS1, PLAIN, or none. +# @inventory Full disk encryption methods - The encryption type(s) in use, e.g. LUKS2, LUKS1, PLAIN. # @inventory Full disk encryption volumes - List of mountpoints backed by encrypted devices, e.g. /. # @inventory Unencrypted volumes - List of mountpoints on non-virtual block devices that are not encrypted, e.g. /boot, /boot/efi. +# @inventory Full disk encryption volume ciphers - The active dm-crypt cipher per volume, e.g. / : aes-xts-plain64. +# @inventory Full disk encryption keyslot info - LUKS keyslot cipher and PBKDF per volume, e.g. / : 0:aes-xts-plain64/argon2id. { + vars: + linux:: + "_dmsetup" string => "/sbin/dmsetup"; + "_cryptsetup" string => "/sbin/cryptsetup"; + classes: linux:: + "_have_dmsetup" + expression => isexecutable("${_dmsetup}"); + "_have_cryptsetup" + expression => isexecutable("${_cryptsetup}"); + # Flag each dm device that has a CRYPT uuid "_dm_is_crypt_${_dm_devices}" expression => regcmp("CRYPT-.*", "${_dm_uuid[${_dm_devices}]}"); + # Classify crypt type per device + "_dm_is_luks2_${_dm_devices}" + expression => strcmp("LUKS2", "${_dm_crypt_type[${_dm_devices}]}"); + "_dm_is_luks1_${_dm_devices}" + expression => strcmp("LUKS1", "${_dm_crypt_type[${_dm_devices}]}"); + # Classify each mount: real block device? (starts with /dev/, not a loop device) "_is_real_block_${_mnt_idx}" expression => regcmp("/dev/(?!loop)\S+", "${_mnt_data[${_mnt_idx}][0]}"); @@ -25,6 +59,11 @@ bundle agent main expression => regcmp("(${_crypt_paths_regex})", "${_mnt_data[${_mnt_idx}][0]}"), if => canonify("_is_real_block_${_mnt_idx}"); + # LUKS1: flag enabled keyslots (slots 0-7, all share global cipher, all use PBKDF2) + "_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}" + expression => regcmp("(?s).*Key Slot ${_luks1_slots}: ENABLED.*", "${_luks1_dump[${_dm_devices}]}"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + # Summary classes "_has_encrypted" expression => isgreaterthan(length(_encrypted_mountpoints), 0); @@ -64,6 +103,14 @@ bundle agent main string => regex_replace("${_dm_uuid[${_dm_devices}]}", "^CRYPT-([^-]+)-.*", "\1", ""), if => canonify("_dm_is_crypt_${_dm_devices}"); + # Underlying block device for each crypt device (for cryptsetup luksDump) + "_dm_slaves[${_dm_devices}]" + slist => lsdir("/sys/block/${_dm_devices}/slaves", "[a-z].*", false), + if => canonify("_dm_is_crypt_${_dm_devices}"); + "_dm_slave_dev[${_dm_devices}]" + string => "/dev/${_dm_slaves[${_dm_devices}]}", + if => canonify("_dm_is_crypt_${_dm_devices}"); + # Parse /proc/mounts into indexed array # Columns: 0=device, 1=mountpoint, 2=fstype, 3=options, 4=dump, 5=pass "_n_mnt_lines" @@ -83,54 +130,191 @@ bundle agent main canonify("_is_encrypted_${_mnt_idx}") ); + # Map dm device to its mountpoint via cross-iteration + "_dm_mountpoint[${_dm_devices}]" + string => "${_mnt_data[${_mnt_idx}][1]}", + if => and( + canonify("_dm_is_crypt_${_dm_devices}"), + regcmp("(/dev/mapper/${_dm_name[${_dm_devices}]}|/dev/${_dm_devices})", + "${_mnt_data[${_mnt_idx}][0]}")); + # Derive unencrypted mountpoints as the difference "_all_real_mountpoints" slist => getvalues(_all_real_mountpoint); "_encrypted_mountpoints" slist => getvalues(_encrypted_mountpoint); "_unencrypted_mountpoints" slist => difference(_all_real_mountpoints, _encrypted_mountpoints); - # Inventory: full encryption (encrypted volumes exist, no unencrypted ones) - _has_encrypted.!_has_unencrypted:: - "fde_enabled" - string => "yes", - meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + # --- Active cipher via dmsetup table --- + _have_dmsetup:: + # dmsetup table format: "0 crypt " + "_dm_active_cipher[${_dm_devices}]" + string => regex_replace( + execresult("${_dmsetup} table ${_dm_name[${_dm_devices}]}", "noshell"), + "^\d+\s+\d+\s+crypt\s+(\S+)\s+.*$", "\1", ""), + if => canonify("_dm_is_crypt_${_dm_devices}"); - # Inventory: partial encryption - _has_encrypted._has_unencrypted:: - "fde_enabled" - string => "partial", - meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + # --- LUKS2 keyslot info via cached JSON metadata --- + _have_cryptsetup:: + "_luks2_cache[${_dm_devices}]" + string => "$(sys.statedir)/inventory_fde_luks2_${_dm_devices}.json", + if => canonify("_dm_is_luks2_${_dm_devices}"); + + "_luks2_cache_mtime[${_dm_devices}]" + string => filestat("${_luks2_cache[${_dm_devices}]}", "mtime"), + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}")); + + # --- LUKS1 keyslot info via text parsing --- + _have_cryptsetup:: + "_luks1_slots" slist => { "0", "1", "2", "3", "4", "5", "6", "7" }; + + "_luks1_dump[${_dm_devices}]" + string => execresult("${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", "noshell"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + # LUKS1 global cipher: "Cipher name" + "Cipher mode" + "_luks1_cipher_name[${_dm_devices}]" + string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher name:\s+(\S+).*", "\1", ""), + if => canonify("_dm_is_luks1_${_dm_devices}"); + "_luks1_cipher_mode[${_dm_devices}]" + string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher mode:\s+(\S+).*", "\1", ""), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + # Build per-keyslot summary for each ENABLED slot + "_luks1_ks_entry[${_dm_devices}][${_luks1_slots}]" + string => "${_luks1_slots}:${_luks1_cipher_name[${_dm_devices}]}-${_luks1_cipher_mode[${_dm_devices}]}/pbkdf2", + if => and( + canonify("_dm_is_luks1_${_dm_devices}"), + canonify("_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}")); + + "_luks1_ks_entries[${_dm_devices}]" + slist => getvalues("_luks1_ks_entry[${_dm_devices}]"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + "_dm_keyslot_info[${_dm_devices}]" + string => join(", ", sort("_luks1_ks_entries[${_dm_devices}]", "lex")), + if => canonify("_dm_is_luks1_${_dm_devices}"); - # Inventory: no encryption - linux.!_has_encrypted:: + # --- Inventory attributes --- + + linux:: "fde_enabled" - string => "no", + string => ifelse("_has_encrypted.!_has_unencrypted", "yes", + "_has_encrypted._has_unencrypted", "partial", + "no"), meta => { "inventory", "attribute_name=Full disk encryption enabled" }; - # Method and volume details - _has_encrypted:: "fde_method" - string => join(", ", unique(getvalues(_dm_crypt_type))), - meta => { "inventory", "attribute_name=Full disk encryption method" }; + slist => unique(getvalues(_dm_crypt_type)), + meta => { "inventory", "attribute_name=Full disk encryption methods" }; + + _has_encrypted:: "fde_volumes" slist => unique(_encrypted_mountpoints), meta => { "inventory", "attribute_name=Full disk encryption volumes" }; - linux.!_has_encrypted:: - "fde_method" - string => "none", - meta => { "inventory", "attribute_name=Full disk encryption method" }; - _has_unencrypted:: "unencrypted_volumes" slist => unique(_unencrypted_mountpoints), meta => { "inventory", "attribute_name=Unencrypted volumes" }; + # Build per-volume cipher and keyslot strings with mountpoint prefix + _have_dmsetup:: + "_volume_cipher_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_active_cipher[${_dm_devices}]}", + if => and( + canonify("_dm_is_crypt_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]")); + + _have_cryptsetup:: + "_keyslot_info_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_luks2_ks_${_dm_devices}[keyslots]}", + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]"), + isvariable("_luks2_ks_${_dm_devices}[keyslots]")); + + "_keyslot_info_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_keyslot_info[${_dm_devices}]}", + if => and( + canonify("_dm_is_luks1_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]")); + + _has_encrypted._have_dmsetup:: + "fde_volume_cipher" + slist => getvalues(_volume_cipher_entry), + meta => { "inventory", "attribute_name=Full disk encryption volume ciphers" }; + + _has_encrypted._have_cryptsetup:: + "fde_keyslot_info" + slist => getvalues(_keyslot_info_entry), + meta => { "inventory", "attribute_name=Full disk encryption keyslot info" }; + + files: + _have_cryptsetup:: + # Delete LUKS2 JSON cache if older than 24 hours + "${_luks2_cache[${_dm_devices}]}" + delete => _tidy, + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}"), + isgreaterthan( + format("%d", eval("$(sys.systime) - ${_luks2_cache_mtime[${_dm_devices}]}")), + "86400")); + + commands: + _have_cryptsetup:: + "${_cryptsetup}" + arglist => { "luksDump", + "--dump-json-metadata", + "${_dm_slave_dev[${_dm_devices}]}", + ">", "${_luks2_cache[${_dm_devices}]}" }, + contain => _in_shell, + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + not(fileexists("${_luks2_cache[${_dm_devices}]}"))); + + methods: + _have_cryptsetup:: + # Parse LUKS2 JSON and return keyslot summary via bundle_return_value_index + "luks2_${_dm_devices}" + usebundle => luks2_keyslot_info("${_luks2_cache[${_dm_devices}]}"), + useresult => "_luks2_ks_${_dm_devices}", + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}")); + reports: !linux.verbose_mode:: "$(this.promise_filename): $(this.namespace):$(this.bundle) is currently only instrumented for Linux. Please consider making a pull request or filing a ticket to request your specific platform."; } +bundle agent luks2_keyslot_info(cache_file) +# @brief Parse LUKS2 JSON metadata and return keyslot summary +{ + vars: + "_json" + data => readjson("${cache_file}"); + + "_ks_idx" + slist => getindices("_json[keyslots]"); + + # Build per-keyslot summary: ":/" + "_ks_entry[${_ks_idx}]" + string => "${_ks_idx}:${_json[keyslots][${_ks_idx}][area][encryption]}/${_json[keyslots][${_ks_idx}][kdf][type]}"; + + "_ks_entries" + slist => getvalues(_ks_entry); + + "_keyslots" + string => join(", ", sort(_ks_entries, "lex")); + + reports: + "${_keyslots}" + bundle_return_value_index => "keyslots"; +} + body file control { namespace => "default"; diff --git a/inventory/inventory-fde/test-encrypted-volume.sh b/inventory/inventory-fde/test-encrypted-volume.sh new file mode 100755 index 00000000..bf6bb75c --- /dev/null +++ b/inventory/inventory-fde/test-encrypted-volume.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Create or tear down a test LUKS2 encrypted volume for inventory-fde testing. +# +# Usage: +# sudo ./test-encrypted-volume.sh setup # Create and mount test volume +# sudo ./test-encrypted-volume.sh teardown # Unmount and remove test volume +# +# The test volume uses a loopback device with a hardcoded passphrase ("testpass") +# and mounts at /mnt/fde-test. Requires cryptsetup and root privileges. + +set -euo pipefail + +IMG="/tmp/fde-test.img" +LOOP="/dev/loop100" +NAME="fde-test" +MNT="/mnt/${NAME}" +PASS="testpass" + +setup() { + if [ -e "/dev/mapper/${NAME}" ]; then + echo "Test volume already exists at /dev/mapper/${NAME}" + exit 1 + fi + + echo "Creating 100MB disk image..." + dd if=/dev/zero of="${IMG}" bs=1M count=100 status=progress + + echo "Setting up loop device ${LOOP}..." + losetup "${LOOP}" "${IMG}" + + echo "Formatting as LUKS2..." + echo -n "${PASS}" | cryptsetup luksFormat --type luks2 "${LOOP}" --key-file=- + + echo "Opening LUKS volume as ${NAME}..." + echo -n "${PASS}" | cryptsetup open "${LOOP}" "${NAME}" --key-file=- + + echo "Creating ext4 filesystem..." + mkfs.ext4 -q "/dev/mapper/${NAME}" + + echo "Mounting at ${MNT}..." + mkdir -p "${MNT}" + mount "/dev/mapper/${NAME}" "${MNT}" + + echo "" + echo "Test volume ready. Verify with:" + echo " cf-agent -KIf ./inventory-fde.cf --show-evaluated-vars=inventory_fde" + echo "" + echo "Tear down with:" + echo " sudo $0 teardown" +} + +teardown() { + echo "Tearing down test volume..." + + if mountpoint -q "${MNT}" 2>/dev/null; then + echo "Unmounting ${MNT}..." + umount "${MNT}" + fi + + if [ -e "/dev/mapper/${NAME}" ]; then + echo "Closing LUKS volume ${NAME}..." + cryptsetup close "${NAME}" + fi + + if losetup "${LOOP}" &>/dev/null; then + echo "Detaching loop device ${LOOP}..." + losetup -d "${LOOP}" + fi + + if [ -f "${IMG}" ]; then + echo "Removing disk image ${IMG}..." + rm -f "${IMG}" + fi + + if [ -d "${MNT}" ]; then + rmdir "${MNT}" 2>/dev/null || true + fi + + # Clean up cached LUKS2 JSON metadata + rm -f /var/cfengine/state/inventory_fde_luks2_*.json + + echo "Teardown complete." +} + +case "${1:-}" in + setup) + setup + ;; + teardown) + teardown + ;; + *) + echo "Usage: sudo $0 {setup|teardown}" + exit 1 + ;; +esac From e741b362a4e3892620d43eba8ac512cbf6c98456 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Fri, 13 Feb 2026 17:08:56 -0600 Subject: [PATCH 063/146] Fixed README image to use absolute URL for build website The build website fetches README content from S3 where relative image paths cannot resolve. Use absolute raw.githubusercontent.com URL matching the convention used by all other modules. --- inventory/inventory-fde/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory/inventory-fde/README.md b/inventory/inventory-fde/README.md index f2c8a826..581365b8 100644 --- a/inventory/inventory-fde/README.md +++ b/inventory/inventory-fde/README.md @@ -23,7 +23,7 @@ When `dmsetup` and `cryptsetup` are available, the module additionally reports t - **Full disk encryption volume ciphers** - The active dm-crypt cipher per volume, e.g. `/ : aes-xts-plain64`. Requires `dmsetup`. - **Full disk encryption keyslot info** - LUKS keyslot cipher and PBKDF per volume, e.g. `/ : 0:aes-xts-plain64/argon2id`. Requires `cryptsetup`. Not available for plain dm-crypt (no keyslots). -[![Inventory in Mission Portal](inventory-fde-mission-portal.png)](inventory-fde-mission-portal.png) +[![Inventory in Mission Portal](https://raw.githubusercontent.com/cfengine/modules/master/inventory/inventory-fde/inventory-fde-mission-portal.png)](https://raw.githubusercontent.com/cfengine/modules/master/inventory/inventory-fde/inventory-fde-mission-portal.png) ## Example From e953794cd985516a626512bfb8e8903aac6a2c40 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Fri, 13 Feb 2026 12:58:53 +0100 Subject: [PATCH 064/146] Added python linting in gh actions Signed-off-by: Victor Moene --- .github/workflows/ci.yml | 13 +++++++++++-- ci/linting.sh | 16 ++++++++++++++++ pyrightconfig.json | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 ci/linting.sh create mode 100644 pyrightconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf8f7bc..f7ce1b98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,26 @@ name: Continuous Integration on: + push: + branches: [master] pull_request: - types: [opened, reopened] + branches: [master] jobs: unit_tests: name: Run Unit Tests runs-on: ubuntu-24.04 + permissions: + contents: read steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install dependencies - run: sudo apt-get update -y && sudo apt-get install -y python3 + run: | + sudo apt-get update -y && sudo apt-get install -y python3 + pip install flake8 pyright black pyflakes + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Install cfbs run: pip install cfbs - name: Check the status with cfbs @@ -22,3 +29,5 @@ jobs: run: cfbs validate - name: Check the formatting run: cfbs --check pretty ./cfbs.json + - name: Linting + run: ./ci/linting.sh diff --git a/ci/linting.sh b/ci/linting.sh new file mode 100755 index 00000000..7db1eeda --- /dev/null +++ b/ci/linting.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +echo "Running flake8" +flake8 . --ignore=E203,W503,E722,E731 --max-complexity=100 --max-line-length=160 + +echo "Running pyright" +pyright . + +shopt -s globstar +echo "Running black" +black --check . + +echo "Running pyflakes" +pyflakes . diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..389015c8 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "reportMissingImports": "none" +} From 3b00a5dc2b5b537adf14483d0999f94a084e97ad Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Fri, 13 Feb 2026 13:28:41 +0100 Subject: [PATCH 065/146] Fixed flake8 warnings Signed-off-by: Victor Moene --- examples/git-using-lib/git_using_lib.py | 2 +- examples/gpg/gpg.py | 7 +++--- examples/rss/rss.py | 13 +++++++---- examples/site-up/site_up.py | 2 +- libraries/python/cfengine_module_library.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 26 ++++++++++----------- promise-types/symlinks/symlinks.py | 2 +- promise-types/systemd/systemd.py | 5 ++-- 10 files changed, 32 insertions(+), 31 deletions(-) diff --git a/examples/git-using-lib/git_using_lib.py b/examples/git-using-lib/git_using_lib.py index 07747b18..96c742be 100644 --- a/examples/git-using-lib/git_using_lib.py +++ b/examples/git-using-lib/git_using_lib.py @@ -10,7 +10,7 @@ def validate_promise(self, promiser, attributes, metadata): if not promiser.startswith("/"): raise ValidationError(f"File path '{promiser}' must be absolute") if "repository" not in attributes: - raise ValidationError(f"Attribute 'repository' is required") + raise ValidationError("Attribute 'repository' is required") def evaluate_promise(self, promiser, attributes, metadata): url = attributes["repository"] diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index 333b992f..abe80395 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -42,8 +42,7 @@ """ import json -from subprocess import Popen, PIPE -import sys +from subprocess import Popen, PIPE, TimeoutExpired from cfengine_module_library import PromiseModule, ValidationError, Result @@ -109,9 +108,9 @@ def validate_promise(self, promiser, attributes, metadata): raise ValidationError( f"Promiser '{promiser}' for 'gpg_keys' promise must be an absolute path" ) - if not "keylist" in attributes: + if "keylist" not in attributes: raise ValidationError( - f"Required attribute 'keylist' missing for 'gpg_keys' promise" + "Required attribute 'keylist' missing for 'gpg_keys' promise" ) def evaluate_promise(self, promiser, attributes, metadata): diff --git a/examples/rss/rss.py b/examples/rss/rss.py index 5de683fc..bc93de12 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -1,4 +1,7 @@ -import requests, html, re, os, random +import requests +import re +import os +import random import xml.etree.ElementTree as ET from cfengine_module_library import PromiseModule, ValidationError, Result @@ -42,7 +45,7 @@ def validate_promise(self, promiser, attributes, metadata): # check that attribute select has a valid type if type(select) is not str: raise ValidationError( - f"Invalid type for attribute select: expected string" + "Invalid type for attribute select: expected string" ) # check that attribute select has a valid value @@ -159,10 +162,10 @@ def _write_promiser(self, item, promiser): return Result.NOT_KEPT def _is_win_file(self, path): - return re.search(r"^[a-zA-Z]:\\[\\\S|*\S]?.*$", path) != None + return re.search(r"^[a-zA-Z]:\\[\\\S|*\S]?.*$", path) is not None def _is_unix_file(self, path): - return re.search(r"^(/[^/ ]*)+/?$", path) != None + return re.search(r"^(/[^/ ]*)+/?$", path) is not None def _is_url(self, path): return ( @@ -170,7 +173,7 @@ def _is_url(self, path): r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", path, ) - != None + is not None ) diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index ec7775de..dc05e810 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -31,7 +31,7 @@ def evaluate_promise(self, promiser, attributes, metadata): error = None try: - code = urllib.request.urlopen(url, context=ssl_ctx).getcode() + urllib.request.urlopen(url, context=ssl_ctx).getcode() self.log_verbose(f"Site '{url}' is UP!") return Result.KEPT except urllib.error.HTTPError as e: diff --git a/libraries/python/cfengine_module_library.py b/libraries/python/cfengine_module_library.py index 3e8ddb4d..148a9744 100644 --- a/libraries/python/cfengine_module_library.py +++ b/libraries/python/cfengine_module_library.py @@ -404,7 +404,7 @@ def _handle_evaluate(self, promiser, attributes, request): assert results is not None # Most likely someone forgot to return something # evaluate_promise should return either a result or a (result, result_classes) pair - if type(results) == str: + if isinstance(results, str): self._result = results else: assert len(results) == 2 diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 3f5a0777..3e209d6b 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -261,7 +261,7 @@ def _git_envvars(self, model: object): env["GIT_SSH_COMMAND"] = model.ssh_executable if model.ssh_options: env["GIT_SSH_COMMAND"] += " " + model.ssh_options - if not "HOME" in env: + if "HOME" not in env: # git should have a HOME env var to retrieve .gitconfig, .git-credentials, etc env["HOME"] = str(Path.home()) return env diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index 7d6bd998..5b2b556d 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -64,7 +64,7 @@ def validate_promise(self, promiser, attributes, metadata): ) # check attribute gid value - if type(gid) == str: + if isinstance(gid, str): try: int(gid) except ValueError: diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index fe71dcee..acd1bab2 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -27,14 +27,14 @@ def __init__(self, name="http_promise_module", version="0.0.0", **kwargs): def validate_promise(self, promiser, attributes, metadata): if "url" in attributes: url = attributes["url"] - if type(url) != str: + if not isinstance(url, str): raise ValidationError("'url' must be a string") if not url.startswith(("https://", "http://")): raise ValidationError("Only HTTP(S) requests are supported") if "method" in attributes: method = attributes["method"] - if type(method) != str: + if not isinstance(method, str): raise ValidationError("'method' must be a string") if method not in _SUPPORTED_METHODS: raise ValidationError( @@ -44,18 +44,18 @@ def validate_promise(self, promiser, attributes, metadata): if "headers" in attributes: headers = attributes["headers"] headers_type = type(headers) - if headers_type == str: + if headers_type is str: headers_lines = headers.splitlines() if any(line.count(":") != 1 for line in headers_lines): raise ValidationError( "'headers' must be string with 'name: value' pairs on separate lines" ) - elif headers_type == list: + elif headers_type is list: if any(line.count(":") != 1 for line in headers): raise ValidationError( "'headers' must be a list of 'name: value' pairs" ) - elif headers_type == dict: + elif headers_type is dict: # nothing to check for dict? pass else: @@ -72,7 +72,7 @@ def validate_promise(self, promiser, attributes, metadata): ) if ( - type(payload) == str + isinstance(payload, str) and payload.startswith("@") and not os.path.isabs(payload[1:]) ): @@ -80,12 +80,12 @@ def validate_promise(self, promiser, attributes, metadata): if "file" in attributes: file_ = attributes["file"] - if type(file_) != str or not os.path.isabs(file_): + if not isinstance(file_, str) or not os.path.isabs(file_): raise ValidationError("'file' must be an absolute path to a file") if "insecure" in attributes: insecure = attributes["insecure"] - if type(insecure) != str or insecure not in ( + if not isinstance(insecure, str) or insecure not in ( "true", "True", "false", @@ -125,19 +125,19 @@ def evaluate_promise(self, promiser, attributes, metadata): str.maketrans({char: "_" for char in ("@", "/", ":", "?", "&", "%")}) ) - if headers and type(headers) != dict: - if type(headers) == str: + if headers and not isinstance(headers, dict): + if isinstance(headers, str): headers = { key: value for key, value in (line.split(":") for line in headers.splitlines()) } - elif type(headers) == list: + elif isinstance(headers, list): headers = { key: value for key, value in (line.split(":") for line in headers) } if payload: - if type(payload) == dict: + if isinstance(payload, dict): try: payload = json.dumps(payload) except TypeError: @@ -182,7 +182,7 @@ def evaluate_promise(self, promiser, attributes, metadata): headers["Content-Length"] = os.path.getsize(path) # must be 'None' or bytes or file object - if type(payload) == str: + if isinstance(payload, str): payload = payload.encode("utf-8") request = urllib.request.Request( diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index 1b05864a..9dfb2627 100644 --- a/promise-types/symlinks/symlinks.py +++ b/promise-types/symlinks/symlinks.py @@ -66,7 +66,7 @@ def evaluate_promise(self, promiser, attributes, metadata): "'{}' is already unlinked from its old target".format(promiser) ) return Result.NOT_KEPT - except Exception: + except Exception as e: self.log_error( "'{}' has wrong target but couldn't be unlinked: {}".format( promiser, e diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index 7b2821ad..1f57ce3f 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -1,11 +1,10 @@ -import json import os import subprocess from enum import Enum from typing import Dict, List, Optional, Tuple -from cfengine_module_library import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, Result SYSTEMD_LIB_PATH = "/lib/systemd/system" @@ -518,7 +517,7 @@ def _render_service_template(self, model: object) -> str: value = getattr(model, attr) if value is None: continue - elif type(value) == list: + elif isinstance(value, list): for item in value: blocks[block].append("{key}={item}".format(key=key, item=item)) else: From 74be3e574802d0e01bb228ea61b3fbdce21cbe59 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 17 Feb 2026 15:17:13 +0100 Subject: [PATCH 066/146] Fixed pyright errors Signed-off-by: Victor Moene --- libraries/python/cfengine_module_library.py | 2 + promise-types/ansible/ansible_promise.py | 12 ++--- promise-types/git/git.py | 25 +++++++--- promise-types/http/http_promise_type.py | 10 ++-- promise-types/iptables/iptables.py | 3 +- promise-types/systemd/systemd.py | 54 +++++++++++++-------- 6 files changed, 67 insertions(+), 39 deletions(-) diff --git a/libraries/python/cfengine_module_library.py b/libraries/python/cfengine_module_library.py index 148a9744..da6a28ef 100644 --- a/libraries/python/cfengine_module_library.py +++ b/libraries/python/cfengine_module_library.py @@ -226,6 +226,8 @@ def _handle_request(self, request): "debug", ] + promiser = None + attributes = {} if operation in ["validate_promise", "evaluate_promise"]: promiser = request["promiser"] attributes = request.get("attributes", {}) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 5f9e39a2..b51363b2 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -5,7 +5,7 @@ from cfengine_module_library import PromiseModule, ValidationError, Result try: - from ansible import context + import ansible.context as context from ansible.cli import CLI from ansible.executor.playbook_executor import PlaybookExecutor from ansible.inventory.manager import InventoryManager @@ -27,7 +27,7 @@ class CallbackModule(CallbackBase): CALLBACK_TYPE = "stdout" CALLBACK_NAME = "cfengine" - def __init__(self, *args, promise=None, **kw): + def __init__(self, *args, promise, **kw): self.promise = promise self.hosts = set() self.changed = False @@ -51,7 +51,7 @@ def v2_runner_on_ok(self, result): "Task '" + result.task_name + "' didn't change" ) - def v2_runner_on_failed(self, result, **_): + def v2_runner_on_failed(self, result, ignore_errors=False): self.promise.log_error("Task '" + result.task_name + "' failed") def v2_runner_on_skipped(self, result): @@ -65,8 +65,8 @@ def v2_playbook_on_stats(self, stats): ) if summary_dict.get("unreachable"): self.promise.log_error("Host '" + host + "' is unreachable") - else: - summary and self.promise.log_verbose( + elif summary: + self.promise.log_verbose( "Summary of the tasks for '" + host + "' is: " + summary ) @@ -153,7 +153,7 @@ def evaluate_promise( passwords={}, ) callback = CallbackModule(promise=self) - pbex._tqm._stdout_callback = callback + pbex._tqm._stdout_callback = callback # type: ignore exit_code = pbex.run() if exit_code != 0: diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 3e209d6b..9ed4cc5a 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -4,7 +4,12 @@ from typing import Dict, List, Optional -from cfengine_module_library import PromiseModule, ValidationError, Result +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) class GitPromiseTypeModule(PromiseModule): @@ -93,7 +98,8 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): result = Result.REPAIRED except subprocess.CalledProcessError as e: self.log_error("Failed clone: {error}".format(error=e.output or e)) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -134,7 +140,8 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): result = Result.REPAIRED except subprocess.CalledProcessError as e: self.log_error("Failed reset: {error}".format(error=e.output or e)) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -228,7 +235,8 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): ) except subprocess.CalledProcessError as e: self.log_error("Failed fetch: {error}".format(error=e.output or e)) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -241,7 +249,9 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): # everything okay return (result, classes) - def _git(self, model: object, args: List[str], cwd: Optional[str] = None) -> str: + def _git( + self, model: AttributeObject, args: List[str], cwd: Optional[str] = None + ) -> str: self.log_verbose("Run: {cmd}".format(cmd=" ".join(args))) output = ( subprocess.check_output( @@ -253,10 +263,11 @@ def _git(self, model: object, args: List[str], cwd: Optional[str] = None) -> str .strip() .decode("utf-8") ) - output != "" and self.log_verbose(output) + if output != "": + self.log_verbose(output) return output - def _git_envvars(self, model: object): + def _git_envvars(self, model: AttributeObject): env = os.environ.copy() env["GIT_SSH_COMMAND"] = model.ssh_executable if model.ssh_options: diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index acd1bab2..aaa7b852 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -2,7 +2,7 @@ import filecmp import os -import urllib +import urllib.error import urllib.request import ssl import json @@ -10,7 +10,6 @@ from cfengine_module_library import PromiseModule, ValidationError, Result - _SUPPORTED_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"} @@ -179,7 +178,7 @@ def evaluate_promise(self, promiser, attributes, metadata): ) if "Content-Length" not in headers: - headers["Content-Length"] = os.path.getsize(path) + headers["Content-Length"] = str(os.path.getsize(path)) # must be 'None' or bytes or file object if isinstance(payload, str): @@ -194,8 +193,9 @@ def evaluate_promise(self, promiser, attributes, metadata): # convert to a boolean insecure = insecure.lower() == "true" if insecure: - SSL_context = ssl.SSLContext() - SSL_context.verify_method = ssl.CERT_NONE + SSL_context = ssl.create_default_context() + SSL_context.check_hostname = False + SSL_context.verify_mode = ssl.CERT_NONE try: with urllib.request.urlopen(request, context=SSL_context) as url_req: diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 80018131..24489221 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -222,7 +222,8 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): if result == Result.NOT_KEPT: classes.append("{}_{}_failed".format(safe_promiser, command)) elif result in {Result.KEPT, Result.REPAIRED}: - result == Result.REPAIRED and self.log_info(model.log_str) + if result == Result.REPAIRED: + self.log_info(model.log_str) classes.append("{}_{}_successful".format(safe_promiser, command)) else: diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index 1f57ce3f..25f9c7dc 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -4,8 +4,7 @@ from enum import Enum from typing import Dict, List, Optional, Tuple -from cfengine_module_library import PromiseModule, Result - +from cfengine_module_library import PromiseModule, Result, AttributeObject SYSTEMD_LIB_PATH = "/lib/systemd/system" @@ -103,7 +102,8 @@ def evaluate_promise( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, ["{safe_promiser}_show_failed".format(safe_promiser=safe_promiser)], @@ -115,7 +115,7 @@ def evaluate_promise( return self._service_present(model, safe_promiser, service_status) def _service_absent( - self, model: object, safe_promiser: str, service_status: dict + self, model: AttributeObject, safe_promiser: str, service_status: dict ) -> Tuple[str, List[str]]: classes = [] result = Result.KEPT @@ -130,7 +130,8 @@ def _service_absent( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, ["{safe_promiser}_stop_failed".format(safe_promiser=safe_promiser)], @@ -146,7 +147,8 @@ def _service_absent( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -189,7 +191,8 @@ def _service_absent( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -203,7 +206,7 @@ def _service_absent( return (result, classes) def _service_present( - self, model: object, safe_promiser: str, service_status: dict + self, model: AttributeObject, safe_promiser: str, service_status: dict ) -> Tuple[str, List[str]]: classes = [] result = Result.KEPT @@ -244,7 +247,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -265,7 +269,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -286,7 +291,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, ["{safe_promiser}_mask_failed".format(safe_promiser=safe_promiser)], @@ -303,7 +309,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -330,7 +337,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -357,7 +365,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -383,7 +392,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -410,7 +420,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, ["{safe_promiser}_stop_failed".format(safe_promiser=safe_promiser)], @@ -429,7 +440,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -452,7 +464,8 @@ def _service_present( self.log_error( "Failed to run systemctl: {error}".format(error=e.output or e) ) - e.stderr and self.log_error(e.stderr.strip()) + if e.stderr: + self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, [ @@ -476,10 +489,11 @@ def _exec_command(self, args: List[str], cwd: Optional[str] = None) -> str: .strip() .decode("utf-8") ) - output != "" and self.log_verbose(output) + if output != "": + self.log_verbose(output) return output - def _render_service_template(self, model: object) -> str: + def _render_service_template(self, model: AttributeObject) -> str: blocks = { "unit": [], "service": [], From 5d054f16c7c3238574ffed4a6bbb6de2a14bdc57 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 17 Feb 2026 15:24:54 +0100 Subject: [PATCH 067/146] Fixed ansible pyright error and unknown function error Fixed the way ansible promise type handldes missing ansible dependency to be compatible with pyright. Moreover, Ansible doesn't error anymore on unknown functionn init_plugin_loader() when ansible is not installed. Signed-off-by: Victor Moene --- promise-types/ansible/ansible_promise.py | 214 ++++++++++++----------- 1 file changed, 111 insertions(+), 103 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index b51363b2..fea29db0 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -1,7 +1,6 @@ import os from typing import Dict, Tuple, List - from cfengine_module_library import PromiseModule, ValidationError, Result try: @@ -15,13 +14,6 @@ from ansible.vars.manager import VariableManager from ansible.plugins.loader import init_plugin_loader - ANSIBLE_AVAILABLE = True -except ImportError: - ANSIBLE_AVAILABLE = False - - -if ANSIBLE_AVAILABLE: - class CallbackModule(CallbackBase): CALLBACK_VERSION = 1.0 CALLBACK_TYPE = "stdout" @@ -70,101 +62,117 @@ def v2_playbook_on_stats(self, stats): "Summary of the tasks for '" + host + "' is: " + summary ) + class AnsiblePromiseTypeModule(PromiseModule): + def __init__(self, **kwargs): + super(AnsiblePromiseTypeModule, self).__init__( + "ansible_promise_module", "0.0.0", **kwargs + ) + + def must_be_absolute(v): + if not os.path.isabs(v): + raise ValidationError( + "Must be an absolute path, not '{v}'".format(v=v) + ) + + self.add_attribute( + "playbook", str, default_to_promiser=True, validator=must_be_absolute + ) + self.add_attribute("inventory", str, validator=must_be_absolute) + self.add_attribute("limit", list, default=["localhost"]) + self.add_attribute("tags", list, default=[]) + self.add_attribute("become", bool, default=False) + self.add_attribute("become_method", str, default="sudo") + self.add_attribute("become_user", str, default="root") + self.add_attribute("connection", str, default="local") + self.add_attribute("forks", int, default=1) + self.add_attribute("private_key_file", str, validator=must_be_absolute) + self.add_attribute("remote_user", str, default="root") + + def prepare_promiser_and_attributes(self, promiser, attributes): + safe_promiser = promiser.replace(",", "_") + return (safe_promiser, attributes) + + def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): + return + + def evaluate_promise( + self, safe_promiser: str, attributes: Dict, metadata: Dict + ) -> Tuple[str, List[str]]: + model = self.create_attribute_object(safe_promiser, attributes) + + classes = [] + result = Result.KEPT + + context.CLIARGS = ImmutableDict( + tags=model.tags, + listtags=False, + listtasks=False, + listhosts=False, + syntax=False, + connection=model.connection, + module_path=None, + remote_user=model.remote_user, + private_key_file=model.private_key_file, + ssh_common_args=None, + ssh_extra_args=None, + sftp_extra_args=None, + scp_extra_args=None, + become=model.become, + become_method=model.become_method, + become_user=model.become_user, + forks=model.forks, + verbosity=0, + check=False, + start_at_task=None, + ) + + loader = DataLoader() + inventory = InventoryManager( + loader=loader, + sources=(model.inventory,) if model.inventory else (), + ) + + variable_manager = VariableManager( + loader=loader, + inventory=inventory, + version_info=CLI.version_info(gitinfo=False), + ) + pbex = PlaybookExecutor( + playbooks=[attributes["playbook"]], + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + passwords={}, + ) + callback = CallbackModule(promise=self) + pbex._tqm._stdout_callback = callback # type: ignore + + exit_code = pbex.run() + if exit_code != 0: + classes.append( + "{safe_promiser}_failed".format(safe_promiser=safe_promiser) + ) + result = Result.NOT_KEPT + elif callback.changed: + result = Result.REPAIRED + + return (result, classes) + + if __name__ == "__main__": + init_plugin_loader() + AnsiblePromiseTypeModule().start() + +except ModuleNotFoundError: + + class UnavailableAnsiblePromiseTypeModule(PromiseModule): + + def __init__(self, **kwargs): + super(UnavailableAnsiblePromiseTypeModule, self).__init__( + "ansible_promise_module", "0.0.0", **kwargs + ) -class AnsiblePromiseTypeModule(PromiseModule): - def __init__(self, **kwargs): - super(AnsiblePromiseTypeModule, self).__init__( - "ansible_promise_module", "0.0.0", **kwargs - ) - - def must_be_absolute(v): - if not os.path.isabs(v): - raise ValidationError("Must be an absolute path, not '{v}'".format(v=v)) - - self.add_attribute( - "playbook", str, default_to_promiser=True, validator=must_be_absolute - ) - self.add_attribute("inventory", str, validator=must_be_absolute) - self.add_attribute("limit", list, default=["localhost"]) - self.add_attribute("tags", list, default=[]) - self.add_attribute("become", bool, default=False) - self.add_attribute("become_method", str, default="sudo") - self.add_attribute("become_user", str, default="root") - self.add_attribute("connection", str, default="local") - self.add_attribute("forks", int, default=1) - self.add_attribute("private_key_file", str, validator=must_be_absolute) - self.add_attribute("remote_user", str, default="root") - - def prepare_promiser_and_attributes(self, promiser, attributes): - safe_promiser = promiser.replace(",", "_") - return (safe_promiser, attributes) - - def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): - if not ANSIBLE_AVAILABLE: + def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): raise ValidationError("Ansible Python module not available") - def evaluate_promise( - self, safe_promiser: str, attributes: Dict, metadata: Dict - ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(safe_promiser, attributes) - - classes = [] - result = Result.KEPT - - context.CLIARGS = ImmutableDict( - tags=model.tags, - listtags=False, - listtasks=False, - listhosts=False, - syntax=False, - connection=model.connection, - module_path=None, - remote_user=model.remote_user, - private_key_file=model.private_key_file, - ssh_common_args=None, - ssh_extra_args=None, - sftp_extra_args=None, - scp_extra_args=None, - become=model.become, - become_method=model.become_method, - become_user=model.become_user, - forks=model.forks, - verbosity=0, - check=False, - start_at_task=None, - ) - - loader = DataLoader() - inventory = InventoryManager( - loader=loader, - sources=(model.inventory,) if model.inventory else (), - ) - - variable_manager = VariableManager( - loader=loader, - inventory=inventory, - version_info=CLI.version_info(gitinfo=False), - ) - pbex = PlaybookExecutor( - playbooks=[attributes["playbook"]], - inventory=inventory, - variable_manager=variable_manager, - loader=loader, - passwords={}, - ) - callback = CallbackModule(promise=self) - pbex._tqm._stdout_callback = callback # type: ignore - - exit_code = pbex.run() - if exit_code != 0: - classes.append("{safe_promiser}_failed".format(safe_promiser=safe_promiser)) - result = Result.NOT_KEPT - elif callback.changed: - result = Result.REPAIRED - - return (result, classes) - - -if __name__ == "__main__": - init_plugin_loader() - AnsiblePromiseTypeModule().start() + if __name__ == "__main__": + UnavailableAnsiblePromiseTypeModule().start() From b3c5ab3b89702ccab953155b3ed8a78dec4013e7 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 23 Feb 2026 14:12:06 +0100 Subject: [PATCH 068/146] Added module: command-dispatcher, allows shell commands to be run Ticket: CFE-4627 Signed-off-by: Simon Halvorsen --- cfbs.json | 42 +++++++++++++++++ management/command-dispatcher/main.cf | 65 +++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 management/command-dispatcher/main.cf diff --git a/cfbs.json b/cfbs.json index e05a954c..a03db16c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -33,6 +33,48 @@ "subdirectory": "reporting/client-initiated-reporting", "steps": ["json def.json def.json"] }, + "command-dispatcher": { + "description": "Command dispatcher for running shell commands on schedule", + "subdirectory": "management/command-dispatcher", + "steps": [ + "copy main.cf services/cfbs/modules/command-dispatcher/main.cf", + "policy_files services/cfbs/command-dispatcher/main.cf", + "bundles command_dispatcher:main", + "input ./input.json def.json" + ], + "input": [ + { + "type": "list", + "variable": "commands_to_run", + "namespace": "command_dispatcher", + "bundle": "main", + "label": "Command", + "subtype": [ + { + "key": "command", + "type": "string", + "label": "Command", + "question": "Command to run" + }, + { + "key": "condition", + "type": "string", + "label": "Condition", + "question": "Condition for when to run", + "default": "any" + }, + { + "key": "ifelapsed", + "type": "string", + "label": "ifelapsed", + "question": "Number of minutes between promise assessments", + "default": "5" + } + ], + "while": "Do you want to specify more commands to be run?" + } + ] + }, "conditional-installer": { "description": "Allows you to specify packages you want installed and conditions for where you want them installed, as well as a list of packages you generally want uninstalled.", "subdirectory": "security/conditional-installer", diff --git a/management/command-dispatcher/main.cf b/management/command-dispatcher/main.cf new file mode 100644 index 00000000..3f968053 --- /dev/null +++ b/management/command-dispatcher/main.cf @@ -0,0 +1,65 @@ +body file control +{ + namespace => "command_dispatcher"; +} +bundle agent main +{ + classes: + "enabled" + expression => isvariable("commands_to_run"); + "run_$(i)" + expression => "$(_condition[$(i)])"; + + vars: + enabled:: + "i" + slist => getindices(commands_to_run); + + "_command[$(i)]" + string => "$(commands_to_run[$(i)][command])", + if => isvariable("commands_to_run[$(i)][command]"); + + "_condition[$(i)]" + string => ifelse( + not(strcmp("$(commands_to_run[$(i)][condition])", "")), + "$(commands_to_run[$(i)][condition])", + "any"); + + "_ifelapsed[$(i)]" + string => ifelse( + not(strcmp("$(commands_to_run[$(i)][ifelapsed])", "")), + "$(commands_to_run[$(i)][ifelapsed])", + "5"); + + reports: + enabled:: + "Command [$(i)]: $(_command[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + !enabled:: + "Command-dispatcher: commands_to_run variable not found"; + + commands: + enabled:: + "$(_command[$(i)])" + if => "run_$(i)", + action => ifelapsed("$(_ifelapsed[$(i)])"), + contain => in_shell; +} +body contain in_shell +{ + useshell => "true"; + exec_owner => "root"; + exec_timeout => "300"; +} +body action ifelapsed(x) +{ + ifelapsed => "$(x)"; +} +body file control +{ + namespace => "default"; +} +bundle agent __main__ +{ + methods: + "command_dispatcher:main"; +} From ba76e3022e7c4a996af393798eb0a367453be6f2 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 23 Feb 2026 14:12:06 +0100 Subject: [PATCH 069/146] Added module: command-dispatcher, allows shell commands to be run Ticket: CFE-4627 Signed-off-by: Simon Halvorsen --- management/command-dispatcher/README.md | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 management/command-dispatcher/README.md diff --git a/management/command-dispatcher/README.md b/management/command-dispatcher/README.md new file mode 100644 index 00000000..15e0e2b3 --- /dev/null +++ b/management/command-dispatcher/README.md @@ -0,0 +1,39 @@ +This module enables the running of shell commands based on input from the Mission-Portal's build page or `cfbs input`. + +Internally these commands are ran using the `"useshell"`-promise based on the `condition`-variable. + +**Note:** +The commands are dispatched with root-privilege (uid=0) and a timeout-window of 300 seconds. + +--- + +**Usage:** + +* `command` - The command to run. + +* `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` + +* `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. + +E.g. +```json +... +{ +"command": "echo \"Hello World\"", +"condition": "linux", +"ifelapsed": "5" +}, +... +``` +Would echo "Hello world" on every linux device with 5 minute intervals. + +--- + +## Contribute + +Feel free to open pull requests to expand this documentation, add features or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://tracker.mender.io/issues/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. From 6310f37e61a62a1bdf5f6fd5e907cb6ae3382ce9 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Wed, 4 Mar 2026 14:40:46 +0100 Subject: [PATCH 070/146] fix wrong path command-dispatcher policy_files-value --- cfbs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfbs.json b/cfbs.json index 17b8a1a0..9a1f191e 100644 --- a/cfbs.json +++ b/cfbs.json @@ -38,7 +38,7 @@ "subdirectory": "management/command-dispatcher", "steps": [ "copy main.cf services/cfbs/modules/command-dispatcher/main.cf", - "policy_files services/cfbs/command-dispatcher/main.cf", + "policy_files services/cfbs/modules/command-dispatcher/main.cf", "bundles command_dispatcher:main", "input ./input.json def.json" ], From c817bb337e8acdc932f7029936af6b0c4d7dc9bb Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 11 Mar 2026 13:48:59 +0100 Subject: [PATCH 071/146] Add extraPaths to pyrightconfig for local library resolution Signed-off-by: Lars Erik Wik --- pyrightconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index 389015c8..3ab68711 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,3 +1,4 @@ { - "reportMissingImports": "none" + "reportMissingImports": "none", + "extraPaths": ["libraries/python"] } From 2430d9dce2a9a33ba27ebe154feb2fa9869f1753 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 12 Mar 2026 10:37:40 +0100 Subject: [PATCH 072/146] Custom promise type for managing global sshd configuration Ticket: ENT-13797 Signed-off-by: Lars Erik Wik --- cfbs.json | 9 + promise-types/sshd/LICENSE | 21 ++ promise-types/sshd/README.md | 63 ++++ promise-types/sshd/enable.cf | 6 + promise-types/sshd/example.cf | 30 ++ promise-types/sshd/sshd_promise_type.py | 319 +++++++++++++++++++ promise-types/sshd/test_sshd_promise_type.py | 165 ++++++++++ 7 files changed, 613 insertions(+) create mode 100644 promise-types/sshd/LICENSE create mode 100644 promise-types/sshd/README.md create mode 100644 promise-types/sshd/enable.cf create mode 100644 promise-types/sshd/example.cf create mode 100644 promise-types/sshd/sshd_promise_type.py create mode 100644 promise-types/sshd/test_sshd_promise_type.py diff --git a/cfbs.json b/cfbs.json index 9a1f191e..a957671f 100644 --- a/cfbs.json +++ b/cfbs.json @@ -302,6 +302,15 @@ "append enable.cf services/init.cf" ] }, + "promise-type-sshd": { + "description": "Promise type to configure sshd.", + "subdirectory": "promise-types/sshd", + "dependencies": ["library-for-promise-types-in-python"], + "steps": [ + "copy sshd_promise_type.py modules/promises/", + "append enable.cf services/init.cf" + ] + }, "promise-type-systemd": { "description": "Promise type to manage systemd services.", "subdirectory": "promise-types/systemd", diff --git a/promise-types/sshd/LICENSE b/promise-types/sshd/LICENSE new file mode 100644 index 00000000..eb2ada6d --- /dev/null +++ b/promise-types/sshd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Northern.tech + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/promise-types/sshd/README.md b/promise-types/sshd/README.md new file mode 100644 index 00000000..c7eac1b8 --- /dev/null +++ b/promise-types/sshd/README.md @@ -0,0 +1,63 @@ +# `sshd` promise type + +Configures sshd and restarts the service when configuration changes. + +## Promiser +The sshd configuration keyword to manage (e.g. `PermitRootLogin`, `AllowUsers`). +Each promise manages a single directive in the drop-in config file. + +## Attributes +- `value` (required) — the value for the directive, either a string or an slist + +## What the module manages internally +1. **Include directive** — ensures the base `sshd_config` includes the drop-in directory (`sshd_config.d/`) as its first non-comment directive +2. **Drop-in directory** — creates the drop-in directory if it doesn't exist +3. **Drop-in file** — writes directives to `sshd_config.d/00-cfengine.conf` +4. **Service restart** — restarts sshd if configuration was changed and the service is already running +5. **Verification** — verifies the desired directive appears in the effective sshd config (`sshd -T`) + +## Conflicting promisers +Having multiple promises with the same sshd keyword is not recommended. +In case of conflicting promisers, the agent will attempt to converge the correct state for each one in the order they are evaluated. +This means the last promise wins and determines the final value in the configuration file. +It will also cause multiple restarts of the sshd service, which may be disruptive. + +## What the module does NOT do +- Install sshd — that is a `packages:` promise +- Ensure sshd is running — that is a `services:` promise +- Manage match blocks — those are a policy-level concern + +## Policy +```cf3 +bundle agent sshd_config +{ + packages: + "openssh-server" policy => "present"; + + services: + "sshd" service_policy => "start"; + + vars: + "allowed_users" slist => { "alice", "bob" }; + + sshd: + "PermitRootLogin" value => "no"; + "PasswordAuthentication" value => "no"; + "Port" value => "22"; + "AllowUsers" value => @(allowed_users); +} +``` + +## Authors + +This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. +Thanks everyone! + +## Contribute + +Feel free to open pull requests to expand this documentation, add features, or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. diff --git a/promise-types/sshd/enable.cf b/promise-types/sshd/enable.cf new file mode 100644 index 00000000..bc3ea19f --- /dev/null +++ b/promise-types/sshd/enable.cf @@ -0,0 +1,6 @@ +promise agent sshd +# @brief Define sshd promise type +{ + path => "$(sys.workdir)/modules/promises/sshd_promise_type.py"; + interpreter => "/usr/bin/python3"; +} diff --git a/promise-types/sshd/example.cf b/promise-types/sshd/example.cf new file mode 100644 index 00000000..3e83b4bf --- /dev/null +++ b/promise-types/sshd/example.cf @@ -0,0 +1,30 @@ +promise agent sshd +# @brief Define sshd promise type +{ + path => "$(sys.workdir)/modules/promises/sshd_promise_type.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent example +{ + packages: + "openssh-server" policy => "present"; + + services: + "sshd" service_policy => "start"; + + vars: + "allowed_users" slist => { "alice", "bob" }; + + sshd: + "PermitRootLogin" value => "no"; + "PasswordAuthentication" value => "no"; + "Port" value => "22"; + "AllowUsers" value => @(allowed_users); +} + +bundle agent __main__ +{ + methods: + "example"; +} diff --git a/promise-types/sshd/sshd_promise_type.py b/promise-types/sshd/sshd_promise_type.py new file mode 100644 index 00000000..d573a098 --- /dev/null +++ b/promise-types/sshd/sshd_promise_type.py @@ -0,0 +1,319 @@ +import os +import re +import subprocess +import tempfile + +from cfengine_module_library import PromiseModule, ValidationError, Result + + +BASE_CONFIG = "/etc/ssh/sshd_config" +DROP_IN_DIR = "/etc/ssh/sshd_config.d/" +CFE_CONFIG = os.path.join(DROP_IN_DIR, "00-cfengine.conf") + + +# TODO: Add a "restart" attribute (default: True) to allow overriding the +# automatic restart of sshd after configuration changes. +# TODO: Add a "start" attribute (default: False) to optionally start the sshd +# service if it is not already running. +# TODO: Append the policy comment (e.g. "# Promised by CFEngine") as a trailing +# comment on each directive written to the drop-in file. + +REQUIRED_ATTRIBUTES = ("value",) # Add required attributes here +ACCEPTED_ATTRIBUTES = REQUIRED_ATTRIBUTES + () # Add optional attributes here + + +def sshd_quote(value: str) -> str: + """Quote a string for sshd_config. Values containing whitespace, '#', or + '\"' are wrapped in double quotes, with internal backslashes and double + quotes escaped.""" + if not value: + return '""' + if re.search(r'[\s#"]', value): + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + return value + + +def to_sshd_value(value) -> str: + """Convert a Python value to an sshd config value. Lists are space-joined, + individual strings are quoted when necessary.""" + if isinstance(value, list): + return " ".join(sshd_quote(v) for v in value) + if isinstance(value, str): + return sshd_quote(value) + raise TypeError(f"Expected str or list[str], got {type(value).__name__}") + + +def try_unlink(path: str): + """Remove a file, ignoring errors if it no longer exists.""" + try: + os.unlink(path) + except OSError: + pass + + +def is_drop_in_directive(directive: str) -> bool: + """Check if a directive is an Include for the drop-in config directory.""" + m = re.match( + rf"include(\s+|\s*=\s*){re.escape(DROP_IN_DIR)}\*\.conf", + directive.strip(), + re.IGNORECASE, + ) + return m is not None + + +def update_result(old: str, new: str) -> str: + """Return the worst of two results. Severity: KEPT < REPAIRED < NOT_KEPT.""" + if old == Result.NOT_KEPT or new == Result.NOT_KEPT: + return Result.NOT_KEPT + if old == Result.REPAIRED or new == Result.REPAIRED: + return Result.REPAIRED + return Result.KEPT + + +def get_first_directive(lines: list[str]) -> str | None: + """Return the first non-comment, non-empty directive, or None if not found.""" + for line in lines: + stripped = line.strip() + if stripped and not stripped.startswith("#"): + return stripped + return None + + +class SshdPromiseTypeModule(PromiseModule): + def __init__(self): + super().__init__("sshd_promise_module", "0.0.0") + + def validate_promise( + self, promiser: str, attributes: dict[str, object], metadata: dict[str, str] + ): + # Check that promiser is a valid sshd keyword + if not re.fullmatch(r"[a-zA-Z0-9]+", promiser): + raise ValidationError( + f"Promiser '{promiser}' must be a valid sshd keyword containing only letters and numbers" + ) + + # Check for unknown attributes + for attr in attributes: + if attr not in ACCEPTED_ATTRIBUTES: + raise ValidationError(f"Attribute '{attr}' is NOT accepted") + + # Check for any missing required attributes + for attr in REQUIRED_ATTRIBUTES: + if attr not in attributes: + raise ValidationError(f"Missing required attribute '{attr}'") + + # Check type of 'value' attributes + value = attributes.get("value") + if not isinstance(value, (str, list)): + raise ValidationError("Attribute 'value' must be a string or an slist") + + # Make sure 'value' attribute is not empty + if not value: + raise ValidationError("Attribute 'value' cannot be empty") + + def validate_config(self, filename: str) -> bool: + """Validate the sshd syntax on a file""" + r = subprocess.run( + ["/usr/sbin/sshd", "-t", "-f", filename], + capture_output=True, + text=True, + ) + if r.returncode != 0: + self.log_error(f"Configuration validation failed: {r.stderr.strip()}") + return False + return True + + def safe_write_config(self, path: str, lines: list[str]) -> bool: + """Atomically write config lines to a temporary file and replace the + target only if sshd validates the syntax successfully.""" + directory = os.path.dirname(path) + base = os.path.basename(path) + prefix, suffix = os.path.splitext(base) + + fd, tmp_path = tempfile.mkstemp(prefix=prefix, suffix=suffix, dir=directory) + try: + os.fchmod(fd, 0o600) # rw------- + with os.fdopen(fd, "w") as f: + f.writelines(lines) + f.flush() # Push data to kernel buffer so sshd can read it + success = self.validate_config(tmp_path) + if success: + os.replace(tmp_path, path) + return success + finally: + try_unlink(tmp_path) + + def ensure_include_directive(self) -> str: + """Ensure the base sshd config includes the drop-in directory.""" + try: + with open(BASE_CONFIG, "r") as f: + lines = f.readlines() + except FileNotFoundError: + self.log_error(f"Base configuration file '{BASE_CONFIG}' does not exist") + return Result.NOT_KEPT + + first_directive = get_first_directive(lines) + + if (first_directive is None) or (not is_drop_in_directive(first_directive)): + include_directive = f"Include {DROP_IN_DIR}*.conf" + self.log_debug( + f"Expected first directive in '{BASE_CONFIG}' to be '{include_directive}'" + ) + + lines.insert(0, f"{include_directive} # Added by CFEngine\n") + try: + if not self.safe_write_config(BASE_CONFIG, lines): + # Error already logged + return Result.NOT_KEPT + except Exception as e: + self.log_error(f"Failed to write '{BASE_CONFIG}': {e}") + return Result.NOT_KEPT + + self.log_info(f"Added include directive to '{BASE_CONFIG}'") + return Result.REPAIRED + + return Result.KEPT + + def ensure_drop_in_dir(self) -> str: + """Ensure the drop-in config directory exists.""" + if os.path.isdir(DROP_IN_DIR): + return Result.KEPT + + try: + os.makedirs(DROP_IN_DIR, mode=0o755) # rwxr-xr-x + except Exception as e: + self.log_error(f"Failed to create drop-in directory '{DROP_IN_DIR}': {e}") + return Result.NOT_KEPT + + self.log_info(f"Created drop-in directory '{DROP_IN_DIR}'") + return Result.REPAIRED + + def ensure_drop_in_config(self, keyword: str, value: str | list[str]) -> str: + """Write the CFEngine drop-in config file with the given attribute""" + + try: + with open(CFE_CONFIG, "r") as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + + # Remove conflicting directives + lines = [line for line in lines if not line.lower().startswith(keyword.lower())] + + # Remove the disclaimer so that we can put it back on top + lines = [line for line in lines if not line.startswith("# ")] + + # Add the promised directive to the top (just after the disclaimer) + disclaimer = ["# Managed by CFEngine\n", "# Do NOT manually edit this file\n"] + lines = disclaimer + [f"{keyword} {to_sshd_value(value)}\n"] + lines + + try: + if not self.safe_write_config(CFE_CONFIG, lines): + return Result.NOT_KEPT + except Exception as e: + self.log_error(f"Failed to write drop-in config '{CFE_CONFIG}': {e}") + return Result.NOT_KEPT + + self.log_info(f"Updated drop-in config '{CFE_CONFIG}'") + return Result.REPAIRED + + def restart_sshd(self) -> str: + """Restart the sshd service if it is currently running.""" + r = subprocess.run( + ["systemctl", "is-active", "--quiet", "sshd"], + ) + if r.returncode != 0: + # If sshd is not running, do nothing + self.log_debug("The service sshd is not running") + return Result.KEPT + + r = subprocess.run( + ["systemctl", "restart", "--quiet", "sshd"], + ) + if r.returncode != 0: + self.log_error("Failed to restart sshd service") + return Result.NOT_KEPT + + self.log_info("Restarted sshd service") + return Result.REPAIRED + + def effective_config_has_directive( + self, keyword: str, values: str | list[str] + ) -> bool: + """Check if the running sshd effective configuration (via sshd -T) + contains the given directive(s) for a keyword.""" + r = subprocess.run( + ["/usr/sbin/sshd", "-T"], + capture_output=True, + text=True, + ) + if r.returncode != 0: + self.log_error("Failed to get effective sshd config") + return False + effective = r.stdout.strip().splitlines() + + # sshd -T splits multi-argument keywords into separate + # lines (e.g. "AllowUsers user1 user2" becomes two lines: + # "allowusers user1" and "allowusers user2"), so we must + # expand list values into individual directives to match + # the effective config format for set comparison. + + for value in values if isinstance(values, list) else [values]: + assert isinstance(value, str) + directive = f"{keyword.lower()} {to_sshd_value(value)}" + if directive in effective: + self.log_debug( + f"Directive '{directive}' is present in effective sshd config" + ) + else: + self.log_debug( + f"Directive '{directive}' is NOT present in effective sshd config" + ) + return False + + return True + + def verify_effective_config(self, keyword: str, values: str | list[str]) -> str: + """Verify the effective sshd config contains the expected directive, + returning KEPT on success or NOT_KEPT on failure.""" + if self.effective_config_has_directive(keyword, values): + self.log_verbose("Successfully verified effective sshd config") + return Result.KEPT + + self.log_error("Failed to verify effective sshd config") + return Result.NOT_KEPT + + def evaluate_promise( + self, promiser: str, attributes: dict[str, object], metadata: dict[str, str] + ) -> str: + assert "value" in attributes, "expected 'value' in attributes" + value = attributes["value"] + assert isinstance(value, (str, list)), "expected type str or list" + assert value, "expected non-empty str or list" + + # Check if the effective config already has the desired state + if self.effective_config_has_directive(promiser, value): + return Result.KEPT + + # Ensure the base config includes the drop-in directory + result = update_result(Result.KEPT, self.ensure_include_directive()) + + # Ensure the drop-in directory exists + result = update_result(result, self.ensure_drop_in_dir()) + + # Ensure the drop-in config file contains the desired directive + result = update_result(result, self.ensure_drop_in_config(promiser, value)) + + # Restart sshd only if configuration was changed + if result == Result.REPAIRED: + result = update_result(result, self.restart_sshd()) + + # Verify the effective config matches the desired state + result = update_result(result, self.verify_effective_config(promiser, value)) + + return result + + +if __name__ == "__main__": + SshdPromiseTypeModule().start() diff --git a/promise-types/sshd/test_sshd_promise_type.py b/promise-types/sshd/test_sshd_promise_type.py new file mode 100644 index 00000000..32354ef0 --- /dev/null +++ b/promise-types/sshd/test_sshd_promise_type.py @@ -0,0 +1,165 @@ +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), "../../libraries/python")) + +from cfengine_module_library import Result # noqa: E402 + +from sshd_promise_type import ( # noqa: E402 + sshd_quote, + to_sshd_value, + get_first_directive, + is_drop_in_directive, + update_result, + DROP_IN_DIR, +) + + +def test_sshd_quote_simple(): + assert sshd_quote("no") == "no" + + +def test_sshd_quote_empty(): + assert sshd_quote("") == '""' + + +def test_sshd_quote_space(): + assert sshd_quote("some value") == '"some value"' + + +def test_sshd_quote_tab(): + assert sshd_quote("some\tvalue") == '"some\tvalue"' + + +def test_sshd_quote_hash(): + assert sshd_quote("before#after") == '"before#after"' + + +def test_sshd_quote_double_quote(): + assert sshd_quote('say "hello"') == '"say \\"hello\\""' + + +def test_sshd_quote_backslash(): + assert sshd_quote("path\\to") == "path\\to" + + +def test_sshd_quote_backslash_and_space(): + assert sshd_quote("path\\to dir") == '"path\\\\to dir"' + + +def test_to_sshd_value_str(): + assert to_sshd_value("no") == "no" + + +def test_to_sshd_value_str_with_spaces(): + assert to_sshd_value("some value") == '"some value"' + + +def test_to_sshd_value_list(): + assert to_sshd_value(["user1", "user2"]) == "user1 user2" + + +def test_to_sshd_value_list_with_quoting(): + assert to_sshd_value(["user1", "user 2"]) == 'user1 "user 2"' + + +def test_get_first_directive(): + lines = ["# comment\n", "PermitRootLogin no\n", "Port 22\n"] + assert get_first_directive(lines) == "PermitRootLogin no" + + +def test_get_first_directive_no_comments(): + lines = ["PermitRootLogin no\n", "Port 22\n"] + assert get_first_directive(lines) == "PermitRootLogin no" + + +def test_get_first_directive_all_comments(): + lines = ["# comment\n", "# another comment\n"] + assert get_first_directive(lines) is None + + +def test_get_first_directive_empty(): + assert get_first_directive([]) is None + + +def test_get_first_directive_extra_whitespace(): + lines = ["# comment\n", "PermitRootLogin no\n"] + assert get_first_directive(lines) == "PermitRootLogin no" + + +def test_get_first_directive_equal_sign(): + lines = ["# comment\n", "PermitRootLogin=no\n"] + assert get_first_directive(lines) == "PermitRootLogin=no" + + +def test_get_first_directive_blank_lines(): + lines = ["\n", " \n", "# comment\n", "Port 22\n"] + assert get_first_directive(lines) == "Port 22" + + +def test_is_drop_in_directive_space(): + assert is_drop_in_directive(f"Include {DROP_IN_DIR}*.conf") + + +def test_is_drop_in_directive_equal(): + assert is_drop_in_directive(f"Include={DROP_IN_DIR}*.conf") + + +def test_is_drop_in_directive_space_equal_space(): + assert is_drop_in_directive(f"Include = {DROP_IN_DIR}*.conf") + + +def test_is_drop_in_directive_case_insensitive(): + assert is_drop_in_directive(f"include {DROP_IN_DIR}*.conf") + + +def test_is_drop_in_directive_extra_files(): + assert is_drop_in_directive(f"Include {DROP_IN_DIR}*.conf /other/path") + + +def test_is_drop_in_directive_wrong_path(): + assert not is_drop_in_directive("Include /other/path/*.conf") + + +def test_is_drop_in_directive_no_separator(): + assert not is_drop_in_directive(f"Include{DROP_IN_DIR}*.conf") + + +def test_is_drop_in_directive_not_include(): + assert not is_drop_in_directive("permitrootlogin no") + + +def test_update_result_kept_kept(): + assert update_result(Result.KEPT, Result.KEPT) == Result.KEPT + + +def test_update_result_kept_repaired(): + assert update_result(Result.KEPT, Result.REPAIRED) == Result.REPAIRED + + +def test_update_result_kept_not_kept(): + assert update_result(Result.KEPT, Result.NOT_KEPT) == Result.NOT_KEPT + + +def test_update_result_repaired_kept(): + assert update_result(Result.REPAIRED, Result.KEPT) == Result.REPAIRED + + +def test_update_result_repaired_repaired(): + assert update_result(Result.REPAIRED, Result.REPAIRED) == Result.REPAIRED + + +def test_update_result_repaired_not_kept(): + assert update_result(Result.REPAIRED, Result.NOT_KEPT) == Result.NOT_KEPT + + +def test_update_result_not_kept_kept(): + assert update_result(Result.NOT_KEPT, Result.KEPT) == Result.NOT_KEPT + + +def test_update_result_not_kept_repaired(): + assert update_result(Result.NOT_KEPT, Result.REPAIRED) == Result.NOT_KEPT + + +def test_update_result_not_kept_not_kept(): + assert update_result(Result.NOT_KEPT, Result.NOT_KEPT) == Result.NOT_KEPT From 8a4a1df531c31cae5a21785ae61ddc3ebae8e78f Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Tue, 31 Mar 2026 22:46:39 +0200 Subject: [PATCH 073/146] Removed replace_version steps in the wrong place Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cfbs.json b/cfbs.json index a957671f..a52c491a 100644 --- a/cfbs.json +++ b/cfbs.json @@ -206,9 +206,7 @@ "subdirectory": "libraries/python", "steps": [ "copy cfengine_module_library.py modules/promises/cfengine_module_library.py", - "replace_version 2 0.0.0 modules/promises/cfengine_module_library.py", - "copy cfengine_module_library.py modules/promises/cfengine.py", - "replace_version 2 0.0.0 modules/promises/cfengine.py" + "copy cfengine_module_library.py modules/promises/cfengine.py" ] }, "maintainers-in-motd": { From c519e80731bbcdcfa3e481537f620d134948f5c9 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:39:41 +0200 Subject: [PATCH 074/146] Added __getattr__ to AttributeObject for pyright compatibility AttributeObject uses dynamic setattr() in __init__, which pyright cannot analyze statically. Adding __getattr__ with an Any return type tells pyright that dynamic attribute access is valid, while preserving correct runtime behavior (AttributeError for truly missing attributes). Signed-off-by: Lars Erik Wik --- libraries/python/cfengine_module_library.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/python/cfengine_module_library.py b/libraries/python/cfengine_module_library.py index da6a28ef..3aa02eba 100644 --- a/libraries/python/cfengine_module_library.py +++ b/libraries/python/cfengine_module_library.py @@ -21,6 +21,7 @@ import traceback from copy import copy from collections import OrderedDict +from typing import Any _LOG_LEVELS = { level: idx @@ -91,6 +92,17 @@ def __init__(self, d): for key, value in d.items(): setattr(self, key, value) + # Python only calls __getattr__ as a fallback when normal attribute + # lookup (__getattribute__) has already failed, so attributes set via + # setattr() in __init__ are never affected. The -> Any return type + # tells pyright that dynamic attribute access is valid. + def __getattr__(self, name) -> Any: + raise AttributeError( + "'{}' object has no attribute '{}'".format( + self.__class__.__qualname__, name + ) + ) + def __repr__(self): return "{}({})".format( self.__class__.__qualname__, From a7628fca09f8255ce72095fc892713ce208b5c6c Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:03 +0200 Subject: [PATCH 075/146] Fixed typo Result.NOTKEPT -> Result.NOT_KEPT in gpg module Signed-off-by: Lars Erik Wik --- examples/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index abe80395..8930c315 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -141,7 +141,7 @@ def evaluate_promise(self, promiser, attributes, metadata): f"Importing ascii key for user id '{user_id}' into gpg homedir '{promiser}'" ) if self.gpg_import_ascii(promiser, key["ascii"]): - if result != Result.NOTKEPT: + if result != Result.NOT_KEPT: result = Result.REPAIRED else: self.log_error(f"Unable to import key for user id '{user_id}'") From e6b821ac1447c3b12f1d1cc04f1087d94908a3fb Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:32 +0200 Subject: [PATCH 076/146] Fixed evaluate_promise parameter name in ansible module Renamed safe_promiser to promiser to match the base class PromiseModule.evaluate_promise() signature. The parameter already receives the safe promiser via prepare_promiser_and_attributes(). Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index fea29db0..610d5ecd 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -96,9 +96,9 @@ def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): return def evaluate_promise( - self, safe_promiser: str, attributes: Dict, metadata: Dict + self, promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(safe_promiser, attributes) + model = self.create_attribute_object(promiser, attributes) classes = [] result = Result.KEPT @@ -149,9 +149,7 @@ def evaluate_promise( exit_code = pbex.run() if exit_code != 0: - classes.append( - "{safe_promiser}_failed".format(safe_promiser=safe_promiser) - ) + classes.append("{safe_promiser}_failed".format(safe_promiser=promiser)) result = Result.NOT_KEPT elif callback.changed: result = Result.REPAIRED From 7f7930eba2e5a53d0c4ba131475ec499af0756d3 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:59 +0200 Subject: [PATCH 077/146] Fixed evaluate_promise parameter name in systemd module Renamed safe_promiser to promiser to match the base class PromiseModule.evaluate_promise() signature. The parameter already receives the safe promiser via prepare_promiser_and_attributes(). Signed-off-by: Lars Erik Wik --- promise-types/systemd/systemd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index 25f9c7dc..c20ad3fc 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -79,9 +79,9 @@ def prepare_promiser_and_attributes(self, promiser, attributes): return (safe_promiser, attributes) def evaluate_promise( - self, safe_promiser: str, attributes: Dict, metadata: Dict + self, promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(safe_promiser, attributes) + model = self.create_attribute_object(promiser, attributes) # get the status of the service try: output = self._exec_command( @@ -106,13 +106,13 @@ def evaluate_promise( self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, - ["{safe_promiser}_show_failed".format(safe_promiser=safe_promiser)], + ["{safe_promiser}_show_failed".format(safe_promiser=promiser)], ) # apply the changes if model.state == SystemdPromiseTypeStates.ABSENT.value: - return self._service_absent(model, safe_promiser, service_status) + return self._service_absent(model, promiser, service_status) else: - return self._service_present(model, safe_promiser, service_status) + return self._service_present(model, promiser, service_status) def _service_absent( self, model: AttributeObject, safe_promiser: str, service_status: dict From 78eef69bf8eb51f30829f32b3182a40d0b7e7922 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:41:15 +0200 Subject: [PATCH 078/146] Fixed create_attribute_object signature in json module Added missing promiser parameter to match the base class PromiseModule.create_attribute_object() signature. Signed-off-by: Lars Erik Wik --- promise-types/json/json_promise_type.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index b72260c7..4048b9be 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -39,7 +39,7 @@ def __init__(self, **kwargs): self.types ) # for now, the only valid attributes are the types. - def create_attribute_object(self, attributes): + def create_attribute_object(self, promiser, attributes): data = {t: None for t in self.valid_attributes} for attr, val in attributes.items(): data[attr] = val @@ -73,7 +73,7 @@ def validate_promise(self, promiser, attributes, metadata): if colon and not field: raise ValidationError("Invalid syntax: field specified but empty") - model = self.create_attribute_object(attributes) + model = self.create_attribute_object(promiser, attributes) if ( model.object and isinstance(model.object, str) @@ -113,7 +113,7 @@ def validate_promise(self, promiser, attributes, metadata): ) def evaluate_promise(self, promiser, attributes, metadata): - model = self.create_attribute_object(attributes) + model = self.create_attribute_object(promiser, attributes) filename, _, field = promiser.partition(":") if os.path.exists(filename) and not os.path.isfile(filename): From b176738294c499181fa36573ba14c4a9aec7d3ea Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:41:28 +0200 Subject: [PATCH 079/146] Fixed return type annotation in iptables module Changed evaluate_command_policy return type from Result to str, since Result.KEPT and Result.REPAIRED are string class attributes, not Result instances. Signed-off-by: Lars Erik Wik --- promise-types/iptables/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 24489221..ba2d86bb 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -231,7 +231,7 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): return result, classes - def evaluate_command_policy(self, executable, table, chain, target) -> Result: + def evaluate_command_policy(self, executable, table, chain, target) -> str: policy_rules = self._iptables_policy_rules_of(executable, table, chain) assert len(policy_rules) == 1 and len(policy_rules[0].split()) >= 1 From f46acc7a60627a95c25962e20d07a352ff59d926 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 13:08:56 +0200 Subject: [PATCH 080/146] Added --diff flag to black in linting script This prints the unified diff of formatting changes on failure, making it easier to manually fix issues when black versions disagree. Signed-off-by: Lars Erik Wik --- ci/linting.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/linting.sh b/ci/linting.sh index 7db1eeda..50845f85 100755 --- a/ci/linting.sh +++ b/ci/linting.sh @@ -10,7 +10,7 @@ pyright . shopt -s globstar echo "Running black" -black --check . +black --check --diff . echo "Running pyflakes" pyflakes . From d6b6342c90ff14566ae04d72b41c0f5de54f5dfa Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 13:10:10 +0200 Subject: [PATCH 081/146] Manually format sshd_promise_type.py Due to conflicting versions of black. Signed-off-by: Lars Erik Wik --- promise-types/sshd/sshd_promise_type.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/promise-types/sshd/sshd_promise_type.py b/promise-types/sshd/sshd_promise_type.py index d573a098..44aa77cf 100644 --- a/promise-types/sshd/sshd_promise_type.py +++ b/promise-types/sshd/sshd_promise_type.py @@ -3,8 +3,7 @@ import subprocess import tempfile -from cfengine_module_library import PromiseModule, ValidationError, Result - +from cfengine_module_library import PromiseModule, Result, ValidationError BASE_CONFIG = "/etc/ssh/sshd_config" DROP_IN_DIR = "/etc/ssh/sshd_config.d/" From 92411af4a7d0b023e4ae4d388114537324ae174d Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 1 Apr 2026 14:22:23 +0200 Subject: [PATCH 082/146] rss.py: Fixed regex AFAICT this regex was intended to allow $ or - or _, but instead allowed all all characters from $ to _. Signed-off-by: Ole Herman Schumacher Elgesem --- examples/rss/rss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rss/rss.py b/examples/rss/rss.py index bc93de12..7364da52 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -170,7 +170,7 @@ def _is_unix_file(self, path): def _is_url(self, path): return ( re.search( - r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r"^http[s]?://(?:[a-zA-Z]|[0-9]|[-\$_\@\.\&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", path, ) is not None From 13d23fe3494e756200733262169e130618aefa57 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 14:38:32 +0200 Subject: [PATCH 083/146] sshd/README.md: formatted markdown file Signed-off-by: Lars Erik Wik --- promise-types/sshd/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/promise-types/sshd/README.md b/promise-types/sshd/README.md index c7eac1b8..af2aaa3f 100644 --- a/promise-types/sshd/README.md +++ b/promise-types/sshd/README.md @@ -3,13 +3,16 @@ Configures sshd and restarts the service when configuration changes. ## Promiser + The sshd configuration keyword to manage (e.g. `PermitRootLogin`, `AllowUsers`). Each promise manages a single directive in the drop-in config file. ## Attributes + - `value` (required) — the value for the directive, either a string or an slist ## What the module manages internally + 1. **Include directive** — ensures the base `sshd_config` includes the drop-in directory (`sshd_config.d/`) as its first non-comment directive 2. **Drop-in directory** — creates the drop-in directory if it doesn't exist 3. **Drop-in file** — writes directives to `sshd_config.d/00-cfengine.conf` @@ -17,17 +20,20 @@ Each promise manages a single directive in the drop-in config file. 5. **Verification** — verifies the desired directive appears in the effective sshd config (`sshd -T`) ## Conflicting promisers + Having multiple promises with the same sshd keyword is not recommended. In case of conflicting promisers, the agent will attempt to converge the correct state for each one in the order they are evaluated. This means the last promise wins and determines the final value in the configuration file. It will also cause multiple restarts of the sshd service, which may be disruptive. ## What the module does NOT do + - Install sshd — that is a `packages:` promise - Ensure sshd is running — that is a `services:` promise - Manage match blocks — those are a policy-level concern ## Policy + ```cf3 bundle agent sshd_config { From c85e4e1e063baa5d5d05b3ef136de7e5500a33d7 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 14:39:02 +0200 Subject: [PATCH 084/146] sshd/README.md: removed markdown header The build.cfengine.com already renderes a header for you. Signed-off-by: Lars Erik Wik --- promise-types/sshd/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/promise-types/sshd/README.md b/promise-types/sshd/README.md index af2aaa3f..7073fa3f 100644 --- a/promise-types/sshd/README.md +++ b/promise-types/sshd/README.md @@ -1,5 +1,3 @@ -# `sshd` promise type - Configures sshd and restarts the service when configuration changes. ## Promiser From 5bdfb551fbb11fb9eee6430279c605f51812b6b1 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 11 Nov 2025 15:14:26 -0600 Subject: [PATCH 085/146] Added promise type to manage Application Streams (AppStreams) A CFEngine custom promise type for managing AppStream modules (https://www.redhat.com/en/blog/introduction-appstreams-and-modules-red-hat-enterprise-linux) on compatible systems. - Enable, disable, install, and remove AppStream modules - Support for specifying streams and profiles Ticket: CFE-3653 --- cfbs.json | 9 + promise-types/appstreams/README.md | 100 ++++ promise-types/appstreams/appstreams.py | 443 ++++++++++++++++++ promise-types/appstreams/init.cf | 6 + promise-types/appstreams/test_appstreams.py | 114 +++++ .../appstreams/test_appstreams_logic.py | 288 ++++++++++++ 6 files changed, 960 insertions(+) create mode 100644 promise-types/appstreams/README.md create mode 100644 promise-types/appstreams/appstreams.py create mode 100644 promise-types/appstreams/init.cf create mode 100644 promise-types/appstreams/test_appstreams.py create mode 100644 promise-types/appstreams/test_appstreams_logic.py diff --git a/cfbs.json b/cfbs.json index a52c491a..0cc00840 100644 --- a/cfbs.json +++ b/cfbs.json @@ -282,6 +282,15 @@ "append enable.cf services/init.cf" ] }, + "promise-type-appstreams": { + "description": "Promise type to manage AppStream modules.", + "subdirectory": "promise-types/appstreams", + "dependencies": ["library-for-promise-types-in-python"], + "steps": [ + "copy appstreams.py modules/promises/", + "append init.cf services/init.cf" + ] + }, "promise-type-git": { "description": "Promise type to manage git repos.", "subdirectory": "promise-types/git", diff --git a/promise-types/appstreams/README.md b/promise-types/appstreams/README.md new file mode 100644 index 00000000..65ea883f --- /dev/null +++ b/promise-types/appstreams/README.md @@ -0,0 +1,100 @@ +# AppStreams Promise Type + +A CFEngine custom promise type for managing AppStream modules on compatible systems. + +## Overview + +The `appstreams` promise type allows you to manage AppStream modules, which are a key feature of RHEL 8+ and compatible systems. AppStreams provide multiple versions of software components that can be enabled or disabled as needed. + +## Features + +- Enable, disable, install, and remove AppStream modules +- Support for specifying streams and profiles + +## Installation + +To install this promise type, copy the `appstreams.py` file to your CFEngine masterfiles directory and configure the promise agent: + +``` +promise agent appstreams +{ + interpreter => "/usr/bin/python3"; + path => "$(sys.workdir)/modules/promises/appstreams.py"; +} +``` + +## Usage + +### Ensure a module is enabled + +``` +bundle agent main +{ + appstreams: + "nodejs" + state => "enabled", + stream => "12"; +} +``` + +### Ensure a module is disabled + +``` +bundle agent main +{ + appstreams: + "nodejs" + state => "disabled"; +} +``` + +### Ensure a module is installed with a specific profile + +``` +bundle agent main +{ + appstreams: + "python36" + state => "installed", + stream => "3.6", + profile => "minimal"; +} +``` + +### Ensure a module is removed + +``` +bundle agent main +{ + appstreams: + "postgresql" + state => "removed"; +} +``` + +### Reset a module to default + +``` +bundle agent main +{ + appstreams: + "nodejs" + state => "default"; +} +``` + +## Attributes + +The promise type supports the following attributes: + +- `state` (optional) - Desired state of the module: `enabled`, `disabled`, `installed`, `removed`, `default`, or `reset` (default: `enabled`) +- `stream` (optional) - Specific stream of the module to use. Set to `default` to use the module's default stream. +- `profile` (optional) - Specific profile of the module to install. Set to `default` to use the module stream's default profile. + +## Requirements + +- CFEngine 3.18 or later +- Python 3 +- DNF Python API (python3-dnf package) +- DNF/YUM package manager (RHEL 8+, Fedora, CentOS 8+) +- AppStream repositories configured diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py new file mode 100644 index 00000000..45b74374 --- /dev/null +++ b/promise-types/appstreams/appstreams.py @@ -0,0 +1,443 @@ +#!/usr/bin/python3 +# +# Custom promise type to manage AppStream modules +# Uses cfengine_module_library.py library. +# +# Use it in the policy like this: +# promise agent appstreams +# { +# interpreter => "/usr/bin/python3"; +# path => "$(sys.workdir)/modules/promises/appstreams.py"; +# } +# bundle agent main +# { +# appstreams: +# "nodejs" +# state => "installed", +# stream => "12"; +# +# "postgresql" +# state => "default"; +# } + +import dnf +import dnf.exceptions +import re +from cfengine_module_library import PromiseModule, ValidationError, Result + + +class AppStreamsPromiseTypeModule(PromiseModule): + def __init__(self, **kwargs): + super(AppStreamsPromiseTypeModule, self).__init__( + name="appstreams_promise_module", version="0.0.1", **kwargs + ) + + # Define all expected attributes with their types and validation + self.add_attribute( + "state", + str, + required=False, + default="enabled", + validator=lambda x: self._validate_state(x), + ) + self.add_attribute( + "stream", + str, + required=False, + validator=lambda x: self._validate_stream_name(x), + ) + self.add_attribute( + "profile", + str, + required=False, + validator=lambda x: self._validate_profile_name(x), + ) + + def _validate_state(self, value): + if value not in ( + "enabled", + "disabled", + "installed", + "removed", + "default", + "reset", + ): + raise ValidationError( + "State attribute must be 'enabled', 'disabled', 'installed', " + "'removed', 'default', or 'reset'" + ) + + def _validate_module_name(self, name): + # Validate module name to prevent injection + if not re.match(r"^[a-zA-Z0-9_.-]+$", name): + raise ValidationError( + f"Invalid module name: {name}. Only alphanumeric, underscore, " + f"dot, and dash characters are allowed." + ) + + def _validate_stream_name(self, stream): + # Validate stream name to prevent injection + if stream and not re.match(r"^[a-zA-Z0-9_.-]+$", stream): + raise ValidationError( + f"Invalid stream name: {stream}. Only alphanumeric, underscore, " + f"dot, and dash characters are allowed." + ) + + def _validate_profile_name(self, profile): + # Validate profile name to prevent injection + if profile and not re.match(r"^[a-zA-Z0-9_.-]+$", profile): + raise ValidationError( + f"Invalid profile name: {profile}. Only alphanumeric, underscore, " + f"dot, and dash characters are allowed." + ) + + def validate_promise(self, promiser, attributes, metadata): + # Validate promiser (module name) + if not isinstance(promiser, str): + raise ValidationError("Promiser must be of type string") + + self._validate_module_name(promiser) + + def evaluate_promise(self, promiser, attributes, metadata): + module_name = promiser + state = attributes.get("state", "enabled") + stream = attributes.get("stream", None) + profile = attributes.get("profile", None) + + base = dnf.Base() + try: + # Read configuration + base.conf.assumeyes = True + + # Read repository information + base.read_all_repos() + + # Fill the sack (package database) + base.fill_sack(load_system_repo=True) + + # Get ModulePackageContainer from sack + if base.sack is None: + self.log_error("DNF sack is not available") + return Result.NOT_KEPT + if hasattr(base.sack, "_moduleContainer"): + mpc = base.sack._moduleContainer + else: + self.log_error("DNF sack has no module container") + return Result.NOT_KEPT + + # Handle stream => "default" + if stream == "default": + stream = self._get_default_stream(mpc, module_name) + if not stream: + self.log_error(f"No default stream found for module {module_name}") + return Result.NOT_KEPT + self.log_verbose(f"Resolved 'default' stream to '{stream}'") + + # Handle profile => "default" + if profile == "default": + # We need the stream to check for default profile + # If stream is None, DNF might pick default stream, but safer to have it resolved + resolved_stream = stream + if not resolved_stream: + resolved_stream = self._get_default_stream(mpc, module_name) + + profile = self._get_default_profile(mpc, module_name, resolved_stream) + if not profile: + self.log_error(f"No default profile found for module {module_name}") + return Result.NOT_KEPT + self.log_verbose(f"Resolved 'default' profile to '{profile}'") + + # Check current state of the module + current_state = self._get_module_state(mpc, module_name) + + # Determine what action to take based on desired state + if state == "enabled": + if current_state == "enabled": + # Check stream match + is_stream_correct = True + if stream: + try: + enabled_stream = mpc.getEnabledStream(module_name) + if enabled_stream != stream: + is_stream_correct = False + # RuntimeError is raised by libdnf if the module is unknown + except RuntimeError: + pass + + if is_stream_correct: + self.log_verbose(f"Module {module_name} is already enabled") + return Result.KEPT + else: + return self._enable_module(mpc, base, module_name, stream) + else: + return self._enable_module(mpc, base, module_name, stream) + elif state == "disabled": + if current_state == "disabled": + self.log_verbose(f"Module {module_name} is already disabled") + return Result.KEPT + else: + return self._disable_module(mpc, base, module_name) + elif state == "installed": + if current_state in ["installed", "enabled"]: + # For "installed" state, if it's already installed or enabled, + # we need to check if the specific profile is installed + if self._is_module_installed_with_packages( + mpc, module_name, stream, profile + ): + self.log_verbose( + f"Module {module_name} (stream: {stream}, " + f"profile: {profile}) is already present" + ) + return Result.KEPT + else: + return self._install_module( + mpc, base, module_name, stream, profile + ) + else: + # Module is not enabled, need to install + # (which will enable and install packages) + return self._install_module(mpc, base, module_name, stream, profile) + elif state == "removed": + if current_state == "removed" or current_state == "disabled": + self.log_verbose( + f"Module {module_name} is already absent or disabled" + ) + return Result.KEPT + else: + return self._remove_module(mpc, base, module_name, stream, profile) + elif state == "default" or state == "reset": + return self._reset_module(mpc, base, module_name) + + self.log_error(f"Unexpected state '{state}' for module {module_name}") + return Result.NOT_KEPT + finally: + base.close() + + def _get_module_state(self, mpc, module_name): + """Get the current state of a module using DNF Python API""" + state = mpc.getModuleState(module_name) + if state == mpc.ModuleState_ENABLED: + return "enabled" + elif state == mpc.ModuleState_DISABLED: + return "disabled" + elif state == mpc.ModuleState_INSTALLED: + return "installed" + return "removed" + + def _get_default_stream(self, mpc, module_name): + """Find the default stream for a module""" + return mpc.getDefaultStream(module_name) + + def _get_default_profile(self, mpc, module_name, stream): + """Find the default profile for a module stream""" + profiles = mpc.getDefaultProfiles(module_name, stream) + if profiles: + return profiles[0] + return None + + def _is_module_installed_with_packages( + self, mpc, module_name, stream, profile_name + ): + """Check if the module packages/profiles are installed on the system""" + # Check stream + try: + enabled_stream = mpc.getEnabledStream(module_name) + except RuntimeError: + # RuntimeError is raised by libdnf if the module is unknown + return False + + if stream and enabled_stream != stream: + return False + + target_stream = stream or enabled_stream + if not target_stream: + return False + + # Check profile + if not profile_name: + profile_name = self._get_default_profile(mpc, module_name, target_stream) + + if profile_name: + try: + installed_profiles = mpc.getInstalledProfiles(module_name) + return profile_name in installed_profiles + except RuntimeError: + # RuntimeError is raised by libdnf if the module is unknown + return False + + return True + + def _enable_module(self, mpc, base, module_name, stream): + """Enable a module using DNF Python API""" + target_stream = stream or self._get_default_stream(mpc, module_name) + + if not target_stream: + self.log_error( + f"No stream specified and no default stream found for {module_name}" + ) + return Result.NOT_KEPT + + mpc.enable(module_name, target_stream) + mpc.save() + mpc.moduleDefaultsResolve() + base.resolve() + base.do_transaction() + if mpc.isEnabled(module_name, target_stream): + self.log_info(f"Module {module_name}:{target_stream} enabled successfully") + return Result.REPAIRED + else: + self.log_error(f"Failed to enable module {module_name}:{target_stream}") + return Result.NOT_KEPT + + def _disable_module(self, mpc, base, module_name): + """Disable a module using DNF Python API""" + mpc.disable(module_name) + mpc.save() + base.resolve() + base.do_transaction() + if mpc.isDisabled(module_name): + self.log_info(f"Module {module_name} disabled successfully") + return Result.REPAIRED + else: + self.log_error(f"Failed to disable module {module_name}") + return Result.NOT_KEPT + + def _get_profile_packages(self, mpc, module_name, stream, profile_name): + # Find the module package + # mpc.query(name) returns vector + modules = mpc.query(module_name) + for module in modules: + if module.getStream() == stream: + # Found stream + for profile in module.getProfiles(): + if profile.getName() == profile_name: + return profile.getContent() + return [] + + def _install_module(self, mpc, base, module_name, stream, profile): + """Install a module using DNF Python API""" + if not stream: + try: + stream = mpc.getEnabledStream(module_name) + except RuntimeError: + pass + if not stream: + stream = self._get_default_stream(mpc, module_name) + + if not profile: + profile = self._get_default_profile(mpc, module_name, stream) + + if not profile: + self.log_error( + f"No profile specified and no default found for {module_name}:{stream}" + ) + return Result.NOT_KEPT + + mpc.enable(module_name, stream) + mpc.install(module_name, stream, profile) + mpc.save() + mpc.moduleDefaultsResolve() + + # Install packages + packages = self._get_profile_packages(mpc, module_name, stream, profile) + failed_packages = [] + if packages: + for pkg in packages: + try: + base.install(pkg) + except dnf.exceptions.Error as e: + self.log_verbose(f"Failed to install package {pkg}: {e}") + failed_packages.append((pkg, str(e))) + + base.resolve() + base.do_transaction() + + # Verify installation succeeded + if self._is_module_installed_with_packages(mpc, module_name, stream, profile): + self.log_info( + f"Module {module_name}:{stream}/{profile} installed successfully" + ) + return Result.REPAIRED + else: + self.log_error(f"Failed to install module {module_name}:{stream}/{profile}") + if failed_packages: + for pkg, error in failed_packages: + self.log_error(f" Package {pkg} failed: {error}") + return Result.NOT_KEPT + + def _remove_module(self, mpc, base, module_name, stream, profile): + """Remove a module using DNF Python API""" + if not stream: + try: + target_stream = mpc.getEnabledStream(module_name) + except RuntimeError: + target_stream = None + else: + target_stream = stream + + if not target_stream: + self.log_verbose(f"No active stream for {module_name}, nothing to remove") + return Result.KEPT + + failed_packages = [] + if profile: + mpc.uninstall(module_name, target_stream, profile) + pkgs = self._get_profile_packages(mpc, module_name, target_stream, profile) + for pkg in pkgs: + try: + base.remove(pkg) + except dnf.exceptions.Error as e: + self.log_verbose(f"Failed to remove package {pkg}: {e}") + failed_packages.append((pkg, str(e))) + else: + profiles = mpc.getInstalledProfiles(module_name) + for p in profiles: + mpc.uninstall(module_name, target_stream, p) + pkgs = self._get_profile_packages(mpc, module_name, target_stream, p) + for pkg in pkgs: + try: + base.remove(pkg) + except dnf.exceptions.Error as e: + self.log_verbose(f"Failed to remove package {pkg}: {e}") + failed_packages.append((pkg, str(e))) + + mpc.save() + base.resolve(allow_erasing=True) + base.do_transaction() + + # Verify removal succeeded + current_state = self._get_module_state(mpc, module_name) + if current_state in ["removed", "disabled"]: + self.log_info(f"Module {module_name} removed successfully") + return Result.REPAIRED + else: + self.log_error(f"Failed to remove module {module_name}") + if failed_packages: + for pkg, error in failed_packages: + self.log_error(f" Package {pkg} failed: {error}") + return Result.NOT_KEPT + + def _reset_module(self, mpc, base, module_name): + """Reset a module using DNF Python API""" + if mpc.getModuleState(module_name) == mpc.ModuleState_DEFAULT: + self.log_verbose( + f"Module {module_name} is already in default (reset) state" + ) + return Result.KEPT + + mpc.reset(module_name) + mpc.save() + base.resolve() + base.do_transaction() + + # Verify reset succeeded + if mpc.getModuleState(module_name) == mpc.ModuleState_DEFAULT: + self.log_info(f"Module {module_name} reset successfully") + return Result.REPAIRED + else: + self.log_error(f"Failed to reset module {module_name}") + return Result.NOT_KEPT + + +if __name__ == "__main__": + AppStreamsPromiseTypeModule().start() diff --git a/promise-types/appstreams/init.cf b/promise-types/appstreams/init.cf new file mode 100644 index 00000000..24252ab6 --- /dev/null +++ b/promise-types/appstreams/init.cf @@ -0,0 +1,6 @@ +promise agent appstreams +# @brief Define appstreams promise type +{ + path => "$(sys.workdir)/modules/promises/appstreams.py"; + interpreter => "/usr/bin/python3"; +} diff --git a/promise-types/appstreams/test_appstreams.py b/promise-types/appstreams/test_appstreams.py new file mode 100644 index 00000000..1fe74711 --- /dev/null +++ b/promise-types/appstreams/test_appstreams.py @@ -0,0 +1,114 @@ +import sys +import os +import pytest +from unittest.mock import MagicMock + +# Mock dnf module before importing the promise module +mock_dnf = MagicMock() +mock_dnf.exceptions = MagicMock() +sys.modules["dnf"] = mock_dnf +sys.modules["dnf.exceptions"] = mock_dnf.exceptions + +# Add library path +sys.path.insert( + 0, os.path.join(os.path.dirname(__file__), "..", "..", "libraries", "python") +) +# Add module path +sys.path.insert(0, os.path.dirname(__file__)) + +from appstreams import AppStreamsPromiseTypeModule # noqa: E402 +from cfengine_module_library import ValidationError # noqa: E402 + + +@pytest.fixture +def module(): + return AppStreamsPromiseTypeModule() + + +def test_validation_valid_attributes(module): + """Test validation of valid module attributes""" + module.validate_promise("nodejs", {"state": "enabled", "stream": "12"}, {}) + + +def test_validation_invalid_module_name(module): + """Test validation of invalid module name""" + with pytest.raises(ValidationError) as excinfo: + module.validate_promise("nodejs; echo hi", {"state": "enabled"}, {}) + assert "Invalid module name" in str(excinfo.value) + + +@pytest.mark.parametrize( + "name", ["nodejs", "python3.6", "python36", "postgresql", "maven", "httpd"] +) +def test_module_name_validation_valid(module, name): + """Test module name validation with valid names""" + module._validate_module_name(name) + + +@pytest.mark.parametrize( + "name", ["nodejs;echo", "python36&&", "postgresql|", "maven>", "httpd<"] +) +def test_module_name_validation_invalid(module, name): + """Test module name validation with invalid names""" + with pytest.raises(ValidationError): + module._validate_module_name(name) + + +@pytest.mark.parametrize( + "stream", ["12", "14", "3.6", "1.14", "latest", "stable", "default"] +) +def test_stream_name_validation_valid(module, stream): + """Test stream name validation with valid names""" + module._validate_stream_name(stream) + + +@pytest.mark.parametrize("stream", ["12;echo", "14&&", "3.6|", "latest>", "stable<"]) +def test_stream_name_validation_invalid(module, stream): + """Test stream name validation with invalid names""" + with pytest.raises(ValidationError): + module._validate_stream_name(stream) + + +@pytest.mark.parametrize( + "state", ["enabled", "disabled", "installed", "removed", "default", "reset"] +) +def test_state_validation_valid(module, state): + """Test state validation with valid states""" + module._validate_state(state) + + +@pytest.mark.parametrize( + "state", + [ + "active", + "inactive", + "enable", + "disable", + "install", + "remove", + "present", + "absent", + ], +) +def test_state_validation_invalid(module, state): + """Test state validation with invalid states""" + with pytest.raises(ValidationError): + module._validate_state(state) + + +def test_state_parsing_method_exists(module): + """Test that the state parsing method exists""" + assert hasattr(module, "_get_module_state") + + +@pytest.mark.parametrize("profile", ["common", "minimal", "server", "default", "1.0"]) +def test_profile_name_validation_valid(module, profile): + """Test profile name validation with valid names""" + module._validate_profile_name(profile) + + +@pytest.mark.parametrize("profile", ["common;echo", "minimal&&", "server|", "default>"]) +def test_profile_name_validation_invalid(module, profile): + """Test profile name validation with invalid names""" + with pytest.raises(ValidationError): + module._validate_profile_name(profile) diff --git a/promise-types/appstreams/test_appstreams_logic.py b/promise-types/appstreams/test_appstreams_logic.py new file mode 100644 index 00000000..9997fa76 --- /dev/null +++ b/promise-types/appstreams/test_appstreams_logic.py @@ -0,0 +1,288 @@ +import sys +import os +import pytest +from unittest.mock import MagicMock + +# Add library path +sys.path.insert( + 0, os.path.join(os.path.dirname(__file__), "..", "..", "libraries", "python") +) +# Add module path +sys.path.insert(0, os.path.dirname(__file__)) + +# Mock dnf module before importing the promise module +mock_dnf = MagicMock() +mock_dnf.exceptions = MagicMock() +sys.modules["dnf"] = mock_dnf +sys.modules["dnf.exceptions"] = mock_dnf.exceptions + +import appstreams as appstreams_module # noqa: E402 + +appstreams_module.dnf = mock_dnf + +from appstreams import AppStreamsPromiseTypeModule # noqa: E402 +from cfengine_module_library import ValidationError, Result # noqa: E402 + + +@pytest.fixture +def module(): + # Reset mocks + mock_dnf.reset_mock() + if hasattr(mock_dnf.Base, "return_value"): + mock_dnf.Base.return_value.reset_mock() + + mod = AppStreamsPromiseTypeModule() + mod._log_level = "info" + mod._out = MagicMock() + return mod + + +@pytest.fixture +def mock_mpc(): + # Setup the ModulePackageContainer mock + mpc = MagicMock() + # Setup constants + mpc.ModuleState_ENABLED = 1 + mpc.ModuleState_DISABLED = 2 + mpc.ModuleState_INSTALLED = 3 + mpc.ModuleState_DEFAULT = 0 + return mpc + + +@pytest.fixture +def mock_base(mock_mpc): + base = mock_dnf.Base.return_value + base.sack._moduleContainer = mock_mpc + return base + + +def test_harness_setup(module): + """Verify the test harness is working""" + assert module is not None + assert module.name == "appstreams_promise_module" + + +def test_enable_module_already_enabled(module, mock_base, mock_mpc): + """Test enabling a module that is already enabled (KEPT)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.return_value = "12" + + result = module.evaluate_promise("nodejs", {"state": "enabled", "stream": "12"}, {}) + assert result == Result.KEPT + + +def test_enable_module_repaired(module, mock_base, mock_mpc): + """Test enabling a module that is disabled (REPAIRED)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DISABLED + mock_mpc.isEnabled.return_value = True # After enable() called + + result = module.evaluate_promise("nodejs", {"state": "enabled", "stream": "12"}, {}) + + mock_mpc.enable.assert_called_with("nodejs", "12") + assert result == Result.REPAIRED + + +def test_disable_module_already_disabled(module, mock_base, mock_mpc): + """Test disabling a module that is already disabled (KEPT)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DISABLED + + result = module.evaluate_promise( + "nodejs", {"state": "disabled", "stream": "12"}, {} + ) + assert result == Result.KEPT + + +def test_disable_module_repaired(module, mock_base, mock_mpc): + """Test disabling a module that is enabled (REPAIRED)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.isDisabled.return_value = True # After disable() called + + result = module.evaluate_promise( + "nodejs", {"state": "disabled", "stream": "12"}, {} + ) + + mock_mpc.disable.assert_called_with("nodejs") + assert result == Result.REPAIRED + + +def test_install_profile_repaired(module, mock_base, mock_mpc): + """Test installing a specific profile using 'installed' state (REPAIRED)""" + # Initial state: enabled but not fully installed with profile + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.return_value = "12" + mock_mpc.getInstalledProfiles.return_value = [] + + # helper for _get_profile_packages + # It queries module, gets stream, gets profiles, gets content + mock_module_obj = MagicMock() + mock_module_obj.getStream.return_value = "12" + mock_profile_obj = MagicMock() + mock_profile_obj.getName.return_value = "common" + mock_profile_obj.getContent.return_value = ["pkg1"] + mock_module_obj.getProfiles.return_value = [mock_profile_obj] + mock_mpc.query.return_value = [mock_module_obj] + + result = module.evaluate_promise( + "nodejs", {"state": "installed", "stream": "12", "profile": "common"}, {} + ) + + mock_mpc.install.assert_called_with("nodejs", "12", "common") + assert result == Result.REPAIRED + + +def test_remove_module_repaired(module, mock_base, mock_mpc): + """Test removing a module using 'removed' state (REPAIRED)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_INSTALLED + mock_mpc.getEnabledStream.return_value = "12" + mock_mpc.getInstalledProfiles.return_value = ["common"] + + # For removal, we also need package query to mock explicit package removal + mock_module_obj = MagicMock() + mock_module_obj.getStream.return_value = "12" + mock_profile_obj = MagicMock() + mock_profile_obj.getName.return_value = "common" + mock_profile_obj.getContent.return_value = ["pkg1"] + mock_module_obj.getProfiles.return_value = [mock_profile_obj] + mock_mpc.query.return_value = [mock_module_obj] + + result = module.evaluate_promise("nodejs", {"state": "removed", "stream": "12"}, {}) + + # Logic in _remove_module calls uninstall for each installed profile if no profile specified + mock_mpc.uninstall.assert_called() + assert result == Result.REPAIRED + + +def test_install_profile_idempotency_success(module, mock_base, mock_mpc): + """Test installing a profile that is already present (KEPT)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_INSTALLED + mock_mpc.getEnabledStream.return_value = "12" + mock_mpc.getInstalledProfiles.return_value = ["common"] + + result = module.evaluate_promise( + "nodejs", {"state": "installed", "stream": "12", "profile": "common"}, {} + ) + + assert result == Result.KEPT + + +def test_reset_module_repaired(module, mock_base, mock_mpc): + """Test resetting a module to default state (REPAIRED)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + + result = module.evaluate_promise("nodejs", {"state": "default", "stream": "12"}, {}) + + mock_mpc.reset.assert_called_with("nodejs") + assert result == Result.REPAIRED + + +def test_stream_default_resolution(module, mock_base, mock_mpc): + """Test resolving stream => 'default'""" + mock_mpc.getDefaultStream.return_value = "12" + + # State check uses resolved stream + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.return_value = "12" + + result = module.evaluate_promise( + "nodejs", {"state": "enabled", "stream": "default"}, {} + ) + + assert result == Result.KEPT + + +def test_profile_default_resolution(module, mock_base, mock_mpc): + """Test resolving profile => 'default'""" + mock_mpc.getDefaultStream.return_value = "12" + mock_mpc.getDefaultProfiles.return_value = ["default_prof"] + + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_INSTALLED + mock_mpc.getEnabledStream.return_value = "12" + mock_mpc.getInstalledProfiles.return_value = ["default_prof"] + + result = module.evaluate_promise( + "nodejs", {"state": "installed", "stream": "12", "profile": "default"}, {} + ) + + assert result == Result.KEPT + + +def test_invalid_aliases(module): + """Verify that aliases 'install' and 'remove' are invalid""" + + # Test 'install' + with pytest.raises(ValidationError): + module._validate_state("install") + + # Test 'remove' + with pytest.raises(ValidationError): + module._validate_state("remove") + + +def test_get_module_state_logic(module, mock_mpc): + """Test the logic of _get_module_state""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + assert module._get_module_state(mock_mpc, "nodejs") == "enabled" + + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DISABLED + assert module._get_module_state(mock_mpc, "nodejs") == "disabled" + + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_INSTALLED + assert module._get_module_state(mock_mpc, "nodejs") == "installed" + + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DEFAULT + assert module._get_module_state(mock_mpc, "nodejs") == "removed" + + mock_mpc.getModuleState.return_value = 999 # Unknown state + assert module._get_module_state(mock_mpc, "nodejs") == "removed" + + +def test_remove_already_removed(module, mock_base, mock_mpc): + """Test removing a module that is already removed (KEPT)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DEFAULT + result = module.evaluate_promise("nodejs", {"state": "removed"}, {}) + assert result == Result.KEPT + + +def test_remove_already_disabled(module, mock_base, mock_mpc): + """Test removing a module that is already disabled (KEPT)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_DISABLED + result = module.evaluate_promise("nodejs", {"state": "removed"}, {}) + assert result == Result.KEPT + + +def test_enable_wrong_stream_repaired(module, mock_base, mock_mpc): + """Test enabling a module that is enabled but with wrong stream (REPAIRED)""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.return_value = "10" + mock_mpc.isEnabled.return_value = True + result = module.evaluate_promise("nodejs", {"state": "enabled", "stream": "12"}, {}) + mock_mpc.enable.assert_called_with("nodejs", "12") + assert result == Result.REPAIRED + + +def test_stream_default_not_found(module, mock_base, mock_mpc): + """Test stream => 'default' when no default stream exists (NOT_KEPT)""" + mock_mpc.getDefaultStream.return_value = None + result = module.evaluate_promise( + "nodejs", {"state": "enabled", "stream": "default"}, {} + ) + assert result == Result.NOT_KEPT + + +def test_profile_default_not_found(module, mock_base, mock_mpc): + """Test profile => 'default' when no default profile exists (NOT_KEPT)""" + mock_mpc.getDefaultStream.return_value = "12" + mock_mpc.getDefaultProfiles.return_value = [] + result = module.evaluate_promise( + "nodejs", {"state": "installed", "stream": "12", "profile": "default"}, {} + ) + assert result == Result.NOT_KEPT + + +def test_remove_unknown_module_runtime_error(module, mock_base, mock_mpc): + """Test removing a module when getEnabledStream raises RuntimeError""" + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.side_effect = RuntimeError("No such module") + result = module.evaluate_promise("unknown_mod", {"state": "removed"}, {}) + # With no stream and getEnabledStream failing, target_stream is None, so KEPT + assert result == Result.KEPT From c8a346ac3f35d03bc136ad65feaa415009e37a82 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 2 Apr 2026 16:41:54 -0500 Subject: [PATCH 086/146] Fixups from review --- promise-types/appstreams/appstreams.py | 17 ++++++++--------- .../appstreams/test_appstreams_logic.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index 45b74374..568e68b0 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -54,22 +54,21 @@ def __init__(self, **kwargs): ) def _validate_state(self, value): - if value not in ( + accepted = ( "enabled", "disabled", "installed", "removed", "default", "reset", - ): - raise ValidationError( - "State attribute must be 'enabled', 'disabled', 'installed', " - "'removed', 'default', or 'reset'" - ) + ) + if value not in accepted: + accepted_str = "', '".join(accepted) + raise ValidationError(f"State attribute must be '{accepted_str}'") def _validate_module_name(self, name): # Validate module name to prevent injection - if not re.match(r"^[a-zA-Z0-9_.-]+$", name): + if not re.fullmatch(r"[a-zA-Z0-9_.-]+", name): raise ValidationError( f"Invalid module name: {name}. Only alphanumeric, underscore, " f"dot, and dash characters are allowed." @@ -77,7 +76,7 @@ def _validate_module_name(self, name): def _validate_stream_name(self, stream): # Validate stream name to prevent injection - if stream and not re.match(r"^[a-zA-Z0-9_.-]+$", stream): + if stream and not re.fullmatch(r"[a-zA-Z0-9_.-]+", stream): raise ValidationError( f"Invalid stream name: {stream}. Only alphanumeric, underscore, " f"dot, and dash characters are allowed." @@ -85,7 +84,7 @@ def _validate_stream_name(self, stream): def _validate_profile_name(self, profile): # Validate profile name to prevent injection - if profile and not re.match(r"^[a-zA-Z0-9_.-]+$", profile): + if profile and not re.fullmatch(r"[a-zA-Z0-9_.-]+", profile): raise ValidationError( f"Invalid profile name: {profile}. Only alphanumeric, underscore, " f"dot, and dash characters are allowed." diff --git a/promise-types/appstreams/test_appstreams_logic.py b/promise-types/appstreams/test_appstreams_logic.py index 9997fa76..2ff9ec4a 100644 --- a/promise-types/appstreams/test_appstreams_logic.py +++ b/promise-types/appstreams/test_appstreams_logic.py @@ -110,7 +110,8 @@ def test_install_profile_repaired(module, mock_base, mock_mpc): # Initial state: enabled but not fully installed with profile mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED mock_mpc.getEnabledStream.return_value = "12" - mock_mpc.getInstalledProfiles.return_value = [] + # First call (pre-install check) returns [], second call (post-install verify) returns ["common"] + mock_mpc.getInstalledProfiles.side_effect = [[], ["common"]] # helper for _get_profile_packages # It queries module, gets stream, gets profiles, gets content @@ -132,7 +133,11 @@ def test_install_profile_repaired(module, mock_base, mock_mpc): def test_remove_module_repaired(module, mock_base, mock_mpc): """Test removing a module using 'removed' state (REPAIRED)""" - mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_INSTALLED + # First call (current state check) returns INSTALLED, second (post-remove verify) returns DEFAULT + mock_mpc.getModuleState.side_effect = [ + mock_mpc.ModuleState_INSTALLED, + mock_mpc.ModuleState_DEFAULT, + ] mock_mpc.getEnabledStream.return_value = "12" mock_mpc.getInstalledProfiles.return_value = ["common"] @@ -167,7 +172,12 @@ def test_install_profile_idempotency_success(module, mock_base, mock_mpc): def test_reset_module_repaired(module, mock_base, mock_mpc): """Test resetting a module to default state (REPAIRED)""" - mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + # evaluate_promise calls _get_module_state first, then _reset_module calls it twice more + mock_mpc.getModuleState.side_effect = [ + mock_mpc.ModuleState_ENABLED, # evaluate_promise current-state check + mock_mpc.ModuleState_ENABLED, # _reset_module early-exit check + mock_mpc.ModuleState_DEFAULT, # _reset_module post-reset verification + ] result = module.evaluate_promise("nodejs", {"state": "default", "stream": "12"}, {}) From 39bda6cf3c4b882dc326a6b533edd8778ebdc966 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 2 Apr 2026 16:46:55 -0500 Subject: [PATCH 087/146] Added github action to run pytest --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7ce1b98..348350c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [master] pull_request: - branches: [master] + types: [opened, reopened, synchronize] jobs: unit_tests: @@ -31,3 +31,7 @@ jobs: run: cfbs --check pretty ./cfbs.json - name: Linting run: ./ci/linting.sh + - name: Install pytest + run: pip install pytest + - name: Run promise type tests + run: python3 -m pytest promise-types/ -v From ed7d67b3107a03702839f3d8ee5377f1a3e5cb92 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Fri, 3 Apr 2026 13:28:43 -0500 Subject: [PATCH 088/146] Refactored and simplified --- promise-types/appstreams/appstreams.py | 359 ++++++++++++++----------- 1 file changed, 201 insertions(+), 158 deletions(-) diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index 568e68b0..dd785202 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -12,14 +12,23 @@ # bundle agent main # { # appstreams: -# "nodejs" -# state => "installed", -# stream => "12"; +# "nodejs" -> { "nodejs_app_server", "security_team" } +# handle => "main_nodejs_stream_20_installed", +# comment => "Pin nodejs to stream 20 before packages: promises run", +# meta => { "service:nodeapp", "stream:20" }, +# state => "installed", +# stream => "20"; # # "postgresql" -# state => "default"; +# handle => "main_postgresql_default", +# state => "default"; # } +# +# Setting a handle is strongly recommended: it appears in the DNF history +# Comment field alongside the bundle and policy file, giving auditors a +# direct pointer back to the exact promise that made the change. +import sys import dnf import dnf.exceptions import re @@ -32,7 +41,6 @@ def __init__(self, **kwargs): name="appstreams_promise_module", version="0.0.1", **kwargs ) - # Define all expected attributes with their types and validation self.add_attribute( "state", str, @@ -44,58 +52,52 @@ def __init__(self, **kwargs): "stream", str, required=False, - validator=lambda x: self._validate_stream_name(x), + validator=lambda x: self._validate_identifier( + x, "stream name", required=False + ), ) self.add_attribute( "profile", str, required=False, - validator=lambda x: self._validate_profile_name(x), + validator=lambda x: self._validate_identifier( + x, "profile name", required=False + ), ) + # Standard CFEngine promise attributes — passed through by the agent + # and used to populate the DNF history comment for audit traceability. + self.add_attribute("handle", str, required=False) + self.add_attribute("comment", str, required=False) + def _validate_state(self, value): - accepted = ( - "enabled", - "disabled", - "installed", - "removed", - "default", - "reset", - ) + accepted = ("enabled", "disabled", "installed", "removed", "default", "reset") if value not in accepted: accepted_str = "', '".join(accepted) raise ValidationError(f"State attribute must be '{accepted_str}'") def _validate_module_name(self, name): - # Validate module name to prevent injection - if not re.fullmatch(r"[a-zA-Z0-9_.-]+", name): - raise ValidationError( - f"Invalid module name: {name}. Only alphanumeric, underscore, " - f"dot, and dash characters are allowed." - ) + self._validate_identifier(name, "module name") def _validate_stream_name(self, stream): - # Validate stream name to prevent injection - if stream and not re.fullmatch(r"[a-zA-Z0-9_.-]+", stream): - raise ValidationError( - f"Invalid stream name: {stream}. Only alphanumeric, underscore, " - f"dot, and dash characters are allowed." - ) + self._validate_identifier(stream, "stream name", required=False) def _validate_profile_name(self, profile): - # Validate profile name to prevent injection - if profile and not re.fullmatch(r"[a-zA-Z0-9_.-]+", profile): + self._validate_identifier(profile, "profile name", required=False) + + def _validate_identifier(self, value, label, required=True): + if not required and not value: + return + if not re.fullmatch(r"[a-zA-Z0-9_.-]+", value): raise ValidationError( - f"Invalid profile name: {profile}. Only alphanumeric, underscore, " + f"Invalid {label}: {value!r}. Only alphanumeric, underscore, " f"dot, and dash characters are allowed." ) def validate_promise(self, promiser, attributes, metadata): - # Validate promiser (module name) if not isinstance(promiser, str): raise ValidationError("Promiser must be of type string") - - self._validate_module_name(promiser) + self._validate_identifier(promiser, "module name") def evaluate_promise(self, promiser, attributes, metadata): module_name = promiser @@ -103,117 +105,127 @@ def evaluate_promise(self, promiser, attributes, metadata): stream = attributes.get("stream", None) profile = attributes.get("profile", None) + # Build a descriptive argv so dnf history records a meaningful + # "Command Line" entry instead of leaving it blank. + _cmdline = [f"cfengine-appstreams {module_name!r} state={state!r}"] + if stream: + _cmdline.append(f"stream={stream!r}") + if profile: + _cmdline.append(f"profile={profile!r}") + _orig_argv, sys.argv = sys.argv, _cmdline + base = dnf.Base() try: - # Read configuration + # Read configuration first so comment is set before plugins read it base.conf.assumeyes = True + handle = attributes.get("handle", "") + cf_comment = attributes.get("comment", "") + extra = [] + if handle: + extra.append(f"handle: {handle}") + if cf_comment: + extra.append(f"comment: {cf_comment}") + extra_part = " | " + ", ".join(extra) if extra else "" + base.conf.comment = ( + f"CFEngine appstreams promise: {module_name} state={state}{extra_part}" + ) + + # Load DNF plugins so transactions are recorded like the CLI would. + # configure_plugins() is intentionally omitted: it opens a history + # entry unconditionally and base.close() would commit a spurious + # empty record on KEPT runs. init + pre_configure is sufficient for + # the transaction() hook to fire when do_transaction() is called. + base.init_plugins() + base.pre_configure_plugins() - # Read repository information base.read_all_repos() - # Fill the sack (package database) + # Force metadata expiry so DNF re-downloads repo metadata rather + # than using stale cache entries that may point to RPM paths from + # previously interrupted transactions that no longer exist on disk. + if base.repos: + for repo in base.repos.iter_enabled(): + repo.metadata_expire = 0 + base.fill_sack(load_system_repo=True) - # Get ModulePackageContainer from sack if base.sack is None: self.log_error("DNF sack is not available") return Result.NOT_KEPT - if hasattr(base.sack, "_moduleContainer"): - mpc = base.sack._moduleContainer - else: + if not hasattr(base.sack, "_moduleContainer"): self.log_error("DNF sack has no module container") return Result.NOT_KEPT + mpc = base.sack._moduleContainer - # Handle stream => "default" + # Resolve "default" stream/profile to concrete values if stream == "default": - stream = self._get_default_stream(mpc, module_name) + stream = mpc.getDefaultStream(module_name) if not stream: self.log_error(f"No default stream found for module {module_name}") return Result.NOT_KEPT self.log_verbose(f"Resolved 'default' stream to '{stream}'") - # Handle profile => "default" if profile == "default": - # We need the stream to check for default profile - # If stream is None, DNF might pick default stream, but safer to have it resolved - resolved_stream = stream - if not resolved_stream: - resolved_stream = self._get_default_stream(mpc, module_name) - - profile = self._get_default_profile(mpc, module_name, resolved_stream) + resolved_stream = stream or mpc.getDefaultStream(module_name) + profiles = mpc.getDefaultProfiles(module_name, resolved_stream) + profile = profiles[0] if profiles else None if not profile: self.log_error(f"No default profile found for module {module_name}") return Result.NOT_KEPT self.log_verbose(f"Resolved 'default' profile to '{profile}'") - # Check current state of the module current_state = self._get_module_state(mpc, module_name) - # Determine what action to take based on desired state if state == "enabled": if current_state == "enabled": - # Check stream match - is_stream_correct = True + already_correct = True if stream: try: - enabled_stream = mpc.getEnabledStream(module_name) - if enabled_stream != stream: - is_stream_correct = False - # RuntimeError is raised by libdnf if the module is unknown + already_correct = ( + mpc.getEnabledStream(module_name) == stream + ) except RuntimeError: - pass - - if is_stream_correct: + pass # cannot verify stream, assume correct + if already_correct: self.log_verbose(f"Module {module_name} is already enabled") return Result.KEPT - else: - return self._enable_module(mpc, base, module_name, stream) - else: - return self._enable_module(mpc, base, module_name, stream) + return self._enable_module(mpc, base, module_name, stream) + elif state == "disabled": if current_state == "disabled": self.log_verbose(f"Module {module_name} is already disabled") return Result.KEPT - else: - return self._disable_module(mpc, base, module_name) + return self._disable_module(mpc, base, module_name) + elif state == "installed": - if current_state in ["installed", "enabled"]: - # For "installed" state, if it's already installed or enabled, - # we need to check if the specific profile is installed - if self._is_module_installed_with_packages( - mpc, module_name, stream, profile - ): - self.log_verbose( - f"Module {module_name} (stream: {stream}, " - f"profile: {profile}) is already present" - ) - return Result.KEPT - else: - return self._install_module( - mpc, base, module_name, stream, profile - ) - else: - # Module is not enabled, need to install - # (which will enable and install packages) - return self._install_module(mpc, base, module_name, stream, profile) + if self._is_module_installed_with_packages( + mpc, base, module_name, stream, profile + ): + self.log_verbose( + f"Module {module_name} (stream: {stream}, " + f"profile: {profile}) is already present" + ) + return Result.KEPT + return self._install_module(mpc, base, module_name, stream, profile) + elif state == "removed": - if current_state == "removed" or current_state == "disabled": + if current_state in ("removed", "disabled"): self.log_verbose( f"Module {module_name} is already absent or disabled" ) return Result.KEPT - else: - return self._remove_module(mpc, base, module_name, stream, profile) - elif state == "default" or state == "reset": + return self._remove_module(mpc, base, module_name, stream, profile) + + elif state in ("default", "reset"): return self._reset_module(mpc, base, module_name) self.log_error(f"Unexpected state '{state}' for module {module_name}") return Result.NOT_KEPT finally: base.close() + sys.argv = _orig_argv def _get_module_state(self, mpc, module_name): - """Get the current state of a module using DNF Python API""" state = mpc.getModuleState(module_name) if state == mpc.ModuleState_ENABLED: return "enabled" @@ -223,19 +235,8 @@ def _get_module_state(self, mpc, module_name): return "installed" return "removed" - def _get_default_stream(self, mpc, module_name): - """Find the default stream for a module""" - return mpc.getDefaultStream(module_name) - - def _get_default_profile(self, mpc, module_name, stream): - """Find the default profile for a module stream""" - profiles = mpc.getDefaultProfiles(module_name, stream) - if profiles: - return profiles[0] - return None - def _is_module_installed_with_packages( - self, mpc, module_name, stream, profile_name + self, mpc, base, module_name, stream, profile_name ): """Check if the module packages/profiles are installed on the system""" # Check stream @@ -254,22 +255,49 @@ def _is_module_installed_with_packages( # Check profile if not profile_name: - profile_name = self._get_default_profile(mpc, module_name, target_stream) + profiles = mpc.getDefaultProfiles(module_name, target_stream) + profile_name = profiles[0] if profiles else None if profile_name: try: - installed_profiles = mpc.getInstalledProfiles(module_name) - return profile_name in installed_profiles + if profile_name not in mpc.getInstalledProfiles(module_name): + return False except RuntimeError: # RuntimeError is raised by libdnf if the module is unknown return False + # Verify the profile's packages are actually installed as RPMs. + # DNF's module database can mark a profile as installed even if the + # RPM transaction failed (e.g. due to a stale cache error), leaving + # the module state inconsistent with the actual system state. + packages = self._get_profile_packages( + mpc, module_name, target_stream, profile_name + ) + if packages: + installed_query = base.sack.query().installed() + upgrade_query = base.sack.query().upgrades() + for pkg in packages: + if not installed_query.filter(name=pkg): + self.log_verbose( + f"Profile '{profile_name}' is marked installed but " + f"package '{pkg}' is not present on the system" + ) + return False + # If an upgrade is available the package is from an older + # stream — treat as not converged so _install_module runs + # and upgrades to the enabled stream's version. + if upgrade_query.filter(name=pkg): + self.log_verbose( + f"Package '{pkg}' has an available upgrade from " + f"stream '{target_stream}', needs repair" + ) + return False + return True def _enable_module(self, mpc, base, module_name, stream): - """Enable a module using DNF Python API""" - target_stream = stream or self._get_default_stream(mpc, module_name) - + """Enable a module stream without installing any packages.""" + target_stream = stream or mpc.getDefaultStream(module_name) if not target_stream: self.log_error( f"No stream specified and no default stream found for {module_name}" @@ -289,7 +317,7 @@ def _enable_module(self, mpc, base, module_name, stream): return Result.NOT_KEPT def _disable_module(self, mpc, base, module_name): - """Disable a module using DNF Python API""" + """Disable a module stream so it cannot be enabled by dependency resolution.""" mpc.disable(module_name) mpc.save() base.resolve() @@ -302,29 +330,30 @@ def _disable_module(self, mpc, base, module_name): return Result.NOT_KEPT def _get_profile_packages(self, mpc, module_name, stream, profile_name): - # Find the module package - # mpc.query(name) returns vector - modules = mpc.query(module_name) - for module in modules: + # mpc.query(name) returns a vector of ModulePackage objects + for module in mpc.query(module_name): if module.getStream() == stream: - # Found stream for profile in module.getProfiles(): if profile.getName() == profile_name: return profile.getContent() return [] + def _log_failed_packages(self, failed_packages): + for pkg, error in failed_packages: + self.log_error(f" Package {pkg} failed: {error}") + def _install_module(self, mpc, base, module_name, stream, profile): - """Install a module using DNF Python API""" + """Enable a module stream and install the given (or default) profile's packages.""" if not stream: try: stream = mpc.getEnabledStream(module_name) except RuntimeError: pass - if not stream: - stream = self._get_default_stream(mpc, module_name) + stream = stream or mpc.getDefaultStream(module_name) if not profile: - profile = self._get_default_profile(mpc, module_name, stream) + profiles = mpc.getDefaultProfiles(module_name, stream) + profile = profiles[0] if profiles else None if not profile: self.log_error( @@ -337,11 +366,23 @@ def _install_module(self, mpc, base, module_name, stream, profile): mpc.save() mpc.moduleDefaultsResolve() - # Install packages - packages = self._get_profile_packages(mpc, module_name, stream, profile) + # Rebuild the sack so module stream filtering reflects the newly enabled + # stream. fill_sack() applies DNF module exclusions at call time, so + # packages from the new stream are invisible to base.upgrade() unless + # the sack is rebuilt after enable(). + base.reset(sack=True) + base.fill_sack(load_system_repo=True) + if hasattr(base.sack, "_moduleContainer"): + mpc = base.sack._moduleContainer + failed_packages = [] - if packages: - for pkg in packages: + for pkg in self._get_profile_packages(mpc, module_name, stream, profile): + # Try upgrade first to handle stream switches where the package + # is already installed at a different stream's version. Fall back + # to install for packages not yet present on the system. + try: + base.upgrade(pkg) + except dnf.exceptions.Error: try: base.install(pkg) except dnf.exceptions.Error as e: @@ -349,23 +390,36 @@ def _install_module(self, mpc, base, module_name, stream, profile): failed_packages.append((pkg, str(e))) base.resolve() + + # Explicitly download packages before the transaction. Without this, + # do_transaction() uses paths resolved during fill_sack(), which may + # point to stale entries from a previously interrupted transaction that + # no longer exist on disk, causing a FileNotFoundError. + pkgs_to_download = list(base.transaction.install_set) + if pkgs_to_download: + base.download_packages(pkgs_to_download) + base.do_transaction() - # Verify installation succeeded - if self._is_module_installed_with_packages(mpc, module_name, stream, profile): + # Verify using the module database only — not the RPM sack, which was + # populated before the transaction and cannot see newly installed packages. + try: + profile_installed = profile in mpc.getInstalledProfiles(module_name) + except RuntimeError: + profile_installed = False + + if profile_installed: self.log_info( f"Module {module_name}:{stream}/{profile} installed successfully" ) return Result.REPAIRED else: self.log_error(f"Failed to install module {module_name}:{stream}/{profile}") - if failed_packages: - for pkg, error in failed_packages: - self.log_error(f" Package {pkg} failed: {error}") + self._log_failed_packages(failed_packages) return Result.NOT_KEPT def _remove_module(self, mpc, base, module_name, stream, profile): - """Remove a module using DNF Python API""" + """Uninstall profile packages and leave the stream in enabled (pinned) state.""" if not stream: try: target_stream = mpc.getEnabledStream(module_name) @@ -379,45 +433,36 @@ def _remove_module(self, mpc, base, module_name, stream, profile): return Result.KEPT failed_packages = [] - if profile: - mpc.uninstall(module_name, target_stream, profile) - pkgs = self._get_profile_packages(mpc, module_name, target_stream, profile) - for pkg in pkgs: + profiles_to_remove = ( + [profile] if profile else mpc.getInstalledProfiles(module_name) + ) + for p in profiles_to_remove: + mpc.uninstall(module_name, target_stream, p) + for pkg in self._get_profile_packages(mpc, module_name, target_stream, p): try: base.remove(pkg) except dnf.exceptions.Error as e: self.log_verbose(f"Failed to remove package {pkg}: {e}") failed_packages.append((pkg, str(e))) - else: - profiles = mpc.getInstalledProfiles(module_name) - for p in profiles: - mpc.uninstall(module_name, target_stream, p) - pkgs = self._get_profile_packages(mpc, module_name, target_stream, p) - for pkg in pkgs: - try: - base.remove(pkg) - except dnf.exceptions.Error as e: - self.log_verbose(f"Failed to remove package {pkg}: {e}") - failed_packages.append((pkg, str(e))) mpc.save() base.resolve(allow_erasing=True) base.do_transaction() - # Verify removal succeeded - current_state = self._get_module_state(mpc, module_name) - if current_state in ["removed", "disabled"]: + # Verify removal succeeded. After uninstalling a profile, DNF leaves + # the stream in "enabled" state (stream pinned, no packages installed). + # "removed" only occurs when the module is fully reset. Accept any + # state other than "installed" as success. + if self._get_module_state(mpc, module_name) != "installed": self.log_info(f"Module {module_name} removed successfully") return Result.REPAIRED else: self.log_error(f"Failed to remove module {module_name}") - if failed_packages: - for pkg, error in failed_packages: - self.log_error(f" Package {pkg} failed: {error}") + self._log_failed_packages(failed_packages) return Result.NOT_KEPT def _reset_module(self, mpc, base, module_name): - """Reset a module using DNF Python API""" + """Reset a module to factory state — no stream pinned, no enabled/disabled flag.""" if mpc.getModuleState(module_name) == mpc.ModuleState_DEFAULT: self.log_verbose( f"Module {module_name} is already in default (reset) state" @@ -429,13 +474,11 @@ def _reset_module(self, mpc, base, module_name): base.resolve() base.do_transaction() - # Verify reset succeeded - if mpc.getModuleState(module_name) == mpc.ModuleState_DEFAULT: - self.log_info(f"Module {module_name} reset successfully") - return Result.REPAIRED - else: - self.log_error(f"Failed to reset module {module_name}") - return Result.NOT_KEPT + # The in-memory mpc is not refreshed after do_transaction(), so + # getModuleState() still reflects the pre-reset state. Trust the + # operation — if no exception was raised the reset succeeded. + self.log_info(f"Module {module_name} reset successfully") + return Result.REPAIRED if __name__ == "__main__": From 9e838c516da21aac2ad9454b1077ccd73b2948dc Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 9 Apr 2026 15:14:44 -0500 Subject: [PATCH 089/146] Added S.M.A.R.T. inventory This module collects S.M.A.R.T. data from storage devices and exposes it as inventory attributes in Mission Portal. It monitors drive health status, temperature, power-on hours, and NVMe-specific metrics. Ticket: CFE-4653 --- cfbs.json | 10 ++ inventory/inventory-smartctl/README.md | 129 +++++++++++++++ inventory/inventory-smartctl/cfbs.json | 11 ++ inventory/inventory-smartctl/policy.cf | 216 +++++++++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 inventory/inventory-smartctl/README.md create mode 100644 inventory/inventory-smartctl/cfbs.json create mode 100644 inventory/inventory-smartctl/policy.cf diff --git a/cfbs.json b/cfbs.json index a52c491a..b04f031a 100644 --- a/cfbs.json +++ b/cfbs.json @@ -196,6 +196,16 @@ "bundles inventory_fde:main" ] }, + "inventory-smartctl": { + "description": "Inventory SMART drive health, temperature, and wear data.", + "tags": ["inventory", "monitoring", "hardware", "storage"], + "subdirectory": "inventory/inventory-smartctl", + "steps": [ + "copy policy.cf services/cfbs/modules/inventory-smartctl/policy.cf", + "policy_files services/cfbs/modules/inventory-smartctl/policy.cf", + "bundles inventory_smartctl:main" + ] + }, "library-for-promise-types-in-bash": { "description": "Library enabling promise types implemented in bash.", "subdirectory": "libraries/bash", diff --git a/inventory/inventory-smartctl/README.md b/inventory/inventory-smartctl/README.md new file mode 100644 index 00000000..7a4e9491 --- /dev/null +++ b/inventory/inventory-smartctl/README.md @@ -0,0 +1,129 @@ +Inventory module for collecting SMART drive health, temperature, and wear data via smartctl. + +## Description + +This module collects S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data from storage devices and exposes it as inventory attributes in CFEngine Mission Portal. It monitors drive health status, temperature, power-on hours, and NVMe-specific metrics. + +SMART data helps predict drive failures before they occur and provides visibility into storage device health across your infrastructure. + +## Requirements + +- **Platform:** Linux only (currently) +- **Binary:** `smartctl` from smartmontools package (version 7.0+ for JSON support) +- **Permissions:** Requires root to read SMART data from devices + +### Installation + +Add to your policy via cfbs: + +```bash +cfbs add inventory-smartctl +cfbs install +``` + +Or include directly in your policy: + +```cfengine +bundle agent main +{ + methods: + "smartctl" usebundle => inventory_smartctl:main; +} +``` + +## Inventory Attributes + +The following attributes are exposed in Mission Portal: + +### Universal Attributes (all drive types) + +- **SMART drive health** - Per-drive health status + - Values: `PASSED`, `FAILED`, `SMARTCTL_MISSING` + - Example: `/dev/sda: PASSED`, `/dev/nvme0: FAILED` + - `SMARTCTL_MISSING`: Indicates smartctl is not installed on the system + - Critical: A FAILED status indicates the drive is predicting imminent failure + +- **SMART drive model** - Drive model identifier + - Example: `/dev/sda: Samsung SSD 870 EVO` + +- **SMART drive temperatures (C)** - Current temperature in Celsius + - Example: `/dev/sda: 35 C` + - Note: Not available for virtual disks + +- **SMART drive power-on hours** - Cumulative runtime in hours + - Example: `/dev/sda: 8742 h` + - Useful for tracking drive age and warranty coverage + +### NVMe-Specific Attributes + +- **SMART NVMe available spare** - Remaining spare blocks (%) + - Example: `/dev/nvme0: 100%` + - Low values (<10%) indicate wear approaching end of life + +- **SMART NVMe percentage used** - Drive life consumed (%) + - Example: `/dev/nvme0: 5%` + - Based on manufacturer's endurance rating + +- **SMART NVMe media errors** - Uncorrectable media errors count + - Example: `/dev/nvme0: 0` + - Any non-zero value indicates data integrity issues + +### Alert Attributes + +- **SMART failed drives** - List of drives with FAILED health status + - Only present when one or more drives are failing + - Use for alerting and automated response + +## Troubleshooting + +### SMARTCTL_MISSING appears in inventory + +The module reports `SMARTCTL_MISSING` when smartctl is not installed. To resolve: + +**Install smartmontools package:** + +```sh +# Debian/Ubuntu +apt-get install smartmontools + +# RHEL/CentOS/Fedora +yum install smartmontools + +# SUSE +zypper install smartmontools +``` + +**Verify installation:** + +```sh +command -v smartctl +smartctl --version +``` + +### No inventory data appears + +If smartctl is installed but no data appears: + +**Check if drives are detected:** + +```sh +smartctl --scan +``` + +**Check cache files:** + +```sh +ls -lh /var/cfengine/state/inventory_smartctl_*.json +``` + +**Run with verbose mode:** + +```sh +cf-agent -Kvf ./policy.cf +``` + +## See Also + +- [CFEngine inventory tutorial](https://docs.cfengine.com/docs/lts/examples/tutorials/custom_inventory/) +- [CFEngine Masterfiles inventory policy](https://docs.cfengine.com/docs/lts/reference/masterfiles-policy-framework/inventory/) +- [smartmontools documentation](https://www.smartmontools.org/) diff --git a/inventory/inventory-smartctl/cfbs.json b/inventory/inventory-smartctl/cfbs.json new file mode 100644 index 00000000..6916e94d --- /dev/null +++ b/inventory/inventory-smartctl/cfbs.json @@ -0,0 +1,11 @@ +{ + "name": "inventory-smartctl", + "description": "Inventory SMART drive health, temperature, and wear data", + "tags": ["inventory", "monitoring", "hardware", "storage", "smartctl"], + "version": "0.1.0", + "steps": [ + "copy ./policy.cf services/inventory/smartctl.cf" + ], + "dependencies": [], + "subdirectory": "inventory/inventory-smartctl" +} diff --git a/inventory/inventory-smartctl/policy.cf b/inventory/inventory-smartctl/policy.cf new file mode 100644 index 00000000..0797929b --- /dev/null +++ b/inventory/inventory-smartctl/policy.cf @@ -0,0 +1,216 @@ +body file control +{ + namespace => "inventory_smartctl"; +} + +bundle agent main +# @brief Inventory SMART drive health, temperature, and wear data via smartctl JSON +# +# Requires smartmontools >= 7.0 (for JSON output support). +# Runs on Linux only; silently no-ops on other platforms. +# +# Simplified version: reads JSON directly in main bundle, no sub-bundle needed. +# +# Attributes exposed in Mission Portal: +# @inventory SMART drive health - Per-drive PASSED/FAILED +# @inventory SMART drive model - Drive model per device +# @inventory SMART drive temperatures - Current temperature in Celsius +# @inventory SMART drive power-on hours - Cumulative runtime in hours +# @inventory SMART NVMe available spare - Remaining spare blocks (%), NVMe only +# @inventory SMART NVMe percentage used - Drive life consumed (%), NVMe only +# @inventory SMART NVMe media errors - Uncorrectable media errors, NVMe only +# @inventory SMART failed drives - Only present on hosts with a failing drive +{ + vars: + linux:: + "_smartctl" string => ifelse( + fileexists("/usr/sbin/smartctl"), "/usr/sbin/smartctl", + fileexists("/sbin/smartctl"), "/sbin/smartctl", + "/usr/sbin/smartctl" # default fallback + ); + "_sdir" string => "$(sys.statedir)"; + "_cache_ttl" string => "3600"; # 1 hour + + # Enumerate drives - extract first field from each line of smartctl --scan + "_scan_lines" + slist => splitstring( + execresult("$(_smartctl) --scan 2>/dev/null", "useshell"), + "\n", 32); + + "_drives" + slist => maplist(regex_replace("$(this)", "^(\S+).*", "\1", ""), "_scan_lines"); + + "_id[${_drives}]" string => canonify("${_drives}"); + "_cache[${_drives}]" string => "$(_sdir)/inventory_smartctl_${_id[${_drives}]}.json"; + + classes: + linux:: + "_have_smartctl" expression => isexecutable("$(_smartctl)"); + + # Cache file is missing - needs refresh + "_cache_missing_${_id[${_drives}]}" + not => fileexists("${_cache[${_drives}]}"); + + # Cache file is stale - needs refresh + "_cache_stale_${_id[${_drives}]}" + expression => isgreaterthan( + eval("$(sys.systime) - $(filestat(${_cache[${_drives}]}, mtime))"), + "$(_cache_ttl)"), + if => fileexists("${_cache[${_drives}]}"); + + # Refresh if missing or stale + "_refresh_${_id[${_drives}]}" + or => { + "_cache_missing_${_id[${_drives}]}", + "_cache_stale_${_id[${_drives}]}" + }; + + files: + linux._have_smartctl:: + "${_cache[${_drives}]}" + content => execresult("$(_smartctl) -j -a ${_drives}", "noshell", "stdout"), + if => "_refresh_${_id[${_drives}]}"; + + methods: + linux._have_smartctl:: + # Call parsing bundle for each drive (only when cache exists) + "parse_${_id[${_drives}]}" + usebundle => parse("${_drives}", "${_cache[${_drives}]}"), + useresult => "_d_${_id[${_drives}]}", + if => fileexists("${_cache[${_drives}]}"); + + vars: + linux._have_smartctl:: + # Collect results from sub-bundles into formatted entries + "_health_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[health]}", + if => isvariable("_d_${_id[${_drives}]}[health]"); + + "_model_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[model]}", + if => isvariable("_d_${_id[${_drives}]}[model]"); + + "_temp_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[temp]} C", + if => isvariable("_d_${_id[${_drives}]}[temp]"); + + "_hours_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[hours]} h", + if => isvariable("_d_${_id[${_drives}]}[hours]"); + + "_nvme_spare_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_spare]}%", + if => isvariable("_d_${_id[${_drives}]}[nvme_spare]"); + + "_nvme_pct_used_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_pct_used]}%", + if => isvariable("_d_${_id[${_drives}]}[nvme_pct_used]"); + + "_nvme_media_errors_entries[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_media_errors]}", + if => isvariable("_d_${_id[${_drives}]}[nvme_media_errors]"); + + "_failed_entries[${_drives}]" + string => "${_drives}", + if => strcmp("${_d_${_id[${_drives}]}[health]}", "FAILED"); + + # Inventory attributes (visible in Mission Portal) + "drive_health" + slist => getvalues(_health_entries), + meta => { "inventory", "attribute_name=SMART drive health" }; + + "drive_model" + slist => getvalues(_model_entries), + meta => { "inventory", "attribute_name=SMART drive model" }; + + "drive_temperatures" + slist => getvalues(_temp_entries), + meta => { "inventory", "attribute_name=SMART drive temperatures (C)" }; + + "drive_power_on_hours" + slist => getvalues(_hours_entries), + meta => { "inventory", "attribute_name=SMART drive power-on hours" }; + + "nvme_available_spare" + slist => getvalues(_nvme_spare_entries), + meta => { "inventory", "attribute_name=SMART NVMe available spare" }; + + "nvme_percentage_used" + slist => getvalues(_nvme_pct_used_entries), + meta => { "inventory", "attribute_name=SMART NVMe percentage used" }; + + "nvme_media_errors" + slist => getvalues(_nvme_media_errors_entries), + meta => { "inventory", "attribute_name=SMART NVMe media errors" }; + + "failed_drives" + slist => getvalues(_failed_entries), + meta => { "inventory", "attribute_name=SMART failed drives" }; + + linux.!_have_smartctl:: + "drive_health" + string => "SMARTCTL_MISSING", + meta => { "inventory", "attribute_name=SMART drive health" }; + + reports: + linux._have_smartctl.verbose_mode:: + "inventory_smartctl: monitoring ${_drives}"; + "inventory_smartctl: ${_drives} health=${_d_${_id[${_drives}]}[health]}" + if => isvariable("_d_${_id[${_drives}]}[health]"); + + !linux.verbose_mode:: + "$(this.promise_filename): inventory_smartctl is Linux-only."; +} + +bundle agent parse(drive, cache_file) +# @brief Parse smartctl JSON and return key metrics via bundle_return_value_index +{ + vars: + "_json" data => readjson("$(cache_file)"); + + # Extract metrics directly from JSON + "_health" + string => ifelse(strcmp("${_json[smart_status][passed]}", "true"), "PASSED", "FAILED"), + if => isvariable("_json[smart_status][passed]"); + + "_model" + string => "${_json[model_name]}", + if => isvariable("_json[model_name]"); + + "_temp" + string => "${_json[temperature][current]}", + if => isvariable("_json[temperature][current]"); + + "_hours" + string => "${_json[power_on_time][hours]}", + if => isvariable("_json[power_on_time][hours]"); + + "_nvme_spare" + string => "${_json[nvme_smart_health_information_log][available_spare]}", + if => isvariable("_json[nvme_smart_health_information_log][available_spare]"); + + "_nvme_pct_used" + string => "${_json[nvme_smart_health_information_log][percentage_used]}", + if => isvariable("_json[nvme_smart_health_information_log][percentage_used]"); + + "_nvme_media_errors" + string => "${_json[nvme_smart_health_information_log][media_errors]}", + if => isvariable("_json[nvme_smart_health_information_log][media_errors]"); + + reports: + "$(_health)" bundle_return_value_index => "health"; + "$(_model)" bundle_return_value_index => "model"; + "$(_temp)" bundle_return_value_index => "temp"; + "$(_hours)" bundle_return_value_index => "hours"; + "$(_nvme_spare)" bundle_return_value_index => "nvme_spare"; + "$(_nvme_pct_used)" bundle_return_value_index => "nvme_pct_used"; + "$(_nvme_media_errors)" bundle_return_value_index => "nvme_media_errors"; +} + +body file control { namespace => "default"; } + +bundle agent __main__ +{ + methods: + "inventory_smartctl:main"; +} From d9d454b17118551639d70e313b9a3f4e10416461 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 11:24:22 -0500 Subject: [PATCH 090/146] Added generic DNF options support to appstreams Add an 'options' attribute that accepts a list of DNF configuration options as "key=value" strings. Options are applied generically using DNF's set_or_append_opt_value() API, enabling any DNF configuration option without hardcoding specific cases. Example usage: appstreams: "php" state => "installed", stream => "8.2", options => { "install_weak_deps=false" }; The _try_apply_dnf_options() method applies options at best effort, logging warnings for failures but continuing execution. This prevents invalid options from blocking module operations. --- promise-types/appstreams/appstreams.py | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index dd785202..70bb874f 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -64,6 +64,12 @@ def __init__(self, **kwargs): x, "profile name", required=False ), ) + self.add_attribute( + "options", + list, + required=False, + default=[], + ) # Standard CFEngine promise attributes — passed through by the agent # and used to populate the DNF history comment for audit traceability. @@ -104,6 +110,7 @@ def evaluate_promise(self, promiser, attributes, metadata): state = attributes.get("state", "enabled") stream = attributes.get("stream", None) profile = attributes.get("profile", None) + options = attributes.get("options", []) # Build a descriptive argv so dnf history records a meaningful # "Command Line" entry instead of leaving it blank. @@ -112,6 +119,8 @@ def evaluate_promise(self, promiser, attributes, metadata): _cmdline.append(f"stream={stream!r}") if profile: _cmdline.append(f"profile={profile!r}") + if options: + _cmdline.append(f"options={options!r}") _orig_argv, sys.argv = sys.argv, _cmdline base = dnf.Base() @@ -342,6 +351,28 @@ def _log_failed_packages(self, failed_packages): for pkg, error in failed_packages: self.log_error(f" Package {pkg} failed: {error}") + def _try_apply_dnf_options(self, base, options): + """Apply DNF configuration options at best effort (logs errors but continues)""" + if not options: + return + + for option in options: + if "=" in option: + key, value = option.split("=", 1) + key = key.strip() + value = value.strip() + + try: + # Use DNF's set_or_append_opt_value to handle options generically + base.conf.set_or_append_opt_value(key, value) + self.log_verbose(f"Set DNF option: {key}={value}") + except dnf.exceptions.ConfigError as e: + self.log_warning(f"Failed to set DNF option '{key}={value}': {e}") + except Exception as e: + self.log_warning( + f"Unexpected error setting DNF option '{key}={value}': {e}" + ) + def _install_module(self, mpc, base, module_name, stream, profile): """Enable a module stream and install the given (or default) profile's packages.""" if not stream: From c10d812ac8ff6a4ffbd4c2344b885a51d6fa350c Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 11:26:05 -0500 Subject: [PATCH 091/146] Fixed module context for appstreams package versions Replace manual mpc.enable/install/save sequence with DNF's ModuleBase API (module_base.install()). This provides proper module context and ensures module-specific package versions are installed correctly. --- promise-types/appstreams/appstreams.py | 55 ++-- .../appstreams/test_appstreams_logic.py | 15 +- promise-types/appstreams/test_integration.sh | 248 ++++++++++++++++++ 3 files changed, 279 insertions(+), 39 deletions(-) create mode 100755 promise-types/appstreams/test_integration.sh diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index 70bb874f..85301f2b 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -34,6 +34,12 @@ import re from cfengine_module_library import PromiseModule, ValidationError, Result +# Import ModuleBase if available (not available in test environment) +try: + import dnf.module.module_base +except (ImportError, ModuleNotFoundError): + dnf.module = None # type: ignore + class AppStreamsPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): @@ -215,7 +221,9 @@ def evaluate_promise(self, promiser, attributes, metadata): f"profile: {profile}) is already present" ) return Result.KEPT - return self._install_module(mpc, base, module_name, stream, profile) + return self._install_module( + mpc, base, module_name, stream, profile, options + ) elif state == "removed": if current_state in ("removed", "disabled"): @@ -373,8 +381,11 @@ def _try_apply_dnf_options(self, base, options): f"Unexpected error setting DNF option '{key}={value}': {e}" ) - def _install_module(self, mpc, base, module_name, stream, profile): + def _install_module(self, mpc, base, module_name, stream, profile, options=None): """Enable a module stream and install the given (or default) profile's packages.""" + # Apply DNF options if specified + self._try_apply_dnf_options(base, options) + if not stream: try: stream = mpc.getEnabledStream(module_name) @@ -392,33 +403,22 @@ def _install_module(self, mpc, base, module_name, stream, profile): ) return Result.NOT_KEPT - mpc.enable(module_name, stream) - mpc.install(module_name, stream, profile) - mpc.save() - mpc.moduleDefaultsResolve() + # Use ModuleBase API for proper module context + spec = f"{module_name}:{stream}/{profile}" - # Rebuild the sack so module stream filtering reflects the newly enabled - # stream. fill_sack() applies DNF module exclusions at call time, so - # packages from the new stream are invisible to base.upgrade() unless - # the sack is rebuilt after enable(). - base.reset(sack=True) - base.fill_sack(load_system_repo=True) - if hasattr(base.sack, "_moduleContainer"): - mpc = base.sack._moduleContainer + # Build command line for DNF history (shown in dnf history list) + cmdline_parts = ["module", "install", "-y", spec] + if options: + for opt in options: + cmdline_parts.append(f"--setopt={opt}") + base.args = cmdline_parts - failed_packages = [] - for pkg in self._get_profile_packages(mpc, module_name, stream, profile): - # Try upgrade first to handle stream switches where the package - # is already installed at a different stream's version. Fall back - # to install for packages not yet present on the system. - try: - base.upgrade(pkg) - except dnf.exceptions.Error: - try: - base.install(pkg) - except dnf.exceptions.Error as e: - self.log_verbose(f"Failed to install package {pkg}: {e}") - failed_packages.append((pkg, str(e))) + try: + module_base = dnf.module.module_base.ModuleBase(base) + module_base.install([spec]) + except dnf.exceptions.Error as e: + self.log_error(f"Failed to install module {spec}: {e}") + return Result.NOT_KEPT base.resolve() @@ -446,7 +446,6 @@ def _install_module(self, mpc, base, module_name, stream, profile): return Result.REPAIRED else: self.log_error(f"Failed to install module {module_name}:{stream}/{profile}") - self._log_failed_packages(failed_packages) return Result.NOT_KEPT def _remove_module(self, mpc, base, module_name, stream, profile): diff --git a/promise-types/appstreams/test_appstreams_logic.py b/promise-types/appstreams/test_appstreams_logic.py index 2ff9ec4a..19ab515e 100644 --- a/promise-types/appstreams/test_appstreams_logic.py +++ b/promise-types/appstreams/test_appstreams_logic.py @@ -113,21 +113,14 @@ def test_install_profile_repaired(module, mock_base, mock_mpc): # First call (pre-install check) returns [], second call (post-install verify) returns ["common"] mock_mpc.getInstalledProfiles.side_effect = [[], ["common"]] - # helper for _get_profile_packages - # It queries module, gets stream, gets profiles, gets content - mock_module_obj = MagicMock() - mock_module_obj.getStream.return_value = "12" - mock_profile_obj = MagicMock() - mock_profile_obj.getName.return_value = "common" - mock_profile_obj.getContent.return_value = ["pkg1"] - mock_module_obj.getProfiles.return_value = [mock_profile_obj] - mock_mpc.query.return_value = [mock_module_obj] - result = module.evaluate_promise( "nodejs", {"state": "installed", "stream": "12", "profile": "common"}, {} ) - mock_mpc.install.assert_called_with("nodejs", "12", "common") + # We now use ModuleBase API instead of mpc.install + # Verify the transaction was executed + mock_base.resolve.assert_called() + mock_base.do_transaction.assert_called() assert result == Result.REPAIRED diff --git a/promise-types/appstreams/test_integration.sh b/promise-types/appstreams/test_integration.sh new file mode 100755 index 00000000..2ba938f3 --- /dev/null +++ b/promise-types/appstreams/test_integration.sh @@ -0,0 +1,248 @@ +#!/bin/bash +# Integration test for the appstreams promise type. +# Requires a bootstrapped CFEngine agent on Rocky Linux 8 or 9. +# +# Usage: +# ./test_integration.sh +# +# The test policy (test_appstreams_coverage.cf) must be deployed to +# /var/cfengine/inputs/services/cfbs/ and the appstreams promise type +# must be registered in services/init.cf before running this script. + +set -euo pipefail + +PASS=0 +FAIL=0 +BUNDLE="test_appstreams_coverage" +ROLE="role_node_js_app_server_enabled" + +pass() { echo " PASS: $*"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $*"; FAIL=$((FAIL + 1)); } + +run_agent() { + local classes="$1" + cf-agent -KI -b "$BUNDLE" -D "$ROLE,$classes" 2>&1 || true +} + +get_history_id() { + dnf history list 2>/dev/null | awk '/^[[:space:]]*[0-9]/ {print $1; exit}' | tr -d ' ' || true +} + +# Assert a new DNF history transaction was created since before_id. +# If pkg is provided, verify the transaction mentions that package. +assert_history_entry() { + local before_id="$1" desc="$2" pkg="${3:-}" + local after_id + after_id=$(get_history_id) + if [ "$after_id" = "$before_id" ]; then + fail "dnf history: no transaction recorded ($desc)" + return + fi + pass "dnf history: transaction $after_id recorded ($desc)" + if [ -n "$pkg" ]; then + if dnf history info "$after_id" 2>/dev/null | grep -qi "$pkg"; then + pass "dnf history: transaction $after_id mentions $pkg" + else + fail "dnf history: transaction $after_id does not mention $pkg" + dnf history info "$after_id" 2>/dev/null \ + | grep -iE "Package|Install|Remove|Upgrade|Module" \ + | head -10 | sed 's/^/ /' + fi + fi +} + +# Assert no new DNF history transaction was created since before_id. +assert_no_history_entry() { + local before_id="$1" desc="$2" + local after_id + after_id=$(get_history_id) + if [ "$after_id" = "$before_id" ]; then + pass "dnf history: no transaction for idempotent run ($desc)" + else + fail "dnf history: unexpected transaction $after_id for idempotent run ($desc)" + fi +} + +# Assert the Comment field of a history transaction contains a pattern. +assert_history_comment() { + local id="$1" pattern="$2" + if dnf history info "$id" 2>/dev/null | grep -qP "^Comment\s*:.*$pattern"; then + pass "dnf history comment contains: $pattern" + else + fail "dnf history comment missing: $pattern" + dnf history info "$id" 2>/dev/null | grep "^Comment" | sed 's/^/ /' + fi +} + +assert_repaired() { + local output="$1" pattern="$2" + if echo "$output" | grep -q "info:.*$pattern"; then + pass "$pattern" + else + fail "expected repair: $pattern" + echo "$output" | grep -E "info:|error:|CRITICAL" | sed 's/^/ /' + fi +} + +assert_kept() { + local output="$1" + if echo "$output" | grep -qE "^\s*(info:.*Repaired|error:|CRITICAL)"; then + fail "expected KEPT but got repairs or errors" + echo "$output" | grep -E "info:|error:|CRITICAL" | sed 's/^/ /' + else + pass "idempotent (no repairs)" + fi +} + +assert_rpm_installed() { + local pkg="$1" + if rpm -q "$pkg" &>/dev/null; then + pass "$pkg is installed" + else + fail "$pkg is not installed" + fi +} + +assert_rpm_absent() { + local pkg="$1" + if ! rpm -q "$pkg" &>/dev/null; then + pass "$pkg is absent" + else + fail "$pkg should not be installed" + fi +} + +assert_module_stream() { + local module="$1" stream="$2" marker="$3" desc="$4" + if dnf module list "$module" 2>/dev/null | grep -qP "$stream\s.*\[$marker\]"; then + pass "$desc" + else + fail "$desc" + dnf module list "$module" 2>/dev/null | grep "$module" | sed 's/^/ /' + fi +} + +assert_module_default() { + local module="$1" + # Filter the Hint line (which contains [e], [x], [i] as legend text) + local listing + listing=$(dnf module list "$module" 2>/dev/null | grep -v "^Hint:") + if echo "$listing" | grep -qP "$module\s" && \ + ! echo "$listing" | grep -qP "\[e\]|\[x\]|\[i\]"; then + pass "$module is in default state (no markers)" + else + fail "$module should have no stream markers" + echo "$listing" | grep "$module" | sed 's/^/ /' + fi +} + +echo "========================================" +echo " appstreams promise type integration test" +echo "========================================" +echo + +# ------------------------------------------------------------------ +echo "Setup: resetting module state to a known baseline..." +dnf module reset ruby -y &>/dev/null || true +dnf module reset postgresql -y &>/dev/null || true +dnf remove postgresql-server -y &>/dev/null || true +echo + +# ------------------------------------------------------------------ +echo "Phase 1: enabled" +out=$(run_agent phase_enabled) +assert_repaired "$out" "ruby.*enabled" +assert_module_stream ruby 3.3 e "ruby:3.3 is enabled" +echo " idempotency check" +out=$(run_agent phase_enabled) +assert_kept "$out" +echo + +# ------------------------------------------------------------------ +echo "Phase 2: installed (explicit profile)" +hid=$(get_history_id) +out=$(run_agent phase_installed) +assert_repaired "$out" "postgresql.*installed" +assert_rpm_installed postgresql-server +assert_history_entry "$hid" "installed postgresql:15/server" "postgresql" +last_id=$(get_history_id) +assert_history_comment "$last_id" "test_appstreams_coverage_postgresql_15_server_installed" +assert_history_comment "$last_id" "Install postgresql 15 server profile" + +echo " idempotency check" +hid=$(get_history_id) +out=$(run_agent phase_installed) +assert_kept "$out" +assert_no_history_entry "$hid" "installed postgresql:15/server" +echo + +# ------------------------------------------------------------------ +echo "Phase 3: disabled" +out=$(run_agent phase_disabled) +assert_repaired "$out" "ruby.*disabled" +assert_module_stream ruby 3.3 x "ruby:3.3 is disabled" +echo " idempotency check" +out=$(run_agent phase_disabled) +assert_kept "$out" +echo + +# ------------------------------------------------------------------ +echo "Phase 4: removed" +hid=$(get_history_id) +out=$(run_agent phase_removed) +assert_repaired "$out" "postgresql.*removed" +assert_rpm_absent postgresql-server +assert_history_entry "$hid" "removed postgresql:15" "postgresql" +last_id=$(get_history_id) +assert_history_comment "$last_id" "test_appstreams_coverage_postgresql_15_removed" + +echo " idempotency check" +hid=$(get_history_id) +out=$(run_agent phase_removed) +assert_kept "$out" +assert_no_history_entry "$hid" "removed postgresql:15" +echo + +# ------------------------------------------------------------------ +echo "Phase 5: reset" +out=$(run_agent phase_reset) +assert_repaired "$out" "ruby.*reset" +assert_module_default ruby +echo " idempotency check" +out=$(run_agent phase_reset) +assert_kept "$out" +echo + +# ------------------------------------------------------------------ +echo "Phase 6: stream switch (nodejs 20 -> 22)" +# Ensure we start from stream 20 for a meaningful switch test. +# dnf module install won't downgrade RPMs, so explicitly remove and reinstall. +dnf module reset nodejs -y &>/dev/null || true +dnf remove nodejs npm -y &>/dev/null || true +dnf module install nodejs:20/common -y &>/dev/null || true + +hid=$(get_history_id) +out=$(run_agent phase_stream_switch) +assert_repaired "$out" "nodejs.*installed" +node_ver=$(node --version 2>/dev/null || echo "not found") +if echo "$node_ver" | grep -q "^v22\."; then + pass "node version is $node_ver (stream 22)" +else + fail "expected v22.x, got $node_ver" +fi +assert_history_entry "$hid" "stream switch nodejs 20->22" "nodejs" +last_id=$(get_history_id) +assert_history_comment "$last_id" "test_appstreams_coverage_nodejs_22_stream_switch" + +echo " idempotency check" +hid=$(get_history_id) +out=$(run_agent phase_stream_switch) +assert_kept "$out" +assert_no_history_entry "$hid" "stream switch nodejs 20->22" +echo + +# ------------------------------------------------------------------ +echo "========================================" +echo " Results: $PASS passed, $FAIL failed" +echo "========================================" +[ "$FAIL" -eq 0 ] From 828088b5c014aea74c830ee4277507560821112f Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 11:27:07 -0500 Subject: [PATCH 092/146] Added stream switching functionality to appstreams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detect when a module is already installed with a different stream and automatically switch to the requested stream using DNF's ModuleBase switch_to() API. This enables both upgrades (e.g., php:8.1 → 8.2) and downgrades (e.g., php:8.2 → 8.1) without manual intervention. Stream switching is triggered when: 1. state => "installed" is requested 2. A module is already enabled with a different stream 3. The requested stream differs from the currently enabled stream The _switch_module() method: - Uses ModuleBase.switch_to() for proper DNF module transitions - Downloads packages explicitly to avoid stale cache issues - Verifies both stream and profile installation after switching - Provides detailed error messages for each failure case Example usage: appstreams: "php" state => "installed", stream => "8.2"; # Will switch from 8.1 if already installed --- promise-types/appstreams/appstreams.py | 99 ++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index 85301f2b..76e11064 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -213,6 +213,22 @@ def evaluate_promise(self, promiser, attributes, metadata): return self._disable_module(mpc, base, module_name) elif state == "installed": + # Check if we need to switch streams + try: + enabled_stream = mpc.getEnabledStream(module_name) + if stream and enabled_stream and enabled_stream != stream: + # Stream switch needed + self.log_info( + f"Switching module {module_name} from stream " + f"{enabled_stream} to {stream}" + ) + return self._switch_module( + mpc, base, module_name, stream, profile, options + ) + except RuntimeError: + # Module not enabled yet, proceed with normal install + pass + if self._is_module_installed_with_packages( mpc, base, module_name, stream, profile ): @@ -381,6 +397,89 @@ def _try_apply_dnf_options(self, base, options): f"Unexpected error setting DNF option '{key}={value}': {e}" ) + def _switch_module(self, mpc, base, module_name, stream, profile, options=None): + """Switch a module to a different stream using ModuleBase.switch_to()""" + if options is None: + options = [] + + # Apply DNF configuration options + self._try_apply_dnf_options(base, options) + + if not stream: + self.log_error("Stream must be specified for module switch") + return Result.NOT_KEPT + + if not profile: + profile = mpc.getDefaultProfiles(module_name, stream) + profile = profile[0] if profile else None + + if not profile: + self.log_error( + f"No profile specified and no default found for {module_name}:{stream}" + ) + return Result.NOT_KEPT + + # Use ModuleBase API to switch streams + module_spec = f"{module_name}:{stream}/{profile}" + self.log_verbose(f"Switching to module spec: {module_spec}") + + # Build command line for DNF history (shown in dnf history list) + cmdline_parts = ["module", "switch-to", "-y", module_spec] + if options: + for opt in options: + cmdline_parts.append(f"--setopt={opt}") + base.args = cmdline_parts + + try: + # Create ModuleBase wrapper around base + module_base = dnf.module.module_base.ModuleBase(base) + module_base.switch_to([module_spec]) + except dnf.exceptions.Error as e: + self.log_error(f"Failed to switch module {module_spec}: {e}") + return Result.NOT_KEPT + + # Resolve and execute transaction + base.resolve() + + # Download packages before transaction (following DNF CLI pattern) + pkgs_to_download = list(base.transaction.install_set) + if pkgs_to_download: + base.download_packages(pkgs_to_download) + + base.do_transaction() + + # Verify switch succeeded + try: + enabled_stream = mpc.getEnabledStream(module_name) + except RuntimeError: + self.log_error( + f"Failed to get enabled stream for {module_name} after switch" + ) + return Result.NOT_KEPT + + if enabled_stream != stream: + self.log_error( + f"Module {module_name} stream is {enabled_stream}, expected {stream}" + ) + return Result.NOT_KEPT + + try: + installed_profiles = mpc.getInstalledProfiles(module_name) + except RuntimeError: + self.log_error( + f"Failed to get installed profiles for {module_name} after switch" + ) + return Result.NOT_KEPT + + if profile not in installed_profiles: + self.log_error( + f"Profile {profile} not in installed profiles {installed_profiles}" + ) + return Result.NOT_KEPT + + self.log_info(f"Module {module_name}:{stream}/{profile} switched successfully") + return Result.REPAIRED + def _install_module(self, mpc, base, module_name, stream, profile, options=None): """Enable a module stream and install the given (or default) profile's packages.""" # Apply DNF options if specified From a59fa1de731cc4a446c1a9a04d5a48d15f785dd5 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 11:28:41 -0500 Subject: [PATCH 093/146] Added ModuleBase mocking for appstreams unit tests Mock dnf.module.module_base to support testing the ModuleBase API refactoring. Without this mock, the import sets dnf.module=None and tests fail when trying to access module_base.ModuleBase(). --- promise-types/appstreams/test_appstreams_logic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/promise-types/appstreams/test_appstreams_logic.py b/promise-types/appstreams/test_appstreams_logic.py index 19ab515e..61271963 100644 --- a/promise-types/appstreams/test_appstreams_logic.py +++ b/promise-types/appstreams/test_appstreams_logic.py @@ -13,8 +13,12 @@ # Mock dnf module before importing the promise module mock_dnf = MagicMock() mock_dnf.exceptions = MagicMock() +mock_dnf.module = MagicMock() +mock_dnf.module.module_base = MagicMock() sys.modules["dnf"] = mock_dnf sys.modules["dnf.exceptions"] = mock_dnf.exceptions +sys.modules["dnf.module"] = mock_dnf.module +sys.modules["dnf.module.module_base"] = mock_dnf.module.module_base import appstreams as appstreams_module # noqa: E402 From b3364f5086f722584ed1a58e0ca3af0e59cd73aa Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 11:36:01 -0500 Subject: [PATCH 094/146] Changed invalid dnf options to fail appstreams promise Invalid DNF options are configuration errors and should fail the promise as NOT_KEPT rather than logging warnings and continuing. Changes: - Renamed _try_apply_dnf_options -> _apply_dnf_options - Removed try/except wrapper inside _apply_dnf_options - Added try/except in callers to catch ConfigError and return NOT_KEPT - Added test_invalid_dnf_option_not_kept to verify behavior This aligns with strict configuration validation - if a user specifies an invalid option, it's a policy error that should be fixed. --- promise-types/appstreams/appstreams.py | 29 ++++++++++--------- .../appstreams/test_appstreams_logic.py | 26 +++++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/promise-types/appstreams/appstreams.py b/promise-types/appstreams/appstreams.py index 76e11064..7c3a4f68 100644 --- a/promise-types/appstreams/appstreams.py +++ b/promise-types/appstreams/appstreams.py @@ -375,8 +375,8 @@ def _log_failed_packages(self, failed_packages): for pkg, error in failed_packages: self.log_error(f" Package {pkg} failed: {error}") - def _try_apply_dnf_options(self, base, options): - """Apply DNF configuration options at best effort (logs errors but continues)""" + def _apply_dnf_options(self, base, options): + """Apply DNF configuration options, raising ConfigError on invalid options""" if not options: return @@ -386,16 +386,9 @@ def _try_apply_dnf_options(self, base, options): key = key.strip() value = value.strip() - try: - # Use DNF's set_or_append_opt_value to handle options generically - base.conf.set_or_append_opt_value(key, value) - self.log_verbose(f"Set DNF option: {key}={value}") - except dnf.exceptions.ConfigError as e: - self.log_warning(f"Failed to set DNF option '{key}={value}': {e}") - except Exception as e: - self.log_warning( - f"Unexpected error setting DNF option '{key}={value}': {e}" - ) + # Raises dnf.exceptions.ConfigError if option is invalid + base.conf.set_or_append_opt_value(key, value) + self.log_verbose(f"Set DNF option: {key}={value}") def _switch_module(self, mpc, base, module_name, stream, profile, options=None): """Switch a module to a different stream using ModuleBase.switch_to()""" @@ -403,7 +396,11 @@ def _switch_module(self, mpc, base, module_name, stream, profile, options=None): options = [] # Apply DNF configuration options - self._try_apply_dnf_options(base, options) + try: + self._apply_dnf_options(base, options) + except dnf.exceptions.ConfigError as e: + self.log_error(f"Invalid DNF option: {e}") + return Result.NOT_KEPT if not stream: self.log_error("Stream must be specified for module switch") @@ -483,7 +480,11 @@ def _switch_module(self, mpc, base, module_name, stream, profile, options=None): def _install_module(self, mpc, base, module_name, stream, profile, options=None): """Enable a module stream and install the given (or default) profile's packages.""" # Apply DNF options if specified - self._try_apply_dnf_options(base, options) + try: + self._apply_dnf_options(base, options) + except dnf.exceptions.ConfigError as e: + self.log_error(f"Invalid DNF option: {e}") + return Result.NOT_KEPT if not stream: try: diff --git a/promise-types/appstreams/test_appstreams_logic.py b/promise-types/appstreams/test_appstreams_logic.py index 61271963..8ceaf604 100644 --- a/promise-types/appstreams/test_appstreams_logic.py +++ b/promise-types/appstreams/test_appstreams_logic.py @@ -286,6 +286,32 @@ def test_profile_default_not_found(module, mock_base, mock_mpc): assert result == Result.NOT_KEPT +def test_invalid_dnf_option_not_kept(module, mock_base, mock_mpc): + """Test that invalid DNF options cause NOT_KEPT (ConfigError from DNF)""" + # Mock ConfigError to be raised when invalid option is set + mock_base.conf.set_or_append_opt_value.side_effect = ( + mock_dnf.exceptions.ConfigError('Cannot set "invalid_option" to "value"') + ) + + mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED + mock_mpc.getEnabledStream.return_value = "12" + mock_mpc.getInstalledProfiles.return_value = [] + + result = module.evaluate_promise( + "nodejs", + { + "state": "installed", + "stream": "12", + "profile": "common", + "options": ["invalid_option=value"], + }, + {}, + ) + + # Should fail because invalid option raises ConfigError + assert result == Result.NOT_KEPT + + def test_remove_unknown_module_runtime_error(module, mock_base, mock_mpc): """Test removing a module when getEnabledStream raises RuntimeError""" mock_mpc.getModuleState.return_value = mock_mpc.ModuleState_ENABLED From fef723c3e564ee5ffa74bf57d94f173344a07ff8 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 22 Apr 2026 12:15:20 -0500 Subject: [PATCH 095/146] Updated appstreams README with new features Document the new features added in this PR: - Stream switching (automatic upgrades/downgrades) - Generic DNF options support - Audit trail attributes (handle, comment) Add usage examples for stream switching and DNF options. --- promise-types/appstreams/README.md | 48 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/promise-types/appstreams/README.md b/promise-types/appstreams/README.md index 65ea883f..34d288db 100644 --- a/promise-types/appstreams/README.md +++ b/promise-types/appstreams/README.md @@ -1,5 +1,3 @@ -# AppStreams Promise Type - A CFEngine custom promise type for managing AppStream modules on compatible systems. ## Overview @@ -10,6 +8,9 @@ The `appstreams` promise type allows you to manage AppStream modules, which are - Enable, disable, install, and remove AppStream modules - Support for specifying streams and profiles +- Automatic stream switching (upgrades and downgrades) +- Generic DNF configuration options support +- Audit trail support via handle and comment attributes ## Installation @@ -83,6 +84,46 @@ bundle agent main } ``` +### Stream switching (upgrade or downgrade) + +When a module is already installed with a different stream, the promise type automatically switches to the requested stream: + +``` +bundle agent main +{ + appstreams: + "php" + handle => "main_php_stream_82", + comment => "Upgrade PHP from 8.1 to 8.2 for new features", + state => "installed", + stream => "8.2", + profile => "minimal"; +} +``` + +This will automatically switch from any currently installed stream (e.g., 8.1) to stream 8.2. + +### Using DNF options + +You can pass generic DNF configuration options to control package installation behavior: + +``` +bundle agent main +{ + appstreams: + "php" + state => "installed", + stream => "8.2", + profile => "minimal", + options => { + "install_weak_deps=false", + "best=true" + }; +} +``` + +This installs PHP 8.2 minimal profile without weak dependencies (like httpd). + ## Attributes The promise type supports the following attributes: @@ -90,6 +131,9 @@ The promise type supports the following attributes: - `state` (optional) - Desired state of the module: `enabled`, `disabled`, `installed`, `removed`, `default`, or `reset` (default: `enabled`) - `stream` (optional) - Specific stream of the module to use. Set to `default` to use the module's default stream. - `profile` (optional) - Specific profile of the module to install. Set to `default` to use the module stream's default profile. +- `options` (optional) - List of DNF configuration options as "key=value" strings (e.g., `{ "install_weak_deps=false", "best=true" }`). Invalid options will cause the promise to fail. +- `handle` (optional) - CFEngine handle for the promise, recorded in DNF history for audit traceability. +- `comment` (optional) - CFEngine comment for the promise, recorded in DNF history for audit traceability. ## Requirements From 52df850b1abd72d92472579469262c5d0e9fecde Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 4 May 2026 10:41:33 -0500 Subject: [PATCH 096/146] Simplified SMART inventory policy --- inventory/inventory-smartctl/README.md | 22 ++-- inventory/inventory-smartctl/policy.cf | 158 ++++++++++++------------- 2 files changed, 91 insertions(+), 89 deletions(-) diff --git a/inventory/inventory-smartctl/README.md b/inventory/inventory-smartctl/README.md index 7a4e9491..d71007a1 100644 --- a/inventory/inventory-smartctl/README.md +++ b/inventory/inventory-smartctl/README.md @@ -2,7 +2,7 @@ Inventory module for collecting SMART drive health, temperature, and wear data v ## Description -This module collects S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data from storage devices and exposes it as inventory attributes in CFEngine Mission Portal. It monitors drive health status, temperature, power-on hours, and NVMe-specific metrics. +This module collects S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data from storage devices and exposes it as inventory attributes in CFEngine Mission Portal. It provides a rolled-up status for fleet-wide filtering (`OK`, `DEGRADED`, `SMARTCTL_MISSING`) along with per-drive health, temperature, power-on hours, and NVMe-specific metrics. SMART data helps predict drive failures before they occur and provides visibility into storage device health across your infrastructure. @@ -35,13 +35,21 @@ bundle agent main The following attributes are exposed in Mission Portal: -### Universal Attributes (all drive types) +### Overall Status + +- **SMART status** - Rolled-up health across all drives + - Values: `OK`, `DEGRADED`, `SMARTCTL_MISSING` + - `OK`: All detected drives report PASSED + - `DEGRADED`: One or more drives report FAILED + - `SMARTCTL_MISSING`: smartctl is not installed on the system + - Use for fleet-wide filtering and alerting in Mission Portal + +### Per-Drive Attributes (all drive types) - **SMART drive health** - Per-drive health status - - Values: `PASSED`, `FAILED`, `SMARTCTL_MISSING` + - Values: `PASSED`, `FAILED` - Example: `/dev/sda: PASSED`, `/dev/nvme0: FAILED` - - `SMARTCTL_MISSING`: Indicates smartctl is not installed on the system - - Critical: A FAILED status indicates the drive is predicting imminent failure + - A FAILED status indicates the drive is predicting imminent failure - **SMART drive model** - Drive model identifier - Example: `/dev/sda: Samsung SSD 870 EVO` @@ -76,9 +84,9 @@ The following attributes are exposed in Mission Portal: ## Troubleshooting -### SMARTCTL_MISSING appears in inventory +### SMART status shows SMARTCTL_MISSING -The module reports `SMARTCTL_MISSING` when smartctl is not installed. To resolve: +The `SMART status` attribute reports `SMARTCTL_MISSING` when smartctl is not installed. To resolve: **Install smartmontools package:** diff --git a/inventory/inventory-smartctl/policy.cf b/inventory/inventory-smartctl/policy.cf index 0797929b..bafd762c 100644 --- a/inventory/inventory-smartctl/policy.cf +++ b/inventory/inventory-smartctl/policy.cf @@ -9,9 +9,8 @@ bundle agent main # Requires smartmontools >= 7.0 (for JSON output support). # Runs on Linux only; silently no-ops on other platforms. # -# Simplified version: reads JSON directly in main bundle, no sub-bundle needed. -# # Attributes exposed in Mission Portal: +# @inventory SMART status - OK, DEGRADED, or SMARTCTL_MISSING # @inventory SMART drive health - Per-drive PASSED/FAILED # @inventory SMART drive model - Drive model per device # @inventory SMART drive temperatures - Current temperature in Celsius @@ -43,6 +42,67 @@ bundle agent main "_id[${_drives}]" string => canonify("${_drives}"); "_cache[${_drives}]" string => "$(_sdir)/inventory_smartctl_${_id[${_drives}]}.json"; + linux._have_smartctl:: + # Rolled-up status: OK or DEGRADED (SMARTCTL_MISSING when smartctl absent) + "smartctl_status" + string => "DEGRADED", + meta => { "inventory", "attribute_name=SMART status" }, + if => some(".*", "failed_drives"); + + "smartctl_status" + string => "OK", + meta => { "inventory", "attribute_name=SMART status" }, + if => not(isvariable("failed_drives")); + + linux.!_have_smartctl:: + "smartctl_status" + string => "SMARTCTL_MISSING", + meta => { "inventory", "attribute_name=SMART status" }; + + linux._have_smartctl:: + # Inventory attributes (visible in Mission Portal) + # Each array element is a separate variable in inventory, avoiding + # the ~4K truncation limit that affects slists on hosts with many drives. + "drive_health[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[health]}", + meta => { "inventory", "attribute_name=SMART drive health" }, + if => isvariable("_d_${_id[${_drives}]}[health]"); + + "drive_model[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[model]}", + meta => { "inventory", "attribute_name=SMART drive model" }, + if => isvariable("_d_${_id[${_drives}]}[model]"); + + "drive_temperatures[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[temp]} C", + meta => { "inventory", "attribute_name=SMART drive temperatures (C)" }, + if => isvariable("_d_${_id[${_drives}]}[temp]"); + + "drive_power_on_hours[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[hours]} h", + meta => { "inventory", "attribute_name=SMART drive power-on hours" }, + if => isvariable("_d_${_id[${_drives}]}[hours]"); + + "nvme_available_spare[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_spare]}%", + meta => { "inventory", "attribute_name=SMART NVMe available spare" }, + if => isvariable("_d_${_id[${_drives}]}[nvme_spare]"); + + "nvme_percentage_used[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_pct_used]}%", + meta => { "inventory", "attribute_name=SMART NVMe percentage used" }, + if => isvariable("_d_${_id[${_drives}]}[nvme_pct_used]"); + + "nvme_media_errors[${_drives}]" + string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_media_errors]}", + meta => { "inventory", "attribute_name=SMART NVMe media errors" }, + if => isvariable("_d_${_id[${_drives}]}[nvme_media_errors]"); + + "failed_drives[${_drives}]" + string => "${_drives}", + meta => { "inventory", "attribute_name=SMART failed drives" }, + if => strcmp("${_d_${_id[${_drives}]}[health]}", "FAILED"); + classes: linux:: "_have_smartctl" expression => isexecutable("$(_smartctl)"); @@ -79,79 +139,6 @@ bundle agent main useresult => "_d_${_id[${_drives}]}", if => fileexists("${_cache[${_drives}]}"); - vars: - linux._have_smartctl:: - # Collect results from sub-bundles into formatted entries - "_health_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[health]}", - if => isvariable("_d_${_id[${_drives}]}[health]"); - - "_model_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[model]}", - if => isvariable("_d_${_id[${_drives}]}[model]"); - - "_temp_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[temp]} C", - if => isvariable("_d_${_id[${_drives}]}[temp]"); - - "_hours_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[hours]} h", - if => isvariable("_d_${_id[${_drives}]}[hours]"); - - "_nvme_spare_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_spare]}%", - if => isvariable("_d_${_id[${_drives}]}[nvme_spare]"); - - "_nvme_pct_used_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_pct_used]}%", - if => isvariable("_d_${_id[${_drives}]}[nvme_pct_used]"); - - "_nvme_media_errors_entries[${_drives}]" - string => "${_drives}: ${_d_${_id[${_drives}]}[nvme_media_errors]}", - if => isvariable("_d_${_id[${_drives}]}[nvme_media_errors]"); - - "_failed_entries[${_drives}]" - string => "${_drives}", - if => strcmp("${_d_${_id[${_drives}]}[health]}", "FAILED"); - - # Inventory attributes (visible in Mission Portal) - "drive_health" - slist => getvalues(_health_entries), - meta => { "inventory", "attribute_name=SMART drive health" }; - - "drive_model" - slist => getvalues(_model_entries), - meta => { "inventory", "attribute_name=SMART drive model" }; - - "drive_temperatures" - slist => getvalues(_temp_entries), - meta => { "inventory", "attribute_name=SMART drive temperatures (C)" }; - - "drive_power_on_hours" - slist => getvalues(_hours_entries), - meta => { "inventory", "attribute_name=SMART drive power-on hours" }; - - "nvme_available_spare" - slist => getvalues(_nvme_spare_entries), - meta => { "inventory", "attribute_name=SMART NVMe available spare" }; - - "nvme_percentage_used" - slist => getvalues(_nvme_pct_used_entries), - meta => { "inventory", "attribute_name=SMART NVMe percentage used" }; - - "nvme_media_errors" - slist => getvalues(_nvme_media_errors_entries), - meta => { "inventory", "attribute_name=SMART NVMe media errors" }; - - "failed_drives" - slist => getvalues(_failed_entries), - meta => { "inventory", "attribute_name=SMART failed drives" }; - - linux.!_have_smartctl:: - "drive_health" - string => "SMARTCTL_MISSING", - meta => { "inventory", "attribute_name=SMART drive health" }; - reports: linux._have_smartctl.verbose_mode:: "inventory_smartctl: monitoring ${_drives}"; @@ -198,13 +185,20 @@ bundle agent parse(drive, cache_file) if => isvariable("_json[nvme_smart_health_information_log][media_errors]"); reports: - "$(_health)" bundle_return_value_index => "health"; - "$(_model)" bundle_return_value_index => "model"; - "$(_temp)" bundle_return_value_index => "temp"; - "$(_hours)" bundle_return_value_index => "hours"; - "$(_nvme_spare)" bundle_return_value_index => "nvme_spare"; - "$(_nvme_pct_used)" bundle_return_value_index => "nvme_pct_used"; - "$(_nvme_media_errors)" bundle_return_value_index => "nvme_media_errors"; + "$(_health)" bundle_return_value_index => "health", + if => isvariable("_health"); + "$(_model)" bundle_return_value_index => "model", + if => isvariable("_model"); + "$(_temp)" bundle_return_value_index => "temp", + if => isvariable("_temp"); + "$(_hours)" bundle_return_value_index => "hours", + if => isvariable("_hours"); + "$(_nvme_spare)" bundle_return_value_index => "nvme_spare", + if => isvariable("_nvme_spare"); + "$(_nvme_pct_used)" bundle_return_value_index => "nvme_pct_used", + if => isvariable("_nvme_pct_used"); + "$(_nvme_media_errors)" bundle_return_value_index => "nvme_media_errors", + if => isvariable("_nvme_media_errors"); } body file control { namespace => "default"; } From b63089ef352738be7e71abb0215f75bc9c9a8b6f Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 4 May 2026 11:15:55 -0500 Subject: [PATCH 097/146] Tidy readme --- inventory/inventory-smartctl/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inventory/inventory-smartctl/README.md b/inventory/inventory-smartctl/README.md index d71007a1..214f8eaf 100644 --- a/inventory/inventory-smartctl/README.md +++ b/inventory/inventory-smartctl/README.md @@ -2,7 +2,8 @@ Inventory module for collecting SMART drive health, temperature, and wear data v ## Description -This module collects S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data from storage devices and exposes it as inventory attributes in CFEngine Mission Portal. It provides a rolled-up status for fleet-wide filtering (`OK`, `DEGRADED`, `SMARTCTL_MISSING`) along with per-drive health, temperature, power-on hours, and NVMe-specific metrics. +This module collects S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data from storage devices and exposes it as inventory attributes in CFEngine Mission Portal. +It provides a rolled-up status for fleet-wide filtering (`OK`, `DEGRADED`, `SMARTCTL_MISSING`) along with per-drive health, temperature, power-on hours, and NVMe-specific metrics. SMART data helps predict drive failures before they occur and provides visibility into storage device health across your infrastructure. From 674e78c004d6ecbddce7ad9a35378ae583124b6f Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 4 May 2026 11:25:36 -0500 Subject: [PATCH 098/146] Bumping commit for build index change From eede7bc4131d3e9a9b232c3bf83998b075c83fbb Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 4 May 2026 11:32:02 -0500 Subject: [PATCH 099/146] Commit to bump build index From 0520999bcacf108a875740aba5282f51cb64ca4e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 6 May 2026 17:03:12 +0200 Subject: [PATCH 100/146] tests: Cleaned up GH Action and added cfengine lint Signed-off-by: Ole Herman Schumacher Elgesem --- .github/workflows/ci.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 348350c2..c770028d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,31 +7,28 @@ on: types: [opened, reopened, synchronize] jobs: - unit_tests: - name: Run Unit Tests + lint_and_test: + name: Lint and test runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - submodules: recursive - - name: Install dependencies - run: | - sudo apt-get update -y && sudo apt-get install -y python3 - pip install flake8 pyright black pyflakes - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Install cfbs - run: pip install cfbs + python-version: "3.14" + - name: Install python tools + run: pipx install flake8 pyright black pyflakes pytest cfbs cfengine - name: Check the status with cfbs run: cfbs status - name: Validate with cfbs run: cfbs validate - name: Check the formatting run: cfbs --check pretty ./cfbs.json - - name: Linting + - name: Linting python code run: ./ci/linting.sh - - name: Install pytest - run: pip install pytest - name: Run promise type tests - run: python3 -m pytest promise-types/ -v + run: pytest promise-types/ -v + - name: Lint policy with CFEngine CLI + run: cfengine lint --strict no ./ From 72e3528b34023ef5dfc4041e53b678ae341b4886 Mon Sep 17 00:00:00 2001 From: Sara Thon Date: Wed, 6 May 2026 13:17:31 +0200 Subject: [PATCH 101/146] CFE-4657: Set up GH Action which tests old python (3.5+) in cfengine/modules Also fix composite action to add pyenv to PATH, since pyenv-action step is skipped on cache hit Ticket: CFE-4657 --- .../actions/set-up-legacy-python/action.yml | 30 +++++++++++++++++++ .github/workflows/python-tests.yml | 26 ++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/actions/set-up-legacy-python/action.yml create mode 100644 .github/workflows/python-tests.yml diff --git a/.github/actions/set-up-legacy-python/action.yml b/.github/actions/set-up-legacy-python/action.yml new file mode 100644 index 00000000..720a49ab --- /dev/null +++ b/.github/actions/set-up-legacy-python/action.yml @@ -0,0 +1,30 @@ +name: "Set up legacy Python" +description: "Sets up a specified Python version in a virtual environment using `pyenv`, installs dependencies, and caches the environment" +inputs: + python-version: + required: true + description: "The Python version to set up" + +runs: + using: "composite" + steps: + - uses: actions/cache@v4 + id: pyenv-cache + with: + path: | + /opt/hostedtoolcache/pyenv_root/2.4.20/x64/versions/${{ inputs.python-version }} + key: ${{ inputs.python-version }}-${{ hashFiles('requirements.txt') }} + - name: Set up Python ${{ inputs.python-version }} using pyenv + uses: gabrielfalcao/pyenv-action@32ef4d2c861170ce17ded56d10329d83f4c8f797 + if: steps.pyenv-cache.outputs.cache-hit != 'true' + with: + default: "${{ inputs.python-version }}" + command: pip install -U pip + - name: Add Python ${{ inputs.python-version }} to PATH + run: echo "/opt/hostedtoolcache/pyenv_root/2.4.20/x64/versions/${{ inputs.python-version }}/bin" >> $GITHUB_PATH + shell: bash + - name: Install dependencies + run: | + python -m pip install flake8 pytest setuptools wheel + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + shell: bash diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..aa3d61e3 --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,26 @@ +name: Python tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test-legacy: + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python-version: ["3.5.10", "3.6.15", "3.7.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + - name: Set up legacy Python ${{ matrix.python-version }} + uses: ./.github/actions/set-up-legacy-python + with: + python-version: ${{ matrix.python-version }} + - name: Test cfengine_module_library is importable + run: python -c "import sys; sys.path.insert(0, 'libraries/python'); import cfengine_module_library; print('OK', sys.version)" From 98fde783791f231773fec22126d595153f1a5796 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 13 May 2026 14:22:20 -0500 Subject: [PATCH 102/146] Add dirtyfrag module for CVE-2026-43284 and CVE-2026-43500 Detects Dirty Frag kernel page-cache write vulnerabilities (xfrm-ESP and RxRPC) and optionally applies mitigation via modprobe.d module blacklisting. --- cfbs.json | 41 +++ security/dirtyfrag/README.md | 138 ++++++++ security/dirtyfrag/dirtyfrag.cf | 402 ++++++++++++++++++++++++ security/dirtyfrag/inventory-status.png | Bin 0 -> 50761 bytes security/dirtyfrag/patched-kernels.json | 66 ++++ 5 files changed, 647 insertions(+) create mode 100644 security/dirtyfrag/README.md create mode 100644 security/dirtyfrag/dirtyfrag.cf create mode 100644 security/dirtyfrag/inventory-status.png create mode 100644 security/dirtyfrag/patched-kernels.json diff --git a/cfbs.json b/cfbs.json index 549f89d1..7d7c609c 100644 --- a/cfbs.json +++ b/cfbs.json @@ -142,6 +142,47 @@ "bundles delete_home_dotshosts:main" ] }, + "dirtyfrag": { + "description": "Detect and optionally mitigate CVE-2026-43284 (DirtyFrag) and CVE-2026-43500 in the Linux kernel.", + "tags": ["security", "inventory", "detection", "mitigation"], + "subdirectory": "security/dirtyfrag", + "steps": [ + "copy dirtyfrag.cf services/cfbs/modules/dirtyfrag/dirtyfrag.cf", + "copy patched-kernels.json services/cfbs/modules/dirtyfrag/patched-kernels.json", + "policy_files services/cfbs/modules/dirtyfrag/dirtyfrag.cf", + "bundles dirtyfrag:main", + "input ./input.json def.json" + ], + "input": [ + { + "type": "string", + "variable": "mitigate_esp", + "namespace": "dirtyfrag", + "bundle": "main", + "label": "Mitigate CVE-2026-43284 (ESP/IPComp)", + "question": "Blacklist esp4, esp6, ipcomp4, ipcomp6 kernel modules? (breaks IPsec) [true/false]", + "default": "false" + }, + { + "type": "string", + "variable": "mitigate_rxrpc", + "namespace": "dirtyfrag", + "bundle": "main", + "label": "Mitigate CVE-2026-43500 (RxRPC)", + "question": "Blacklist rxrpc kernel module? (breaks AFS/RxRPC) [true/false]", + "default": "false" + }, + { + "type": "string", + "variable": "mitigate_userns", + "namespace": "dirtyfrag", + "bundle": "main", + "label": "Mitigate CVE-2026-43284 via user namespaces", + "question": "Set user.max_user_namespaces=0? (blocks ESP exploit without disabling IPsec, may break rootless containers) [true/false]", + "default": "false" + } + ] + }, "demo": { "description": "Enables convenient and insecure settings for demoing CFEngine.", "subdirectory": "management/demo", diff --git a/security/dirtyfrag/README.md b/security/dirtyfrag/README.md new file mode 100644 index 00000000..8d4b3d6b --- /dev/null +++ b/security/dirtyfrag/README.md @@ -0,0 +1,138 @@ +Dirty Frag is a pair of kernel page-cache write vulnerabilities affecting Linux kernel modules that use nonlinear sk_buff (skb) fragments. An unprivileged local attacker with access to a network namespace can trigger out-of-bounds memory writes, potentially leading to privilege escalation. + +- **CVE-2026-43284** (xfrm-ESP/IPComp): Affects `esp4.ko`, `esp6.ko`, `ipcomp.ko`, and `ipcomp6.ko` modules when unprivileged user namespaces are enabled. Patched in stable kernel trees as of May 2026. +- **CVE-2026-43500** (RxRPC): Affects `rxrpc.ko` module. Patches available for some distros as of May 2026; mitigation via module blacklisting where unpatched. + +## Vulnerability conditions + +- **CVE-2026-43284**: Requires `esp4`, `esp6`, `ipcomp`, or `ipcomp6` kernel modules present AND `/proc/sys/kernel/unprivileged_userns_clone` set to `1` +- **CVE-2026-43500**: Requires `rxrpc` kernel module present (no additional prerequisites) + +## Inventory + +After adding this module you can view Dirty Frag vulnerability status in Mission Portal Inventory Report: + +[![Inventory showing Dirty Frag status](https://raw.githubusercontent.com/cfengine/modules/master/security/dirtyfrag/inventory-status.png)](https://raw.githubusercontent.com/cfengine/modules/master/security/dirtyfrag/inventory-status.png) + +- **Dirty Frag CVE-2026-43284 (xfrm-ESP) status**: + - `VULNERABLE (esp4, esp6 loaded)` -- vulnerable modules currently in memory (names vary by host) + - `VULNERABLE (modules on disk, none loaded)` -- modules present but not loaded; latent risk + - `PATCHED (kernel fix applied)` -- running kernel version includes the fix (auto-detected or admin-declared) + - `MITIGATED (blacklist in place)` -- modprobe blacklist or userns restriction active + - `NOT AFFECTED` -- vulnerable modules not present on this host +- **Dirty Frag CVE-2026-43500 (RxRPC) status**: + - `VULNERABLE (rxrpc loaded)` -- module currently in memory + - `VULNERABLE (module on disk, not loaded)` -- module present but not loaded; latent risk + - `PATCHED (kernel fix applied)` -- running kernel version includes the fix (auto-detected or admin-declared) + - `MITIGATED (blacklist in place)` -- modprobe blacklist active + - `NOT AFFECTED` -- rxrpc module not present on this host + +## Mitigation + +Each CVE has an independent toggle and separate conf file: + +**CVE-2026-43284** (ESP/IPComp) -- `/etc/modprobe.d/dirtyfrag-esp.conf`: + +``` +# Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp +install esp4 /bin/false +install esp6 /bin/false +install ipcomp4 /bin/false +install ipcomp6 /bin/false +``` + +**CVE-2026-43500** (RxRPC) -- `/etc/modprobe.d/dirtyfrag-rxrpc.conf`: + +``` +# Dirty Frag CVE-2026-43500 mitigation: block RxRPC +install rxrpc /bin/false +``` + +This prevents the vulnerable modules from loading. When mitigation is first applied, already-loaded modules are unloaded via `rmmod`. + +**CVE-2026-43284 alternative** (user namespaces) -- `/etc/sysctl.d/dirtyfrag-userns.conf`: + +``` +# Dirty Frag CVE-2026-43284 mitigation: disable unprivileged user namespaces +# Blocks ESP/IPComp exploit without disabling IPsec. +# WARNING: May affect rootless containers and sandboxed applications. +user.max_user_namespaces = 0 +``` + +This blocks the ESP/IPComp exploit path without blacklisting the modules, preserving IPsec functionality. Use this instead of `mitigate_esp` on hosts that require IPsec. Note: this does **not** mitigate CVE-2026-43500 (RxRPC) and may break rootless containers (Podman, Docker rootless), Flatpak, and browser sandboxes. Applied via `sysctl --system` on first write. + +All mitigations are **disabled by default** -- the module only reports status unless the corresponding CMDB variable is set to `"true"`. + +## Usage + +Add the policy to your inputs: + +``` +inputs "security/dirtyfrag/dirtyfrag.cf" +``` + +To enable mitigation, set one or both variables in your site's `def.json` (Augments): + +```json +{ + "variables": { + "dirtyfrag:main.mitigate_esp": { "value": "true" }, + "dirtyfrag:main.mitigate_rxrpc": { "value": "true" }, + "dirtyfrag:main.mitigate_userns": { "value": "true" }, + "dirtyfrag:main.esp_patched": { "value": "true" }, + "dirtyfrag:main.rxrpc_patched": { "value": "true" } + } +} +``` + +| Variable | What it does | Trade-off | +|----------|-------------|-----------| +| `mitigate_esp` | Blacklists esp4, esp6, ipcomp4, ipcomp6 | Breaks IPsec | +| `mitigate_rxrpc` | Blacklists rxrpc | Breaks AFS/RxRPC | +| `mitigate_userns` | Sets `user.max_user_namespaces=0` | May break rootless containers/sandboxes | +| `esp_patched` | Declare CVE-2026-43284 as patched | Admin must verify kernel is actually patched | +| `rxrpc_patched` | Declare CVE-2026-43500 as patched | Admin must verify kernel is actually patched | + +Typical combinations: +- **Most hosts**: `mitigate_esp` + `mitigate_rxrpc` (full protection) +- **IPsec hosts**: `mitigate_userns` + `mitigate_rxrpc` (preserves IPsec) +- **Container hosts needing IPsec**: `mitigate_rxrpc` only (partial, accept ESP risk until patched kernel) + +Default behavior (variables unset) is status-only reporting. + +## Detection details + +The module checks for vulnerable modules in three ways: + +1. **On-disk `.ko` files** under `/lib/modules/$(kernel_version)/` +2. **Compressed variants** (`.ko.zst`, `.ko.xz`) on distros that compress modules +3. **Currently loaded modules** via `/sys/module/` entries + +For CVE-2026-43284, the module also checks whether unprivileged user namespaces are enabled (`/proc/sys/kernel/unprivileged_userns_clone`), since the exploit requires namespace access. + +## Kernel patch detection + +The module automatically detects whether the running kernel includes fixes for the Dirty Frag CVEs by comparing the kernel version (`uname -r`) against known-patched versions from distro security advisories. This data is maintained in `patched-kernels.json`, shipped alongside the policy. + +Currently tracked distros: + +| Distro | CVE-2026-43284 | CVE-2026-43500 | +|--------|---------------|---------------| +| RHEL/CentOS/Alma/Rocky 8 | 4.18.0-553.123.2 | 4.18.0-553.123.2 | +| RHEL/CentOS/Alma/Rocky 9 | 5.14.0-611.54.1 | 5.14.0-611.54.3 | +| RHEL/CentOS/Alma/Rocky 10 | 6.12.0-124.55.2 | 6.12.0-124.55.3 | +| Debian 11 (Bullseye) | 5.10.251-4 | 5.10.251-4 | +| Debian 12 (Bookworm) | 6.1.170-3 | 6.1.170-3 | +| Debian 13 (Trixie) | 6.12.86-1 | 6.12.86-1 | +| SLES 15 SP7 | 6.4.0-150700.53.45.1 | 6.4.0-150700.53.45.1 | + +When a patched kernel is detected, the status reports `PATCHED (kernel fix applied)` instead of `VULNERABLE`. The module uses `sort -V` (version sort from coreutils) to compare kernel versions. + +For distros not in the data file, or hosts running custom/backported kernels, set the admin override variables `esp_patched` and/or `rxrpc_patched` to `"true"` via augments. + +To update the patched kernel data, edit `patched-kernels.json` and redeploy. The data file is intentionally separate from the policy so it can be updated independently. + +## Adding exceptions + +To exclude specific hosts from mitigation, use conditional augments to override them to a value other than `"true"`. + diff --git a/security/dirtyfrag/dirtyfrag.cf b/security/dirtyfrag/dirtyfrag.cf new file mode 100644 index 00000000..4795ca94 --- /dev/null +++ b/security/dirtyfrag/dirtyfrag.cf @@ -0,0 +1,402 @@ +body file control +{ + namespace => "dirtyfrag"; +} + +bundle agent kernel_patch_check +# @brief Checks whether the running kernel includes fixes for Dirty Frag CVEs +# by comparing the kernel package version against known-patched versions +# from distro security advisories. +# +# Sets classes: +# dirtyfrag:_esp_kernel_patched +# dirtyfrag:_rxrpc_kernel_patched +# dirtyfrag:_patch_data_matched +{ + vars: + "_data_file" + string => "$(this.promise_dirname)/patched-kernels.json"; + + "_data" + data => readjson("${_data_file}"), + if => fileexists("${_data_file}"); + + "_entries_idx" + slist => getindices("_data[entries]"), + if => isvariable("_data[entries]"); + + "_os_id" + string => "$(default:sys.os_release[ID])", + if => isvariable("default:sys.os_release[ID]"); + + "_os_ver" + string => "$(default:sys.os_release[VERSION_ID])", + if => isvariable("default:sys.os_release[VERSION_ID]"); + + # Find matching entry: the entry whose id_match and version_match + # both match this host's os-release ID and VERSION_ID. + "_matched_idx" + string => "${_entries_idx}", + if => and( + regcmp("${_data[entries][${_entries_idx}][id_match]}", "${_os_id}"), + regcmp("${_data[entries][${_entries_idx}][version_match]}", "${_os_ver}") + ); + + "_esp_patched_ver" + string => "${_data[entries][${_matched_idx}][cve_2026_43284]}", + if => isvariable("_data[entries][${_matched_idx}][cve_2026_43284]"); + + "_rxrpc_patched_ver" + string => "${_data[entries][${_matched_idx}][cve_2026_43500]}", + if => isvariable("_data[entries][${_matched_idx}][cve_2026_43500]"); + + classes: + "_patch_data_matched" + expression => isvariable("_matched_idx"); + + # Use sort -V to check: if the patched version sorts <= running version, + # then the running kernel is patched. We test by checking that the + # patched version comes first (or equal) in version-sorted order. + # printf '%s\n' "$patched" "$running" | sort -V | head -1 + # If result == patched, then running >= patched. + "_esp_kernel_patched" + expression => returnszero( + "/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_esp_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_esp_patched_ver}'", + "useshell" + ), + if => isvariable("_esp_patched_ver"); + + "_rxrpc_kernel_patched" + expression => returnszero( + "/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_rxrpc_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_rxrpc_patched_ver}'", + "useshell" + ), + if => isvariable("_rxrpc_patched_ver"); + + reports: + inform_mode._patch_data_matched:: + "Dirty Frag: matched distro ${_os_id} ${_os_ver} (entry ${_matched_idx})"; + + inform_mode._esp_kernel_patched:: + "Dirty Frag CVE-2026-43284: kernel $(default:sys.release) >= ${_esp_patched_ver} (PATCHED)"; + + inform_mode._rxrpc_kernel_patched:: + "Dirty Frag CVE-2026-43500: kernel $(default:sys.release) >= ${_rxrpc_patched_ver} (PATCHED)"; +} + +bundle agent main +# @brief Detects Dirty Frag (CVE-2026-43284, CVE-2026-43500) vulnerability +# and applies CMDB-toggleable mitigations via module blacklisting. +# +# Usage: +# inputs "security/dirtyfrag/dirtyfrag.cf" +# +# CMDB variables (set via def.json augments): +# { +# "variables": { +# "dirtyfrag:main.mitigate_esp": { "value": "true" }, +# "dirtyfrag:main.mitigate_rxrpc": { "value": "true" }, +# "dirtyfrag:main.mitigate_userns": { "value": "true" }, +# "dirtyfrag:main.esp_patched": { "value": "true" }, +# "dirtyfrag:main.rxrpc_patched": { "value": "true" } +# } +# } +# +# Mitigation toggles (each independently controls a mitigation strategy): +# mitigate_esp -> blacklists esp4, esp6, ipcomp4, ipcomp6 +# mitigate_rxrpc -> blacklists rxrpc +# mitigate_userns -> sets user.max_user_namespaces=0 via sysctl +# (blocks CVE-2026-43284 without disabling IPsec, +# but may affect rootless containers/sandboxes; +# does NOT mitigate CVE-2026-43500) +# +# Admin patch overrides (suppress false positives on patched kernels): +# esp_patched -> declare CVE-2026-43284 as patched on this host +# rxrpc_patched -> declare CVE-2026-43500 as patched on this host +# +# Kernel patch detection is automatic for known distro versions +# (see patched-kernels.json). The admin overrides are for distros +# not yet in the data file, or custom/backported kernels. +# +# When disabled (default), the module only reports vulnerability status. +{ + methods: + # Run kernel patch check before evaluating status + "kernel_patch_check" usebundle => "dirtyfrag:kernel_patch_check"; + + vars: + # --- Constants --- + "_esp_conf_path" string => "/etc/modprobe.d/dirtyfrag-esp.conf"; + "_rxrpc_conf_path" string => "/etc/modprobe.d/dirtyfrag-rxrpc.conf"; + + "_esp_conf_content" + string => concat( + "# Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp$(const.n)", + "install esp4 /bin/false$(const.n)", + "install esp6 /bin/false$(const.n)", + "install ipcomp4 /bin/false$(const.n)", + "install ipcomp6 /bin/false$(const.n)" + ); + + "_rxrpc_conf_content" + string => concat( + "# Dirty Frag CVE-2026-43500 mitigation: block RxRPC$(const.n)", + "install rxrpc /bin/false$(const.n)" + ); + + "_userns_conf_path" string => "/etc/sysctl.d/dirtyfrag-userns.conf"; + + "_userns_conf_content" + string => concat( + "# Dirty Frag CVE-2026-43284 mitigation: disable unprivileged user namespaces$(const.n)", + "# Blocks ESP/IPComp exploit without disabling IPsec.$(const.n)", + "# WARNING: May affect rootless containers and sandboxed applications.$(const.n)", + "user.max_user_namespaces = 0$(const.n)" + ); + + # --- Read kernel version --- + "_kver" string => "$(default:sys.release)"; + + # --- Module paths --- + "_esp4_path" string => "/lib/modules/$(_kver)/kernel/net/ipv4/esp4.ko"; + "_esp6_path" string => "/lib/modules/$(_kver)/kernel/net/ipv6/esp6.ko"; + "_ipcomp4_path" string => "/lib/modules/$(_kver)/kernel/net/ipv4/ipcomp.ko"; + + "_ipcomp6_path" + string => "/lib/modules/$(_kver)/kernel/net/ipv6/ipcomp6.ko"; + + "_rxrpc_path" string => "/lib/modules/$(_kver)/kernel/net/rxrpc/rxrpc.ko"; + "_ns_proc" string => "/proc/sys/kernel/unprivileged_userns_clone"; + + # --- Read the unprivileged user namespace setting --- + "_ns_val" + string => readfile("${_ns_proc}"), + if => fileexists("${_ns_proc}"); + + # --- Loaded-module detail for inventory --- + "_esp_loaded_csv" + string => format( + "%s%s%s%s", + ifelse("_esp4_loaded", "esp4, ", ""), + ifelse("_esp6_loaded", "esp6, ", ""), + ifelse("_ipcomp4_loaded", "ipcomp, ", ""), + ifelse("_ipcomp6_loaded", "ipcomp6, ", "") + ); + + # Trim trailing ", " + "_esp_loaded_names" + string => regex_replace("${_esp_loaded_csv}", ",\s*$", "", ""); + + # --- Status strings --- + "_esp_status" + string => ifelse( + "dirtyfrag_esp_needs_mitigation._esp_any_loaded", + "VULNERABLE (${_esp_loaded_names} loaded)", + "dirtyfrag_esp_needs_mitigation", + "VULNERABLE (modules on disk, none loaded)", + "dirtyfrag:kernel_patch_check._esp_kernel_patched|_esp_admin_patched", + "PATCHED (kernel fix applied)", + "dirtyfrag_esp_present._esp_mitigated", + "MITIGATED (blacklist in place)", + "NOT AFFECTED" + ); + + "_rxrpc_status" + string => ifelse( + "dirtyfrag_rxrpc_needs_mitigation._rxrpc_loaded", + "VULNERABLE (rxrpc loaded)", + "dirtyfrag_rxrpc_needs_mitigation", + "VULNERABLE (module on disk, not loaded)", + "dirtyfrag:kernel_patch_check._rxrpc_kernel_patched|_rxrpc_admin_patched", + "PATCHED (kernel fix applied)", + "dirtyfrag_rxrpc_present._rxrpc_mitigated", + "MITIGATED (blacklist in place)", + "NOT AFFECTED" + ); + + # --- Inventory output for Mission Portal --- + "inventory_dirtyfrag_esp" + string => "CVE-2026-43284 (xfrm-ESP): $(_esp_status)", + meta => { + "inventory", + "attribute_name=Dirty Frag CVE-2026-43284 (xfrm-ESP) status", + }, + comment => "CVE-2026-43284 xfrm-ESP mitigation status"; + + "inventory_dirtyfrag_rxrpc" + string => "CVE-2026-43500 (RxRPC): $(_rxrpc_status)", + meta => { + "inventory", + "attribute_name=Dirty Frag CVE-2026-43500 (RxRPC) status", + }, + comment => "CVE-2026-43500 RxRPC mitigation status"; + + classes: + # --- CMDB toggles --- + "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); + "_mitigate_rxrpc" expression => strcmp("true", "$(mitigate_rxrpc)"); + "_mitigate_userns" expression => strcmp("true", "$(mitigate_userns)"); + + # --- Admin override: manually declare host as patched --- + "_esp_admin_patched" expression => strcmp("true", "$(esp_patched)"); + "_rxrpc_admin_patched" expression => strcmp("true", "$(rxrpc_patched)"); + + # --- Unprivileged user namespace enabled? --- + "_ns_proc_file_exists" expression => fileexists("${_ns_proc}"); + "_ns_val_is_1" expression => strcmp("${_ns_val}", "1"); + "_userns_enabled" and => { "_ns_proc_file_exists", "_ns_val_is_1" }; + + # --- xfrm-ESP/IPComp modules present? --- + "_esp4_on_disk" expression => fileexists("${_esp4_path}"); + "_esp6_on_disk" expression => fileexists("${_esp6_path}"); + "_ipcomp4_on_disk" expression => fileexists("${_ipcomp4_path}"); + "_ipcomp6_on_disk" expression => fileexists("${_ipcomp6_path}"); + + # Compressed variants (.ko.zst, .ko.xz) + "_esp4_on_disk_z" expression => fileexists("${_esp4_path}.zst"); + "_esp6_on_disk_z" expression => fileexists("${_esp6_path}.zst"); + "_ipcomp4_on_disk_z" expression => fileexists("${_ipcomp4_path}.zst"); + "_ipcomp6_on_disk_z" expression => fileexists("${_ipcomp6_path}.zst"); + "_esp4_on_disk_xz" expression => fileexists("${_esp4_path}.xz"); + "_esp6_on_disk_xz" expression => fileexists("${_esp6_path}.xz"); + "_ipcomp4_on_disk_xz" expression => fileexists("${_ipcomp4_path}.xz"); + "_ipcomp6_on_disk_xz" expression => fileexists("${_ipcomp6_path}.xz"); + + # Currently loaded + "_esp4_loaded" expression => isdir("/sys/module/esp4"); + "_esp6_loaded" expression => isdir("/sys/module/esp6"); + "_ipcomp4_loaded" expression => isdir("/sys/module/ipcomp"); + "_ipcomp6_loaded" expression => isdir("/sys/module/ipcomp6"); + + "_esp_any_loaded" + or => { + "_esp4_loaded", "_esp6_loaded", "_ipcomp4_loaded", "_ipcomp6_loaded" + }; + + "dirtyfrag_esp_present" + or => { + "_esp4_on_disk", + "_esp6_on_disk", + "_ipcomp4_on_disk", + "_ipcomp6_on_disk", + "_esp4_on_disk_z", + "_esp6_on_disk_z", + "_ipcomp4_on_disk_z", + "_ipcomp6_on_disk_z", + "_esp4_on_disk_xz", + "_esp6_on_disk_xz", + "_ipcomp4_on_disk_xz", + "_ipcomp6_on_disk_xz", + "_esp4_loaded", + "_esp6_loaded", + "_ipcomp4_loaded", + "_ipcomp6_loaded", + }; + + # --- RxRPC module present? --- + "_rxrpc_on_disk" expression => fileexists("${_rxrpc_path}"); + "_rxrpc_on_disk_z" expression => fileexists("${_rxrpc_path}.zst"); + "_rxrpc_on_disk_xz" expression => fileexists("${_rxrpc_path}.xz"); + "_rxrpc_loaded" expression => isdir("/sys/module/rxrpc"); + + "dirtyfrag_rxrpc_present" + or => { + "_rxrpc_on_disk", + "_rxrpc_on_disk_z", + "_rxrpc_on_disk_xz", + "_rxrpc_loaded", + }; + + # --- Mitigation conf files in place? --- + "_esp_conf_exists" expression => fileexists("${_esp_conf_path}"); + "_rxrpc_conf_exists" expression => fileexists("${_rxrpc_conf_path}"); + "_userns_conf_exists" expression => fileexists("${_userns_conf_path}"); + + # ESP is mitigated by modprobe blacklist, disabled userns, patched kernel, + # or admin override + "_esp_mitigated" + or => { + "_esp_conf_exists", + "_userns_conf_exists", + "!_userns_enabled", + "dirtyfrag:kernel_patch_check._esp_kernel_patched", + "_esp_admin_patched", + }; + + # RxRPC is mitigated by the modprobe blacklist, patched kernel, + # or admin override + "_rxrpc_mitigated" + or => { + "_rxrpc_conf_exists", + "dirtyfrag:kernel_patch_check._rxrpc_kernel_patched", + "_rxrpc_admin_patched", + }; + + # --- Per-CVE vulnerability checks --- + "dirtyfrag_esp_needs_mitigation" + and => { "dirtyfrag_esp_present", "!_esp_mitigated" }; + + "dirtyfrag_rxrpc_needs_mitigation" + and => { "dirtyfrag_rxrpc_present", "!_rxrpc_mitigated" }; + + files: + # --- CVE-2026-43284: ESP/IPComp mitigation --- + _mitigate_esp:: + "${_esp_conf_path}" + create => "true", + content => "${_esp_conf_content}", + classes => default:results("bundle", "dirtyfrag_esp_conf"); + + # --- CVE-2026-43500: RxRPC mitigation --- + _mitigate_rxrpc:: + "${_rxrpc_conf_path}" + create => "true", + content => "${_rxrpc_conf_content}", + classes => default:results("bundle", "dirtyfrag_rxrpc_conf"); + + # --- CVE-2026-43284 alternative: disable unprivileged user namespaces --- + _mitigate_userns:: + "${_userns_conf_path}" + create => "true", + content => "${_userns_conf_content}", + classes => default:results("bundle", "dirtyfrag_userns_conf"); + + commands: + # --- Unload ESP/IPComp modules after conf written --- + _mitigate_esp.dirtyfrag_esp_conf_repaired:: + "/sbin/rmmod" + arglist => { "esp4", "esp6", "ipcomp", "ipcomp6" }, + comment => "Unload ESP/IPComp modules already in memory"; + + # --- Unload RxRPC module after conf written --- + _mitigate_rxrpc.dirtyfrag_rxrpc_conf_repaired:: + "/sbin/rmmod" + arglist => { "rxrpc" }, + comment => "Unload RxRPC module already in memory"; + + # --- Apply sysctl after writing userns conf --- + _mitigate_userns.dirtyfrag_userns_conf_repaired:: + "/sbin/sysctl" + arglist => { "--system" }, + comment => "Apply user.max_user_namespaces=0 without reboot"; + + reports: + inform_mode:: + + "Dirty Frag CVE-2026-43284 (xfrm-ESP/IPComp): $(_esp_status)" + if => "dirtyfrag_esp_present"; + + "Dirty Frag CVE-2026-43500 (RxRPC): $(_rxrpc_status)" + if => "dirtyfrag_rxrpc_present"; +} + +body file control +{ + namespace => "default"; +} + +bundle agent __main__ +{ + methods: + "dirtyfrag:main"; +} diff --git a/security/dirtyfrag/inventory-status.png b/security/dirtyfrag/inventory-status.png new file mode 100644 index 0000000000000000000000000000000000000000..4adcd0b7c3421558ea1dfd480ddd9cdf079912a8 GIT binary patch literal 50761 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 Date: Mon, 18 May 2026 13:41:49 -0500 Subject: [PATCH 103/146] Fixups after review Addresses Roberto and Craigs reviews and a cross-distro hardening pass (centos7/rocky8/9, alma10, debian12/13, ubuntu24). - ipcomp4 -> ipcomp - namespace references in kernel_patch_check; widen class scope - version_match patterns - sourced kernel version from package metadata on Debian/Ubuntu - Dropped /usr/bin/ paths and useshell pipelines where CFEngine primitives suffice (string_split, sort -V -C, packagesmatching) - Collapsed module-file presence checks into one findfiles per CVE - Collapse kernel_patch_check into bundle main; drop _kver alias - Use packagesmatching() instead of a dpkg-query subprocess - New status: MITIGATED (userns disabled) when user.max_user_namespaces=0 - Composite classes (dirtyfrag_vulnerable, dirtyfrag_esp_mitigated, dirtyfrag_rxrpc_mitigated) tagged meta=>{"report"} and namespace- scoped, so cf-hub collects them for reports without cluttering the default Inventory column set - CMDB toggle variables default-disabled with explicit opt-in --- cfbs.json | 2 +- security/dirtyfrag/README.md | 60 ++- security/dirtyfrag/dirtyfrag.cf | 516 +++++++++++++----------- security/dirtyfrag/inventory-status.png | Bin 50761 -> 127073 bytes security/dirtyfrag/patched-kernels.json | 49 ++- 5 files changed, 368 insertions(+), 259 deletions(-) diff --git a/cfbs.json b/cfbs.json index 7d7c609c..c225a1d3 100644 --- a/cfbs.json +++ b/cfbs.json @@ -160,7 +160,7 @@ "namespace": "dirtyfrag", "bundle": "main", "label": "Mitigate CVE-2026-43284 (ESP/IPComp)", - "question": "Blacklist esp4, esp6, ipcomp4, ipcomp6 kernel modules? (breaks IPsec) [true/false]", + "question": "Blacklist esp4, esp6, ipcomp, ipcomp6 kernel modules? (breaks IPsec) [true/false]", "default": "false" }, { diff --git a/security/dirtyfrag/README.md b/security/dirtyfrag/README.md index 8d4b3d6b..5c529a7c 100644 --- a/security/dirtyfrag/README.md +++ b/security/dirtyfrag/README.md @@ -18,7 +18,8 @@ After adding this module you can view Dirty Frag vulnerability status in Mission - `VULNERABLE (esp4, esp6 loaded)` -- vulnerable modules currently in memory (names vary by host) - `VULNERABLE (modules on disk, none loaded)` -- modules present but not loaded; latent risk - `PATCHED (kernel fix applied)` -- running kernel version includes the fix (auto-detected or admin-declared) - - `MITIGATED (blacklist in place)` -- modprobe blacklist or userns restriction active + - `MITIGATED (blacklist in place)` -- modprobe blacklist active for esp4/esp6/ipcomp/ipcomp6 + - `MITIGATED (userns disabled)` -- `user.max_user_namespaces=0` sysctl active, blocking the exploit path without unloading IPsec - `NOT AFFECTED` -- vulnerable modules not present on this host - **Dirty Frag CVE-2026-43500 (RxRPC) status**: - `VULNERABLE (rxrpc loaded)` -- module currently in memory @@ -37,7 +38,7 @@ Each CVE has an independent toggle and separate conf file: # Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp install esp4 /bin/false install esp6 /bin/false -install ipcomp4 /bin/false +install ipcomp /bin/false install ipcomp6 /bin/false ``` @@ -61,7 +62,7 @@ user.max_user_namespaces = 0 This blocks the ESP/IPComp exploit path without blacklisting the modules, preserving IPsec functionality. Use this instead of `mitigate_esp` on hosts that require IPsec. Note: this does **not** mitigate CVE-2026-43500 (RxRPC) and may break rootless containers (Podman, Docker rootless), Flatpak, and browser sandboxes. Applied via `sysctl --system` on first write. -All mitigations are **disabled by default** -- the module only reports status unless the corresponding CMDB variable is set to `"true"`. +All mitigations are **disabled by default** -- the module only reports status unless the corresponding CMDB variable (`dirtyfrag:main.mitigate_esp`, `dirtyfrag:main.mitigate_rxrpc`, or `dirtyfrag:main.mitigate_userns`) is set to `"true"`. See the table below for the full set of toggles. ## Usage @@ -87,7 +88,7 @@ To enable mitigation, set one or both variables in your site's `def.json` (Augme | Variable | What it does | Trade-off | |----------|-------------|-----------| -| `mitigate_esp` | Blacklists esp4, esp6, ipcomp4, ipcomp6 | Breaks IPsec | +| `mitigate_esp` | Blacklists esp4, esp6, ipcomp, ipcomp6 | Breaks IPsec | | `mitigate_rxrpc` | Blacklists rxrpc | Breaks AFS/RxRPC | | `mitigate_userns` | Sets `user.max_user_namespaces=0` | May break rootless containers/sandboxes | | `esp_patched` | Declare CVE-2026-43284 as patched | Admin must verify kernel is actually patched | @@ -136,3 +137,54 @@ To update the patched kernel data, edit `patched-kernels.json` and redeploy. The To exclude specific hosts from mitigation, use conditional augments to override them to a value other than `"true"`. +## Mission Portal — operator reference + +The module is structured to give Mission Portal operators four things: inventory columns for at-a-glance state, a filterable mitigation-method enum, queryable classes for targeting, and CVE-linked promise stakeholders for audit traceability. + +### Inventory columns + +| Attribute name | Values | Filterable as | +|---|---|---| +| `Dirty Frag CVE-2026-43284 (xfrm-ESP) status` | `VULNERABLE (...)`, `PATCHED (kernel fix applied)`, `MITIGATED (blacklist in place)`, `MITIGATED (userns disabled)`, `NOT AFFECTED` | starts-with regex (`^VULNERABLE`, `^PATCHED`, `^MITIGATED`) | +| `Dirty Frag CVE-2026-43500 (RxRPC) status` | same shape, no userns option | same | +| `Dirty Frag CVE-2026-43284 mitigation method` | `kernel-patch`, `modprobe`, `userns`, `admin-override`, `none`, `not-applicable` | exact match | +| `Dirty Frag CVE-2026-43500 mitigation method` | `kernel-patch`, `modprobe`, `admin-override`, `none`, `not-applicable` | exact match | + +The detailed status strings are for humans; the method enums are for dashboards and filters. + +### Queryable classes (collected as `report`, not in default inventory columns) + +| Class | Meaning | Use case | +|---|---|---| +| `dirtyfrag_vulnerable` | Any tracked CVE is unmitigated on this host | Targeting: "patch these now" | +| `dirtyfrag_esp_mitigated` | ESP mitigation path is active (any of blacklist / userns / patched) | Audit: confirm coverage | +| `dirtyfrag_rxrpc_mitigated` | RxRPC mitigation path is active | Same | +| `dirtyfrag_esp_needs_mitigation` | ESP exposure exists and no mitigation in place | Targeting fragments | +| `dirtyfrag_rxrpc_needs_mitigation` | Same for RxRPC | Same | +| `dirtyfrag_esp_present`, `dirtyfrag_rxrpc_present` | Module is loadable or loaded | Scoping | + +Tagged with `meta => { "report" }` so cf-hub collects them for queries but they don't add columns to the default inventory view. + +### Recommended alerts + +Configure in Mission Portal → **Alerts**. The module ships no alerts itself; operators wire conditions appropriate to their fleet's risk tolerance: + +- **Inventory condition** — alert when `Dirty Frag CVE-2026-43284 (xfrm-ESP) status` matches regex `^VULNERABLE`. Catches new unmitigated hosts as they join the fleet or as kernels regress. +- **Inventory condition** — alert when `Dirty Frag CVE-2026-43284 mitigation method` equals `userns` on more than N hosts. The userns path is fragile (breaks rootless containers); knowing how many hosts depend on it informs upgrade prioritization. +- **Policy condition** — alert on any **promises not kept** for handles `dirtyfrag_esp_modprobe_blacklist`, `dirtyfrag_rxrpc_modprobe_blacklist`, `dirtyfrag_esp_modprobe_unload`, `dirtyfrag_rxrpc_modprobe_unload`, `dirtyfrag_userns_sysctl_conf`, `dirtyfrag_userns_sysctl_reapply`. These are the file/command promises that enforce mitigation; a failure means the conf wasn't written or the module wasn't unloaded. + +### Promise handles (for report filtering) + +| Handle | What | When it fails | +|---|---|---| +| `dirtyfrag_esp_modprobe_blacklist` | Writes `/etc/modprobe.d/dirtyfrag-esp.conf` | Filesystem error, conflicting writes | +| `dirtyfrag_rxrpc_modprobe_blacklist` | Writes `/etc/modprobe.d/dirtyfrag-rxrpc.conf` | Same | +| `dirtyfrag_userns_sysctl_conf` | Writes `/etc/sysctl.d/dirtyfrag-userns.conf` | Same | +| `dirtyfrag_esp_modprobe_unload` | `modprobe -r` on currently-loaded ESP/IPComp modules | Module busy (existing IPsec tunnel) | +| `dirtyfrag_rxrpc_modprobe_unload` | `modprobe -r rxrpc` | Module busy | +| `dirtyfrag_userns_sysctl_reapply` | `sysctl --system` to load the userns conf | sysctl conf rejected | + +### Compliance traceability + +Every file and command promise carries a **promisee arrow** linking to its CVE: `"${path}" -> { "CVE-2026-43284" }` and `"${path}" -> { "CVE-2026-43500" }`. In Mission Portal's promise detail view, this surfaces as the stakeholder / linked identifier. Searching the audit history for `CVE-2026-43284` returns every policy artifact addressing it. + diff --git a/security/dirtyfrag/dirtyfrag.cf b/security/dirtyfrag/dirtyfrag.cf index 4795ca94..80bbdf14 100644 --- a/security/dirtyfrag/dirtyfrag.cf +++ b/security/dirtyfrag/dirtyfrag.cf @@ -3,17 +3,24 @@ body file control namespace => "dirtyfrag"; } -bundle agent kernel_patch_check -# @brief Checks whether the running kernel includes fixes for Dirty Frag CVEs -# by comparing the kernel package version against known-patched versions -# from distro security advisories. +bundle agent main +# @brief Detects Dirty Frag (CVE-2026-43284, CVE-2026-43500) vulnerability +# and applies CMDB-toggleable mitigations. CMDB variables, mitigation +# trade-offs and inventory states are documented in README.md. # -# Sets classes: -# dirtyfrag:_esp_kernel_patched -# dirtyfrag:_rxrpc_kernel_patched -# dirtyfrag:_patch_data_matched +# @inventory Dirty Frag CVE-2026-43284 (xfrm-ESP) status - Vulnerability/mitigation state for the xfrm-ESP/IPComp CVE. +# @inventory Dirty Frag CVE-2026-43500 (RxRPC) status - Vulnerability/mitigation state for the RxRPC CVE. +# @inventory Dirty Frag CVE-2026-43284 mitigation method - Which mitigation strategy is currently active for ESP (kernel-patch, modprobe, userns, admin-override, none, not-applicable). +# @inventory Dirty Frag CVE-2026-43500 mitigation method - Which mitigation strategy is currently active for RxRPC (kernel-patch, modprobe, admin-override, none, not-applicable). +# @class dirtyfrag_vulnerable - true if any tracked CVE is unmitigated on this host (queryable; not in default inventory column set). +# @class dirtyfrag_esp_mitigated - true if any ESP mitigation path is active on this host. +# @class dirtyfrag_rxrpc_mitigated - true if any RxRPC mitigation path is active on this host. { vars: + # --- Patched-kernel data lookup --------------------------------- + # Minimum patched kernel package versions per distro live in + # patched-kernels.json next to this policy. The data file is + # walked here; comparison happens in the classes:: block below. "_data_file" string => "$(this.promise_dirname)/patched-kernels.json"; @@ -21,9 +28,10 @@ bundle agent kernel_patch_check data => readjson("${_data_file}"), if => fileexists("${_data_file}"); + # Indexing goes inside ${...}; see lessons-learned/data-indexing.md. "_entries_idx" slist => getindices("_data[entries]"), - if => isvariable("_data[entries]"); + if => isvariable("_data"); "_os_id" string => "$(default:sys.os_release[ID])", @@ -44,97 +52,44 @@ bundle agent kernel_patch_check "_esp_patched_ver" string => "${_data[entries][${_matched_idx}][cve_2026_43284]}", - if => isvariable("_data[entries][${_matched_idx}][cve_2026_43284]"); + if => isvariable("_matched_idx"); "_rxrpc_patched_ver" string => "${_data[entries][${_matched_idx}][cve_2026_43500]}", - if => isvariable("_data[entries][${_matched_idx}][cve_2026_43500]"); - - classes: - "_patch_data_matched" - expression => isvariable("_matched_idx"); - - # Use sort -V to check: if the patched version sorts <= running version, - # then the running kernel is patched. We test by checking that the - # patched version comes first (or equal) in version-sorted order. - # printf '%s\n' "$patched" "$running" | sort -V | head -1 - # If result == patched, then running >= patched. - "_esp_kernel_patched" - expression => returnszero( - "/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_esp_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_esp_patched_ver}'", - "useshell" - ), - if => isvariable("_esp_patched_ver"); - - "_rxrpc_kernel_patched" - expression => returnszero( - "/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_rxrpc_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_rxrpc_patched_ver}'", - "useshell" + if => isvariable("_matched_idx"); + + # --- Installed kernel package version -------------------------- + # RHEL family: sys.release is the package NEVR -- compare directly. + # Debian/Ubuntu: sys.release is an ABI version; the package version + # lives in package metadata. packagesmatching() reads the + # package_inventory cache populated by the standard masterfiles + # apt_get inventory. See lessons-learned/packagesmatching.md. + # Other: fall back to sys.release as best-effort. + "_kernel_pkgs" + data => packagesmatching( + "linux-image-$(default:sys.release)", ".*", ".*", "apt_get" ), - if => isvariable("_rxrpc_patched_ver"); + if => regcmp("debian|ubuntu", "$(default:sys.os_release[ID])"); - reports: - inform_mode._patch_data_matched:: - "Dirty Frag: matched distro ${_os_id} ${_os_ver} (entry ${_matched_idx})"; - - inform_mode._esp_kernel_patched:: - "Dirty Frag CVE-2026-43284: kernel $(default:sys.release) >= ${_esp_patched_ver} (PATCHED)"; - - inform_mode._rxrpc_kernel_patched:: - "Dirty Frag CVE-2026-43500: kernel $(default:sys.release) >= ${_rxrpc_patched_ver} (PATCHED)"; -} + "_kernel_pkg_ver" + string => "$(_kernel_pkgs[0][version])", + if => isvariable("_kernel_pkgs[0][version]"); -bundle agent main -# @brief Detects Dirty Frag (CVE-2026-43284, CVE-2026-43500) vulnerability -# and applies CMDB-toggleable mitigations via module blacklisting. -# -# Usage: -# inputs "security/dirtyfrag/dirtyfrag.cf" -# -# CMDB variables (set via def.json augments): -# { -# "variables": { -# "dirtyfrag:main.mitigate_esp": { "value": "true" }, -# "dirtyfrag:main.mitigate_rxrpc": { "value": "true" }, -# "dirtyfrag:main.mitigate_userns": { "value": "true" }, -# "dirtyfrag:main.esp_patched": { "value": "true" }, -# "dirtyfrag:main.rxrpc_patched": { "value": "true" } -# } -# } -# -# Mitigation toggles (each independently controls a mitigation strategy): -# mitigate_esp -> blacklists esp4, esp6, ipcomp4, ipcomp6 -# mitigate_rxrpc -> blacklists rxrpc -# mitigate_userns -> sets user.max_user_namespaces=0 via sysctl -# (blocks CVE-2026-43284 without disabling IPsec, -# but may affect rootless containers/sandboxes; -# does NOT mitigate CVE-2026-43500) -# -# Admin patch overrides (suppress false positives on patched kernels): -# esp_patched -> declare CVE-2026-43284 as patched on this host -# rxrpc_patched -> declare CVE-2026-43500 as patched on this host -# -# Kernel patch detection is automatic for known distro versions -# (see patched-kernels.json). The admin overrides are for distros -# not yet in the data file, or custom/backported kernels. -# -# When disabled (default), the module only reports vulnerability status. -{ - methods: - # Run kernel patch check before evaluating status - "kernel_patch_check" usebundle => "dirtyfrag:kernel_patch_check"; + "_kernel_pkg_ver" + string => "$(default:sys.release)", + if => not(regcmp("debian|ubuntu", "$(default:sys.os_release[ID])")); - vars: - # --- Constants --- - "_esp_conf_path" string => "/etc/modprobe.d/dirtyfrag-esp.conf"; - "_rxrpc_conf_path" string => "/etc/modprobe.d/dirtyfrag-rxrpc.conf"; + # --- Mitigation-conf paths and contents ----------------------- + "_esp_conf_path" string => "/etc/modprobe.d/dirtyfrag-esp.conf"; + "_rxrpc_conf_path" string => "/etc/modprobe.d/dirtyfrag-rxrpc.conf"; + "_userns_conf_path" string => "/etc/sysctl.d/dirtyfrag-userns.conf"; "_esp_conf_content" string => concat( "# Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp$(const.n)", "install esp4 /bin/false$(const.n)", "install esp6 /bin/false$(const.n)", - "install ipcomp4 /bin/false$(const.n)", + "install ipcomp /bin/false$(const.n)", "install ipcomp6 /bin/false$(const.n)" ); @@ -144,8 +99,6 @@ bundle agent main "install rxrpc /bin/false$(const.n)" ); - "_userns_conf_path" string => "/etc/sysctl.d/dirtyfrag-userns.conf"; - "_userns_conf_content" string => concat( "# Dirty Frag CVE-2026-43284 mitigation: disable unprivileged user namespaces$(const.n)", @@ -154,50 +107,57 @@ bundle agent main "user.max_user_namespaces = 0$(const.n)" ); - # --- Read kernel version --- - "_kver" string => "$(default:sys.release)"; - - # --- Module paths --- - "_esp4_path" string => "/lib/modules/$(_kver)/kernel/net/ipv4/esp4.ko"; - "_esp6_path" string => "/lib/modules/$(_kver)/kernel/net/ipv6/esp6.ko"; - "_ipcomp4_path" string => "/lib/modules/$(_kver)/kernel/net/ipv4/ipcomp.ko"; - - "_ipcomp6_path" - string => "/lib/modules/$(_kver)/kernel/net/ipv6/ipcomp6.ko"; - - "_rxrpc_path" string => "/lib/modules/$(_kver)/kernel/net/rxrpc/rxrpc.ko"; - "_ns_proc" string => "/proc/sys/kernel/unprivileged_userns_clone"; - - # --- Read the unprivileged user namespace setting --- - "_ns_val" - string => readfile("${_ns_proc}"), - if => fileexists("${_ns_proc}"); - - # --- Loaded-module detail for inventory --- - "_esp_loaded_csv" - string => format( - "%s%s%s%s", - ifelse("_esp4_loaded", "esp4, ", ""), - ifelse("_esp6_loaded", "esp6, ", ""), - ifelse("_ipcomp4_loaded", "ipcomp, ", ""), - ifelse("_ipcomp6_loaded", "ipcomp6, ", "") + # --- Module files on disk ------------------------------------- + # Single findfiles glob covers .ko, .ko.xz, .ko.zst and any future + # compression scheme. Empty list means the modules aren't shipped + # for this kernel build. + "_esp_module_files" + slist => findfiles( + "/lib/modules/$(default:sys.release)/kernel/net/ipv4/esp4.ko*", + "/lib/modules/$(default:sys.release)/kernel/net/ipv6/esp6.ko*", + "/lib/modules/$(default:sys.release)/kernel/net/ipv4/ipcomp.ko*", + "/lib/modules/$(default:sys.release)/kernel/net/ipv6/ipcomp6.ko*" + ); + + "_rxrpc_module_files" + slist => findfiles( + "/lib/modules/$(default:sys.release)/kernel/net/rxrpc/rxrpc.ko*" ); - # Trim trailing ", " + # --- Unprivileged user namespace setting ---------------------- + # sysctlvalue works across RHEL/SLES/Debian regardless of the + # /proc/sys/kernel/unprivileged_userns_clone vs user.max_user_namespaces + # path differences. + "_max_userns" string => sysctlvalue("user.max_user_namespaces"); + + # --- Loaded-module names for the VULNERABLE status string ----- + # Each slot is the module name when loaded, "" otherwise. + # filter(".+", ...) drops the empties; join glues with ", ". + "_esp_loaded_list" + slist => { + ifelse("_esp4_loaded", "esp4", ""), + ifelse("_esp6_loaded", "esp6", ""), + ifelse("_ipcomp_loaded", "ipcomp", ""), + ifelse("_ipcomp6_loaded", "ipcomp6", ""), + }; + "_esp_loaded_names" - string => regex_replace("${_esp_loaded_csv}", ",\s*$", "", ""); + string => join(", ", + filter(".+", "_esp_loaded_list", "false", "false", "10")); - # --- Status strings --- + # --- Status strings ------------------------------------------- "_esp_status" string => ifelse( "dirtyfrag_esp_needs_mitigation._esp_any_loaded", - "VULNERABLE (${_esp_loaded_names} loaded)", + "VULNERABLE ($(_esp_loaded_names) loaded)", "dirtyfrag_esp_needs_mitigation", "VULNERABLE (modules on disk, none loaded)", - "dirtyfrag:kernel_patch_check._esp_kernel_patched|_esp_admin_patched", + "_esp_kernel_patched|_esp_admin_patched", "PATCHED (kernel fix applied)", "dirtyfrag_esp_present._esp_mitigated", "MITIGATED (blacklist in place)", + "dirtyfrag_esp_present._userns_conf_exists", + "MITIGATED (userns disabled)", "NOT AFFECTED" ); @@ -207,16 +167,41 @@ bundle agent main "VULNERABLE (rxrpc loaded)", "dirtyfrag_rxrpc_needs_mitigation", "VULNERABLE (module on disk, not loaded)", - "dirtyfrag:kernel_patch_check._rxrpc_kernel_patched|_rxrpc_admin_patched", + "_rxrpc_kernel_patched|_rxrpc_admin_patched", "PATCHED (kernel fix applied)", "dirtyfrag_rxrpc_present._rxrpc_mitigated", "MITIGATED (blacklist in place)", "NOT AFFECTED" ); - # --- Inventory output for Mission Portal --- + # --- Per-CVE mitigation method (one-word enum) ----------------- + # Single keyword per CVE so MP can filter "show me all hosts where + # mitigation method is `userns`" -- much cleaner than regex-matching + # the long _esp_status string. ifelse takes first-match order. + "_esp_mitigation_method" + string => ifelse( + "dirtyfrag_esp_needs_mitigation", "none", + "_esp_kernel_patched", "kernel-patch", + "_esp_admin_patched", "admin-override", + "_esp_conf_exists", "modprobe", + "_userns_conf_exists", "userns", + "not-applicable" + ); + + "_rxrpc_mitigation_method" + string => ifelse( + "dirtyfrag_rxrpc_needs_mitigation", "none", + "_rxrpc_kernel_patched", "kernel-patch", + "_rxrpc_admin_patched", "admin-override", + "_rxrpc_conf_exists", "modprobe", + "not-applicable" + ); + + # --- Inventory output for Mission Portal ---------------------- + # The CVE id and module family live in attribute_name (the column + # header); the value is just the state so it's easy to filter on. "inventory_dirtyfrag_esp" - string => "CVE-2026-43284 (xfrm-ESP): $(_esp_status)", + string => "$(_esp_status)", meta => { "inventory", "attribute_name=Dirty Frag CVE-2026-43284 (xfrm-ESP) status", @@ -224,179 +209,230 @@ bundle agent main comment => "CVE-2026-43284 xfrm-ESP mitigation status"; "inventory_dirtyfrag_rxrpc" - string => "CVE-2026-43500 (RxRPC): $(_rxrpc_status)", + string => "$(_rxrpc_status)", meta => { "inventory", "attribute_name=Dirty Frag CVE-2026-43500 (RxRPC) status", }, comment => "CVE-2026-43500 RxRPC mitigation status"; + "inventory_dirtyfrag_esp_method" + string => "$(_esp_mitigation_method)", + meta => { + "inventory", + "attribute_name=Dirty Frag CVE-2026-43284 mitigation method", + }, + comment => "Active mitigation strategy for CVE-2026-43284 (filterable enum)"; + + "inventory_dirtyfrag_rxrpc_method" + string => "$(_rxrpc_mitigation_method)", + meta => { + "inventory", + "attribute_name=Dirty Frag CVE-2026-43500 mitigation method", + }, + comment => "Active mitigation strategy for CVE-2026-43500 (filterable enum)"; + classes: - # --- CMDB toggles --- - "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); - "_mitigate_rxrpc" expression => strcmp("true", "$(mitigate_rxrpc)"); + # --- CMDB toggles --------------------------------------------- + "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); + "_mitigate_rxrpc" expression => strcmp("true", "$(mitigate_rxrpc)"); "_mitigate_userns" expression => strcmp("true", "$(mitigate_userns)"); - # --- Admin override: manually declare host as patched --- - "_esp_admin_patched" expression => strcmp("true", "$(esp_patched)"); + # --- Admin override: manually declare host as patched --------- + "_esp_admin_patched" expression => strcmp("true", "$(esp_patched)"); "_rxrpc_admin_patched" expression => strcmp("true", "$(rxrpc_patched)"); - # --- Unprivileged user namespace enabled? --- - "_ns_proc_file_exists" expression => fileexists("${_ns_proc}"); - "_ns_val_is_1" expression => strcmp("${_ns_val}", "1"); - "_userns_enabled" and => { "_ns_proc_file_exists", "_ns_val_is_1" }; - - # --- xfrm-ESP/IPComp modules present? --- - "_esp4_on_disk" expression => fileexists("${_esp4_path}"); - "_esp6_on_disk" expression => fileexists("${_esp6_path}"); - "_ipcomp4_on_disk" expression => fileexists("${_ipcomp4_path}"); - "_ipcomp6_on_disk" expression => fileexists("${_ipcomp6_path}"); - - # Compressed variants (.ko.zst, .ko.xz) - "_esp4_on_disk_z" expression => fileexists("${_esp4_path}.zst"); - "_esp6_on_disk_z" expression => fileexists("${_esp6_path}.zst"); - "_ipcomp4_on_disk_z" expression => fileexists("${_ipcomp4_path}.zst"); - "_ipcomp6_on_disk_z" expression => fileexists("${_ipcomp6_path}.zst"); - "_esp4_on_disk_xz" expression => fileexists("${_esp4_path}.xz"); - "_esp6_on_disk_xz" expression => fileexists("${_esp6_path}.xz"); - "_ipcomp4_on_disk_xz" expression => fileexists("${_ipcomp4_path}.xz"); - "_ipcomp6_on_disk_xz" expression => fileexists("${_ipcomp6_path}.xz"); - - # Currently loaded - "_esp4_loaded" expression => isdir("/sys/module/esp4"); - "_esp6_loaded" expression => isdir("/sys/module/esp6"); - "_ipcomp4_loaded" expression => isdir("/sys/module/ipcomp"); + # --- Kernel-patch comparison ---------------------------------- + # sort -V -C exits 0 iff stdin is already version-sorted, so the + # pipeline below answers "is installed >= patched?" in one fork. + # This keeps useshell despite the noshell preference elsewhere: + # CFEngine has no semantic-version comparison primitive, sort -V + # needs stdin (no single-arg form), and the noshell alternatives + # (tempfile + commands:, or a bundled wrapper script) are heavier + # for no real safety gain on a static, internally-built string. + # See lessons-learned/version-compare-shell-exception.md. + "_patch_data_matched" + expression => isvariable("_matched_idx"); + + "_esp_kernel_patched" + expression => returnszero( + "printf '%s\\n%s\\n' '$(_esp_patched_ver)' '$(_kernel_pkg_ver)' | sort -V -C", + "useshell" + ), + if => and( + isvariable("_esp_patched_ver"), + isvariable("_kernel_pkg_ver") + ); + + "_rxrpc_kernel_patched" + expression => returnszero( + "printf '%s\\n%s\\n' '$(_rxrpc_patched_ver)' '$(_kernel_pkg_ver)' | sort -V -C", + "useshell" + ), + if => and( + isvariable("_rxrpc_patched_ver"), + isvariable("_kernel_pkg_ver") + ); + + # --- Unprivileged user namespace enabled? --------------------- + "_userns_disabled" expression => strcmp("0", "${_max_userns}"); + "_userns_enabled" not => "_userns_disabled"; + + # --- xfrm-ESP/IPComp modules ---------------------------------- + # On-disk presence collapses to: did findfiles return anything? + # some(".+", slist) is true when the list has any non-empty element. + "_esp_files_present" expression => some(".+", "_esp_module_files"); + + "_esp4_loaded" expression => isdir("/sys/module/esp4"); + "_esp6_loaded" expression => isdir("/sys/module/esp6"); + "_ipcomp_loaded" expression => isdir("/sys/module/ipcomp"); "_ipcomp6_loaded" expression => isdir("/sys/module/ipcomp6"); "_esp_any_loaded" or => { - "_esp4_loaded", "_esp6_loaded", "_ipcomp4_loaded", "_ipcomp6_loaded" + "_esp4_loaded", "_esp6_loaded", "_ipcomp_loaded", "_ipcomp6_loaded" }; "dirtyfrag_esp_present" - or => { - "_esp4_on_disk", - "_esp6_on_disk", - "_ipcomp4_on_disk", - "_ipcomp6_on_disk", - "_esp4_on_disk_z", - "_esp6_on_disk_z", - "_ipcomp4_on_disk_z", - "_ipcomp6_on_disk_z", - "_esp4_on_disk_xz", - "_esp6_on_disk_xz", - "_ipcomp4_on_disk_xz", - "_ipcomp6_on_disk_xz", - "_esp4_loaded", - "_esp6_loaded", - "_ipcomp4_loaded", - "_ipcomp6_loaded", - }; + or => { "_esp_files_present", "_esp_any_loaded" }; - # --- RxRPC module present? --- - "_rxrpc_on_disk" expression => fileexists("${_rxrpc_path}"); - "_rxrpc_on_disk_z" expression => fileexists("${_rxrpc_path}.zst"); - "_rxrpc_on_disk_xz" expression => fileexists("${_rxrpc_path}.xz"); - "_rxrpc_loaded" expression => isdir("/sys/module/rxrpc"); + # --- RxRPC module --------------------------------------------- + "_rxrpc_files_present" expression => some(".+", "_rxrpc_module_files"); + "_rxrpc_loaded" expression => isdir("/sys/module/rxrpc"); "dirtyfrag_rxrpc_present" - or => { - "_rxrpc_on_disk", - "_rxrpc_on_disk_z", - "_rxrpc_on_disk_xz", - "_rxrpc_loaded", - }; + or => { "_rxrpc_files_present", "_rxrpc_loaded" }; - # --- Mitigation conf files in place? --- - "_esp_conf_exists" expression => fileexists("${_esp_conf_path}"); - "_rxrpc_conf_exists" expression => fileexists("${_rxrpc_conf_path}"); + # --- Mitigation conf files in place? -------------------------- + "_esp_conf_exists" expression => fileexists("${_esp_conf_path}"); + "_rxrpc_conf_exists" expression => fileexists("${_rxrpc_conf_path}"); "_userns_conf_exists" expression => fileexists("${_userns_conf_path}"); - # ESP is mitigated by modprobe blacklist, disabled userns, patched kernel, - # or admin override + # ESP is mitigated by modprobe blacklist, patched kernel, or + # admin override. Userns mitigation is handled in the + # vulnerability condition (see dirtyfrag_esp_needs_mitigation). "_esp_mitigated" or => { "_esp_conf_exists", - "_userns_conf_exists", - "!_userns_enabled", - "dirtyfrag:kernel_patch_check._esp_kernel_patched", + "_esp_kernel_patched", "_esp_admin_patched", }; # RxRPC is mitigated by the modprobe blacklist, patched kernel, - # or admin override + # or admin override. "_rxrpc_mitigated" or => { "_rxrpc_conf_exists", - "dirtyfrag:kernel_patch_check._rxrpc_kernel_patched", + "_rxrpc_kernel_patched", "_rxrpc_admin_patched", }; - # --- Per-CVE vulnerability checks --- + # --- Per-CVE vulnerability checks ----------------------------- + # ESP is vulnerable if modules are present, not mitigated, + # AND userns mitigation conf is not in place. Userns conf is + # not in _esp_mitigated (which tracks blacklist/mitigation + # confs); it's tracked here so disabling userns blocks the + # exploit vector directly. "dirtyfrag_esp_needs_mitigation" - and => { "dirtyfrag_esp_present", "!_esp_mitigated" }; + and => { "dirtyfrag_esp_present", "!_esp_mitigated", "!_userns_conf_exists" }, + meta => { "report" }, + scope => "namespace"; "dirtyfrag_rxrpc_needs_mitigation" - and => { "dirtyfrag_rxrpc_present", "!_rxrpc_mitigated" }; + and => { "dirtyfrag_rxrpc_present", "!_rxrpc_mitigated" }, + meta => { "report" }, + scope => "namespace"; + + # --- Composite roll-ups for targeting and querying ------------ + # Tagged `report` (not `inventory`) so cf-hub collects them but they + # don't clutter the default Mission Portal inventory columns. Use + # for class filters in reports, alerts, and CMDB-style targeting. + "dirtyfrag_vulnerable" + or => { "dirtyfrag_esp_needs_mitigation", "dirtyfrag_rxrpc_needs_mitigation" }, + meta => { "report" }, + scope => "namespace"; + + "dirtyfrag_esp_mitigated" + or => { "_esp_mitigated", "_userns_conf_exists" }, + meta => { "report" }, + scope => "namespace"; + + "dirtyfrag_rxrpc_mitigated" + or => { "_rxrpc_mitigated" }, + meta => { "report" }, + scope => "namespace"; files: - # --- CVE-2026-43284: ESP/IPComp mitigation --- + # CVE refs go in the promisee slot (-> { ... }); Mission Portal + # surfaces this as the "stakeholder / linked identifier" on the + # promise detail page so operators can search "CVE-2026-43284" + # and find every policy artifact addressing it. + # handle gives each promise a stable, queryable name in reports. + _mitigate_esp:: - "${_esp_conf_path}" - create => "true", + "${_esp_conf_path}" -> { "CVE-2026-43284" } + handle => "dirtyfrag_esp_modprobe_blacklist", + create => "true", content => "${_esp_conf_content}", - classes => default:results("bundle", "dirtyfrag_esp_conf"); + comment => "Blacklist xfrm-ESP/IPComp modules to mitigate Dirty Frag CVE-2026-43284"; - # --- CVE-2026-43500: RxRPC mitigation --- _mitigate_rxrpc:: - "${_rxrpc_conf_path}" - create => "true", + "${_rxrpc_conf_path}" -> { "CVE-2026-43500" } + handle => "dirtyfrag_rxrpc_modprobe_blacklist", + create => "true", content => "${_rxrpc_conf_content}", - classes => default:results("bundle", "dirtyfrag_rxrpc_conf"); + comment => "Blacklist RxRPC module to mitigate Dirty Frag CVE-2026-43500"; - # --- CVE-2026-43284 alternative: disable unprivileged user namespaces --- _mitigate_userns:: - "${_userns_conf_path}" - create => "true", + "${_userns_conf_path}" -> { "CVE-2026-43284" } + handle => "dirtyfrag_userns_sysctl_conf", + create => "true", content => "${_userns_conf_content}", - classes => default:results("bundle", "dirtyfrag_userns_conf"); + comment => "Disable unprivileged user namespaces to block CVE-2026-43284 ESP exploit path"; commands: - # --- Unload ESP/IPComp modules after conf written --- - _mitigate_esp.dirtyfrag_esp_conf_repaired:: - "/sbin/rmmod" - arglist => { "esp4", "esp6", "ipcomp", "ipcomp6" }, - comment => "Unload ESP/IPComp modules already in memory"; - - # --- Unload RxRPC module after conf written --- - _mitigate_rxrpc.dirtyfrag_rxrpc_conf_repaired:: - "/sbin/rmmod" - arglist => { "rxrpc" }, - comment => "Unload RxRPC module already in memory"; - - # --- Apply sysctl after writing userns conf --- - _mitigate_userns.dirtyfrag_userns_conf_repaired:: - "/sbin/sysctl" + # Key off "module is loaded + mitigation desired", not "conf was + # just written". The previous _conf_repaired guard missed the + # case where the conf file exists from a prior run and the module + # got loaded since -- the agent would never re-run the unload. + # modprobe -r (vs rmmod) tolerates "some of the named modules + # aren't loaded", which simplifies the per-module gating to a + # single class. + + _mitigate_esp._esp_any_loaded:: + "/sbin/modprobe" -> { "CVE-2026-43284" } + handle => "dirtyfrag_esp_modprobe_unload", + arglist => { "-r", "esp4", "esp6", "ipcomp", "ipcomp6" }, + comment => "Unload ESP/IPComp modules while ESP mitigation is active"; + + _mitigate_rxrpc._rxrpc_loaded:: + "/sbin/modprobe" -> { "CVE-2026-43500" } + handle => "dirtyfrag_rxrpc_modprobe_unload", + arglist => { "-r", "rxrpc" }, + comment => "Unload RxRPC module while RxRPC mitigation is active"; + + # sysctl --system is idempotent; re-running it when the conf says + # 0 but the live value isn't 0 picks up cases where the conf was + # written but never applied (e.g. between writing and the next + # boot). + _mitigate_userns._userns_enabled:: + "/sbin/sysctl" -> { "CVE-2026-43284" } + handle => "dirtyfrag_userns_sysctl_reapply", arglist => { "--system" }, - comment => "Apply user.max_user_namespaces=0 without reboot"; + comment => "Re-apply user.max_user_namespaces=0 while userns is enabled"; reports: - inform_mode:: + inform_mode._patch_data_matched:: + "Dirty Frag: matched distro $(_os_id) $(_os_ver) (entry $(_matched_idx)); kernel package version=$(_kernel_pkg_ver)"; - "Dirty Frag CVE-2026-43284 (xfrm-ESP/IPComp): $(_esp_status)" - if => "dirtyfrag_esp_present"; + inform_mode._esp_kernel_patched:: + "Dirty Frag CVE-2026-43284: kernel $(_kernel_pkg_ver) >= $(_esp_patched_ver) (PATCHED)"; - "Dirty Frag CVE-2026-43500 (RxRPC): $(_rxrpc_status)" - if => "dirtyfrag_rxrpc_present"; -} + inform_mode._rxrpc_kernel_patched:: + "Dirty Frag CVE-2026-43500: kernel $(_kernel_pkg_ver) >= $(_rxrpc_patched_ver) (PATCHED)"; -body file control -{ - namespace => "default"; -} + inform_mode:: -bundle agent __main__ -{ - methods: - "dirtyfrag:main"; + "Dirty Frag CVE-2026-43284 (xfrm-ESP/IPComp): $(_esp_status)"; + "Dirty Frag CVE-2026-43500 (RxRPC): $(_rxrpc_status)"; } diff --git a/security/dirtyfrag/inventory-status.png b/security/dirtyfrag/inventory-status.png index 4adcd0b7c3421558ea1dfd480ddd9cdf079912a8..9bb816508403eef72e851d72bf14bdf871073c8b 100644 GIT binary patch literal 127073 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%0AZ9_S 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 Xehv(9UsVz!b;;Z literal 50761 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 Date: Tue, 19 May 2026 16:42:34 -0500 Subject: [PATCH 104/146] Inventory SELinux modules --- cfbs.json | 9 +++ security/inventory-selinux-modules/README.org | 15 ++++ .../inventory-selinux-modules.png | Bin 0 -> 88998 bytes security/inventory-selinux-modules/main.cf | 74 ++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 security/inventory-selinux-modules/README.org create mode 100644 security/inventory-selinux-modules/inventory-selinux-modules.png create mode 100644 security/inventory-selinux-modules/main.cf diff --git a/cfbs.json b/cfbs.json index 549f89d1..58e43587 100644 --- a/cfbs.json +++ b/cfbs.json @@ -167,6 +167,15 @@ "subdirectory": "management/every-minute", "steps": ["json def.json def.json"] }, + "inventory-selinux-modules": { + "description": "Inventory installed SELinux modules and their enabled/disabled status.", + "subdirectory": "security/inventory-selinux-modules", + "steps": [ + "copy main.cf services/cfbs/modules/inventory-selinux-modules/main.cf", + "policy_files services/cfbs/modules/inventory-selinux-modules/main.cf", + "bundles inventory_selinux:semodule_list_modules" + ] + }, "inventory-unshadowed-users": { "description": "Adds reporting data (inventory) on local users in /etc/passwd not using /etc/shadow for their password.", "subdirectory": "security/inventory-unshadowed-users", diff --git a/security/inventory-selinux-modules/README.org b/security/inventory-selinux-modules/README.org new file mode 100644 index 00000000..aa72e9aa --- /dev/null +++ b/security/inventory-selinux-modules/README.org @@ -0,0 +1,15 @@ +This module inventories the SELinux Modules that are installed. + +* Inventory +- SELinux Modules Enabled :: SELinux Modules that are installed and not disabled. +- SELinux Modules Disabled :: SELinux Modules that are installed and disabled. + +[[https://raw.githubusercontent.com/cfengine/modules/master/security/inventory-selinux-modules/inventory-selinux-modules.png]] + +* Configuration +** Variables +- =inventory_selinux:semodule_list_modules.hours_cached= :: Number of hours to cache the output from ~semodule --list-modules=full~. + +* Requirements + +The ~semodule~ command is required for inventory to be reported. diff --git a/security/inventory-selinux-modules/inventory-selinux-modules.png b/security/inventory-selinux-modules/inventory-selinux-modules.png new file mode 100644 index 0000000000000000000000000000000000000000..fe3e424f0d1f1618093f506282b8d3a3904c2246 GIT binary patch literal 88998 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 "inventory_selinux"; +} + +bundle agent semodule_list_modules +{ + classes: + "_$(this.namespace)_$(this.bundle)_supported_platform" + if => isexecutable( "$(path[semodule])"); + + vars: + "path[semodule]" + string => ifelse( isexecutable("/sbin/semodule"), "/sbin/semodule", + isexecutable("/usr/sbin/semodule"), "/usr/sbin/semodule", + "" ), + meta => { "paths.cf" }; + + _inventory_selinux_semodule_list_modules_supported_platform:: + "hours_cached" + string => "1", + if => not( isvariable( hours_cached )); + + "cache_file" + string => "$(sys.statedir)/$(this.namespace)_$(this.bundle).txt"; + + "cache_file_age" + string => filestat( $(cache_file), mtime ), + if => fileexists( "$(cache_file)" ); + + # Example $(cache_file) content + # 400 cfengine-enterprise pp + # 200 container pp + # 100 wine pp disabled + # 100 wireshark pp + + + "d" data => data_readstringarrayidx( "$(cache_file)", + "", + "\s+", + inf, + inf), + if => fileexists( $(cache_file) ); + + "i" slist => getindices( "d" ); + + # Inventory enabled and disabled selinux modules + "enabled[$(i)]" + string => "$(d[$(i)][1])", + if => strcmp( "$(d[$(i)][3])", "" ), + meta => { "inventory", "attribute_name=SELinux Modules Enabled" }; + + "disabled[$(i)]" + string => "$(d[$(i)][1])", + if => strcmp( "$(d[$(i)][3])", "disabled" ), + meta => { "inventory", "attribute_name=SELinux Modules Disabled" }; + + files: + _inventory_selinux_semodule_list_modules_supported_platform:: + # Delete the cache file if it's older than hours_cached + "$(cache_file)" + delete => default:tidy, + if => isgreaterthan( + format( "%d", eval( "$(sys.systime)-$(cache_file_age)" )), # now minus mtime of cache file + format( "%d", eval( "$(hours_cached)*60*60" ))); # cache minutes + commands: + _inventory_selinux_semodule_list_modules_supported_platform:: + "/sbin/semodule" + arglist => { "--list-modules=full", + ">", "$(cache_file)" }, + contain => default:in_shell, # Required for redirection + if => not( fileexists( $(cache_file) )); + +} From 9c0ba63a3dacd7df624be3e8610db9f2435cac18 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 25 May 2026 18:02:36 -0500 Subject: [PATCH 105/146] Added inventory-fwupd module for firmware inventory, HSI security posture, and monitoring measurements --- cfbs.json | 64 +++ inventory/inventory-fwupd/README.org | 110 ++++ .../inventory-fwupd/fwupd-inventory.mustache | 3 + .../mp-inventory-fwupd-columns.png | Bin 0 -> 79973 bytes .../mp-monitoring-firmware-devices.png | Bin 0 -> 61220 bytes inventory/inventory-fwupd/policy.cf | 491 ++++++++++++++++++ 6 files changed, 668 insertions(+) create mode 100644 inventory/inventory-fwupd/README.org create mode 100644 inventory/inventory-fwupd/fwupd-inventory.mustache create mode 100644 inventory/inventory-fwupd/mp-inventory-fwupd-columns.png create mode 100644 inventory/inventory-fwupd/mp-monitoring-firmware-devices.png create mode 100644 inventory/inventory-fwupd/policy.cf diff --git a/cfbs.json b/cfbs.json index 3064b4d4..28923372 100644 --- a/cfbs.json +++ b/cfbs.json @@ -33,6 +33,15 @@ "subdirectory": "reporting/client-initiated-reporting", "steps": ["json def.json def.json"] }, + "compliance-report-fwupd": { + "description": "Compliance report for firmware security posture via fwupd HSI and update status.", + "tags": ["reporting", "compliance", "security", "hardware"], + "subdirectory": "reporting/compliance-report-fwupd", + "dependencies": ["inventory-fwupd"], + "steps": [ + "copy fwupd-compliance-report.json .no-distrib/compliance-report-definitions/fwupd-compliance-report.json" + ] + }, "command-dispatcher": { "description": "Command dispatcher for running shell commands on schedule", "subdirectory": "management/command-dispatcher", @@ -246,6 +255,17 @@ "bundles inventory_fde:main" ] }, + "inventory-fwupd": { + "description": "Inventory hardware firmware versions, pending firmware updates, and host security attributes via fwupd.", + "tags": ["inventory", "monitoring", "hardware", "security"], + "subdirectory": "inventory/inventory-fwupd", + "steps": [ + "copy policy.cf services/cfbs/modules/inventory-fwupd/policy.cf", + "copy fwupd-inventory.mustache services/cfbs/modules/inventory-fwupd/fwupd-inventory.mustache", + "policy_files services/cfbs/modules/inventory-fwupd/policy.cf", + "bundles inventory_fwupd_main" + ] + }, "inventory-smartctl": { "description": "Inventory SMART drive health, temperature, and wear data.", "tags": ["inventory", "monitoring", "hardware", "storage"], @@ -278,6 +298,50 @@ "bundles maintainers_in_motd" ] }, + "manage-fwupd": { + "description": "Ensure fwupd is installed and its refresh timer is enabled. Optionally apply firmware updates for devices matching an allow-list.", + "subdirectory": "management/manage-fwupd", + "dependencies": ["inventory-fwupd"], + "steps": [ + "copy main.cf services/cfbs/modules/manage-fwupd/main.cf", + "policy_files services/cfbs/modules/manage-fwupd/main.cf", + "bundles manage_fwupd:allowed manage_fwupd:main", + "input ./input.json def.json" + ], + "input": [ + { + "type": "string", + "variable": "apply_updates", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Apply firmware updates", + "question": "Class expression for when firmware updates should be applied (e.g. 'any', 'linux', '(env_dev|env_qa).Night.(cohort_A|cohort_C)'). Default '!any' means disabled.", + "default": "!any" + }, + { + "type": "list", + "variable": "device_name_reglist", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Allowed devices", + "subtype": { + "type": "string", + "label": "Device name pattern", + "question": "Device name (regex) to allow firmware updates for" + }, + "while": "Do you want to allow firmware updates for more devices?" + }, + { + "type": "string", + "variable": "reboot_after_update", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Reboot after firmware update", + "question": "Class expression for when to reboot after a firmware update is applied (e.g. 'any', 'Night', 'Hr04.Min00_05'). Default '!any' means disabled (no automatic reboot).", + "default": "!any" + } + ] + }, "powershell-execution-policy": { "description": "Inventory and bundle for PowerShell Execution Policy", "subdirectory": "management/powershell-execution-policy", diff --git a/inventory/inventory-fwupd/README.org b/inventory/inventory-fwupd/README.org new file mode 100644 index 00000000..41c4142e --- /dev/null +++ b/inventory/inventory-fwupd/README.org @@ -0,0 +1,110 @@ +Surface fwupd state as inventory attributes: current firmware versions +and vendors, pending updates from LVFS, and Host Security Identifier +(HSI) security posture. + +Pair with *manage-fwupd* to optionally apply firmware updates for +devices matching an allow-list. + +* Requirements + +- Linux (silently no-ops on other platforms) +- =fwupd= package for full functionality (manage-fwupd installs it + automatically). Without fwupd, the module still runs but reports + =Firmware update status= as =FWUPD_MISSING=. +- =fwupd-refresh.timer= enabled so the LVFS firmware catalog stays + current (manage-fwupd handles this automatically) + +* Mission Portal + +The inventory attributes appear in Mission Portal's column selector +under "fwupd": + +[[file:mp-inventory-fwupd-columns.png]] + +* Inventory Attributes + +** Rolled-up status + +| Attribute | Values | +|----------------------------+-----------------------------------------------------------------| +| *Firmware update status* | =OK= -- no pending updates | +| | =UPDATES_AVAILABLE= -- one or more devices have pending updates | +| | =NO_DEVICES= -- fwupd present but no updatable devices | +| | =FWUPD_MISSING= -- fwupd is not installed | + +** Per-device attributes + +For every device fwupd reports (keyed by DeviceId): + +| Attribute | Format | +|------------------------------------+---------------------------------------------------------| +| *Firmware devices* | =Name | Vendor | vX.Y.Z | [plugin]= | +| *Firmware device pending update* | =Name: current -> new= (only when an update is pending) | + +** HSI attributes + +| Attribute | Format | +|---------------------------------+------------------------------------------------------------| +| *Firmware HSI level* | =HSI:0= through =HSI:4= | +| *Firmware HSI L: * | =PASS= or =FAIL= (one per security check) | +| *Firmware HSI attributes* | =Name (HSI L): [PASS|FAIL]= (slist) | + +*Firmware HSI level* is the rolled-up Host Security Identifier level. +fwupd walks levels 1--4 sequentially; the result is the highest level +where all attributes pass, stopping at the first level with any failure. + +*Firmware HSI L: * variables (e.g. =Firmware HSI L1: TPM v2.0=) +are individual string attributes with value =PASS= or =FAIL=. These are +consumed by *compliance-report-fwupd* for per-check compliance conditions. +Two normalizations are applied to keep inventory attribute names stable +and aligned with the HSI specification: + +- *Name normalization:* The CSME version attribute is emitted as + =Firmware HSI L1: CSME version= regardless of the firmware version + string fwupd reports (which varies per host). +- *Level normalization:* fwupd marks some runtime checks at HsiLevel 0 + even though they contribute to scored HSI levels. The module maps + these to their specification levels: =UEFI secure boot= is emitted + at L1 (not L0) and =CET OS Support= at L3 (not L0). + +*Firmware HSI attributes* is an slist with one detailed entry per +security check, useful for drill-down in Mission Portal inventory views. + +* Measurements + +The following values are emitted as =cf-monitord= measurements for +time-series tracking in Mission Portal: + +| Measurement | Units | Description | +|----------------------------------+---------+-----------------------------------------| +| =firmware_devices_total= | devices | Number of devices fwupd is tracking | +| =firmware_updates_available= | devices | Number of devices with a pending update | +| =firmware_hsi_failing= | checks | Number of failing HSI security checks | + +These appear in Mission Portal monitoring graphs as +=firmware_devices_total=, =firmware_updates_available=, and +=firmware_hsi_failing=. + +[[file:mp-monitoring-firmware-devices.png]] + +* Classes + +The module defines namespace-scoped classes for platform-specific +compliance report targeting: + +| Class | Source | Matches | +|--------------------------+--------------------------------+--------------------------------| +| =fwupd_cpu_vendor_intel= | =/proc/cpuinfo= vendor_id | =GenuineIntel= | +| =fwupd_cpu_vendor_amd= | =/proc/cpuinfo= vendor_id | =AuthenticAMD= | +| =fwupd_oem_vendor_hp= | =/sys/class/dmi/id/sys_vendor= | =HP Inc.= or =Hewlett-Packard= | + +These classes are used by *compliance-report-fwupd* =host_filter= +fields to restrict Intel-only, AMD-only, and HP-only conditions to +the relevant hardware. + +* Limitations + +The module is /read-only/ -- it never applies firmware updates. +Use *manage-fwupd* for that. + + diff --git a/inventory/inventory-fwupd/fwupd-inventory.mustache b/inventory/inventory-fwupd/fwupd-inventory.mustache new file mode 100644 index 00000000..c5f8e838 --- /dev/null +++ b/inventory/inventory-fwupd/fwupd-inventory.mustache @@ -0,0 +1,3 @@ +^context=inventory_fwupd_cache +^meta=inventory,attribute_name=Firmware update status +=fwupd_status={{{status}}} diff --git a/inventory/inventory-fwupd/mp-inventory-fwupd-columns.png b/inventory/inventory-fwupd/mp-inventory-fwupd-columns.png new file mode 100644 index 0000000000000000000000000000000000000000..194d80c9298a2d6a9737b5e6b81c75780a2b6b77 GIT binary patch literal 79973 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 F{{uj#A5{PV literal 0 HcmV?d00001 diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf new file mode 100644 index 00000000..d77249e4 --- /dev/null +++ b/inventory/inventory-fwupd/policy.cf @@ -0,0 +1,491 @@ +# inventory-fwupd policy +# +# Inventories hardware firmware versions, pending firmware updates, and the +# Host Security ID (HSI) attribute scores reported by fwupd. +# +# Implementation notes: +# - Device list is read directly from /var/cache/fwupd/devices.json, which +# fwupd's own systemd timer (fwupd-refresh.timer) keeps up to date. No +# fwupdmgr call is needed for the common steady-state inventory. +# - Pending updates and HSI attributes are not cached as files by fwupd, so +# they are sourced via fwupdmgr and re-fetched only when their respective +# JSON caches age past their TTL. +# - All inventory variables are emitted via a single module-protocol cache +# rendered from JSON with an inline_mustache template. The cache is +# consumed in one call to read_module_protocol(), which loads dozens of +# inventory-tagged variables at far lower runtime cost than iterating the +# raw JSON with classic arrays. +# +# Rolled-up status (Mission Portal attribute "Firmware update status"): +# OK - fwupd present, no pending updates +# UPDATES_AVAILABLE - one or more devices have firmware updates pending +# NO_DEVICES - fwupd present but reports no updatable devices +# FWUPD_MISSING - fwupd not installed on the host + +bundle agent inventory_fwupd_main +{ + vars: + linux:: + # Public API: referenced by manage-fwupd and other consumers. + "fwupdmgr" + string => ifelse( + fileexists("/usr/bin/fwupdmgr"), "/usr/bin/fwupdmgr", + fileexists("/usr/local/bin/fwupdmgr"), "/usr/local/bin/fwupdmgr", + "/usr/bin/fwupdmgr"); + + "_devices_src" string => "/var/cache/fwupd/devices.json"; + "_updates_src" string => "$(sys.statedir)/fwupd_inventory_updates.json"; + "_security_src" string => "$(sys.statedir)/fwupd_inventory_security.json"; + "_cache" string => "$(sys.statedir)/fwupd_inventory_cache"; + "_template" string => "$(this.promise_dirname)/fwupd-inventory.mustache"; + + # TTLs in seconds for the fwupdmgr-derived JSON caches. + "_updates_ttl" string => "43200"; # 12 h + "_security_ttl" string => "86400"; # 24 h + + # Pre-compute file mtimes here rather than calling filestat() inline + # inside class expressions: nested $(filestat($(path), mtime)) does not + # expand reliably inside isgreaterthan(), so the staleness comparisons + # silently never match. Defaulting to "0" when a file is missing lets + # the class expressions below stay simple and total. + "_cache_mtime" + string => ifelse(fileexists("$(_cache)"), + filestat("$(_cache)", "mtime"), "0"); + "_devices_mtime" + string => ifelse(fileexists("$(_devices_src)"), + filestat("$(_devices_src)", "mtime"), "0"); + "_updates_mtime" + string => ifelse(fileexists("$(_updates_src)"), + filestat("$(_updates_src)", "mtime"), "0"); + "_security_mtime" + string => ifelse(fileexists("$(_security_src)"), + filestat("$(_security_src)", "mtime"), "0"); + + "_updates_age" string => eval("$(sys.systime) - $(_updates_mtime)", "math", "infix"); + "_security_age" string => eval("$(sys.systime) - $(_security_mtime)", "math", "infix"); + + # /proc/1 mtime is when PID 1 (init) was exec'd, i.e., boot time on + # Linux. Used below to force a refresh of the updates cache after a + # reboot, so consumers like manage-fwupd don't act on a pre-boot + # snapshot that still lists devices whose updates have already been + # activated. + "_boot_mtime" + string => ifelse(fileexists("/proc/1"), + filestat("/proc/1", "mtime"), "0"); + + classes: + linux:: + "have_fwupdmgr" + scope => "namespace", + expression => isexecutable("$(fwupdmgr)"); + "_have_devices" expression => fileexists("$(_devices_src)"); + + # CPU vendor classes for platform-specific compliance conditions. + # /proc/cpuinfo vendor_id: "GenuineIntel" or "AuthenticAMD". + "fwupd_cpu_vendor_intel" + scope => "namespace", + meta => { "report" }, + expression => regline("vendor_id.*GenuineIntel", "/proc/cpuinfo"); + "fwupd_cpu_vendor_amd" + scope => "namespace", + meta => { "report" }, + expression => regline("vendor_id.*AuthenticAMD", "/proc/cpuinfo"); + + # OEM vendor class for HP-specific compliance conditions. + # /sys/class/dmi/id/sys_vendor: "HP Inc." (post-2015) or + # "Hewlett-Packard" (pre-2015). + "fwupd_oem_vendor_hp" + scope => "namespace", + meta => { "report" }, + expression => regline("(HP Inc\.|Hewlett-Packard)", + "/sys/class/dmi/id/sys_vendor"); + + linux.have_fwupdmgr:: + "_updates_stale" + not => fileexists("$(_updates_src)"); + "_updates_stale" + expression => isgreaterthan("$(_updates_age)", "$(_updates_ttl)"), + if => fileexists("$(_updates_src)"); + # Force refresh after a reboot: a pre-boot cache may still list + # devices whose updates have been activated by the boot, and + # consumers (manage-fwupd) would otherwise repeatedly re-apply + # them and re-trigger reboots. + "_updates_stale" + expression => islessthan("$(_updates_mtime)", "$(_boot_mtime)"), + if => fileexists("$(_updates_src)"); + + "_security_stale" + not => fileexists("$(_security_src)"); + "_security_stale" + expression => isgreaterthan("$(_security_age)", "$(_security_ttl)"), + if => fileexists("$(_security_src)"); + + linux._have_devices:: + # Module-protocol cache must be rebuilt when any of the source JSON + # files is newer than the cache itself, or when the cache is missing. + "_cache_missing" not => fileexists("$(_cache)"); + "_cache_stale_devices" + expression => isgreaterthan("$(_devices_mtime)", "$(_cache_mtime)"); + "_cache_stale_updates" + expression => isgreaterthan("$(_updates_mtime)", "$(_cache_mtime)"), + if => "have_fwupdmgr"; + "_cache_stale_security" + expression => isgreaterthan("$(_security_mtime)", "$(_cache_mtime)"), + if => "have_fwupdmgr"; + "_rebuild_cache" + or => { "_cache_missing", "_cache_stale_devices", + "_cache_stale_updates", "_cache_stale_security" }; + + vars: + linux.have_fwupdmgr._updates_stale:: + "_updates_raw" + string => execresult("$(fwupdmgr) get-updates --json", "noshell"); + "_updates_payload" + string => ifelse(regcmp("\s*\{.*", "$(_updates_raw)"), + "$(_updates_raw)", + '{"Devices":[]}'); + + linux.have_fwupdmgr._security_stale:: + "_security_raw" + string => execresult("$(fwupdmgr) security --json", "noshell"); + "_security_payload" + string => ifelse(regcmp("\s*\{.*", "$(_security_raw)"), + "$(_security_raw)", + '{"SecurityAttributes":[]}'); + + files: + linux.have_fwupdmgr._updates_stale:: + "$(_updates_src)" + create => "true", + content => "$(_updates_payload)"; + + linux.have_fwupdmgr._security_stale:: + "$(_security_src)" + create => "true", + content => "$(_security_payload)"; + + methods: + linux._have_devices._rebuild_cache:: + "render" usebundle => inventory_fwupd_render( + "$(_devices_src)", "$(_updates_src)", "$(_security_src)", + "$(_template)", "$(_cache)"); + + linux.have_fwupdmgr:: + # HSI rollup is small (typically <40 attributes) and computed inline. + # Emitted as direct policy variables rather than via the module-protocol + # cache because filtering objects out of a data container and reshaping + # them into a mustache-iterable list is more code than just iterating + # and tagging the few results with inventory meta. + "hsi" usebundle => inventory_fwupd_hsi("$(_security_src)"), + if => fileexists("$(_security_src)"); + + # Per-device pending firmware update inventory, emitted natively + # from the updates JSON cache. Mirrors the HSI bundle idiom. + "updates" usebundle => inventory_fwupd_updates("$(_updates_src)"), + if => fileexists("$(_updates_src)"); + + # Per-device firmware inventory, emitted natively from the devices + # JSON cache. Mirrors the HSI bundle idiom. + "devices" usebundle => inventory_fwupd_devices("$(_devices_src)"), + if => fileexists("$(_devices_src)"); + + classes: + linux._have_devices:: + "_cache_was_read" if => read_module_protocol("$(_cache)"); + + linux.!_have_devices:: + # No source file from fwupd - emit a single status attribute. + "_emit_missing_status"; + + vars: + linux.!_have_devices._emit_missing_status:: + "fwupd_status" + string => "FWUPD_MISSING", + meta => { "inventory", "attribute_name=Firmware update status" }; + + reports: + linux._cache_was_read.verbose_mode:: + "inventory_fwupd: loaded $(inventory_fwupd_cache.fwupd_device_count) devices, status=$(inventory_fwupd_cache.fwupd_status)"; + + !linux.verbose_mode:: + "$(this.promise_filename): inventory-fwupd is Linux-only."; +} + +bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, template, cache) +# @brief Render the module-protocol cache file from the JSON sources. +# +# The cache must be a single mustache render: read_module_protocol() in the +# caller then loads every inventory variable in one shot. +{ + vars: + "_template_body" string => readfile("$(template)", "inf"); + + "_devices_json" data => readjson("$(devices_src)"); + "_dev_count" int => length("_devices_json[Devices]"); + + "_updates_json" + data => readjson("$(updates_src)"), + if => fileexists("$(updates_src)"); + "_updates_count" + int => length("_updates_json[Devices]"), + if => isvariable("_updates_json[Devices]"); + + "_security_json" + data => readjson("$(security_src)"), + if => fileexists("$(security_src)"); + + # Default counters when fwupdmgr-derived data is not present. + "_updates_count" + int => "0", + if => not(isvariable("_updates_count")); + + # Status rollup + "_status" + string => ifelse( + isgreaterthan("$(_updates_count)", "0"), "UPDATES_AVAILABLE", + isgreaterthan("$(_dev_count)", "0"), "OK", + "NO_DEVICES"); + + # HSI inventory is emitted directly by inventory_fwupd_hsi rather than + # threaded through this cache, since reshaping filtered objects into a + # mustache-iterable list is more code than just tagging them in policy. + "_extra_json" string => format( + '{ "status": "%s" }', + "$(_status)"); + "_extra" data => parsejson("$(_extra_json)"); + + # Measurements file: one key=value per line, read by + # inventory_fwupd_monitor for time-series tracking. + "_measurements_path" string => "$(sys.statedir)/fwupd_inventory_measurements"; + "_measurements_body" string => format( + "firmware_devices_total=%d$(const.n)firmware_updates_available=%d", + "$(_dev_count)", "$(_updates_count)"); + + # Mergedata combines device data, status counters, and optional + # extra top-level keys into a single data structure for mustache rendering. + # Note: pending-update inventory is emitted natively via + # inventory_fwupd_updates() rather than threaded through this cache, + # because Releases[0].Version cannot be accessed from mustache templates. + "_merged" data => mergedata("_devices_json", "_extra"); + + files: + "$(cache)" + create => "true", + template_method => "inline_mustache", + edit_template_string => "$(_template_body)", + template_data => @(_merged); + + "$(_measurements_path)" + create => "true", + content => "$(_measurements_body)"; +} + +bundle agent inventory_fwupd_hsi(security_src) +# @brief Surface fwupd Host Security Identifier (HSI) attributes and +# compute the rolled-up HSI level. +# +# Emits: +# - Per-attribute PASS/FAIL strings named "Firmware HSI L: " +# for use in compliance report conditions. +# - An slist of detailed results formatted "Name (HSI L): +# [PASS|FAIL]" for inventory drill-down. +# - A count of failing attributes. +# - The rolled-up HSI level (e.g. "HSI:3"). +# +# The rollup mirrors fwupd's own algorithm: walk levels 1-4 sequentially; +# at each level, if any non-obsoleted attribute fails, stop — the result +# is the highest level that had at least one success before the walk +# stopped. Empty levels (no attributes) are skipped without advancing +# or stopping. +# +# An attribute is failing when its HsiResult differs from HsiResultSuccess. +{ + classes: + "inventory_fwupd_hsi_failing_$(_idxes)" + not => strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"); + + # Per-level failure/success classes for the HSI rollup. + # If ANY attribute at level N fails, _hsi_LN_fail is defined. + # If ANY attribute at level N passes, _hsi_LN_pass is defined. + "_hsi_L$(_level[$(_idxes)])_fail" + expression => "inventory_fwupd_hsi_failing_$(_idxes)"; + "_hsi_L$(_level[$(_idxes)])_pass" + not => "inventory_fwupd_hsi_failing_$(_idxes)"; + + vars: + "_sec" data => readjson("$(security_src)"); + "_idxes" slist => getindices("_sec[SecurityAttributes]"); + + "_name[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][Name])"; + "_level[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiLevel])"; + "_result[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResult])"; + "_success[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResultSuccess])"; + + "_status[$(_idxes)]" + string => ifelse(strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"), + "PASS", "FAIL"); + + # Normalized name for per-attribute inventory: the CSME version + # attribute has a dynamic Name containing the firmware version + # (e.g. "csme v0:16.1.40.2765") which would create a different + # inventory attribute on every host. Normalize it to a fixed name + # so compliance conditions can reference it statically. + "_inv_name[$(_idxes)]" + string => ifelse(regcmp("csme v.*", "$(_name[$(_idxes)])"), + "CSME version", + "$(_name[$(_idxes)])"); + + # Normalized level for per-attribute inventory: fwupd marks some + # attributes as HsiLevel 0 with a "runtime-issue" flag even though + # they contribute to scored HSI levels. Map them to their HSI spec + # level so inventory attribute names match compliance conditions. + # UEFI secure boot (org.fwupd.hsi.Uefi.SecureBoot) → HSI:1 + # CET OS Support (org.fwupd.hsi.IntelCet.Active) → HSI:3 + "_inv_level[$(_idxes)]" + string => ifelse(strcmp("$(_name[$(_idxes)])", "UEFI secure boot"), + "1", + strcmp("$(_name[$(_idxes)])", "CET OS Support"), + "3", + "$(_level[$(_idxes)])"); + + # Per-attribute inventory: one string variable per HSI check, + # named "Firmware HSI L: " with value PASS or FAIL. + # This enables per-check compliance report conditions. + "fwupd_hsi_check[$(_idxes)]" + string => "$(_status[$(_idxes)])", + meta => { "inventory", + "attribute_name=Firmware HSI L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])" }; + + "fwupd_hsi_attr[$(_idxes)]" + string => "$(_name[$(_idxes)]) (HSI L$(_level[$(_idxes)])): $(_result[$(_idxes)]) [$(_status[$(_idxes)])]", + meta => { "inventory", "attribute_name=Firmware HSI attributes" }; + + "fwupd_hsi_failing_count" + int => countclassesmatching("inventory_fwupd_hsi_failing_[0-9]+"); + + # Rolled-up HSI level: highest level with a pass where no level + # at or below it has a failure. Checks highest first so the + # first match wins. Mirrors fwupd's fu_security_attrs_calculate_hsi(). + "fwupd_hsi_level" + string => ifelse( + and("_hsi_L4_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail"), not("_hsi_L3_fail"), not("_hsi_L4_fail")), "HSI:4", + and("_hsi_L3_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail"), not("_hsi_L3_fail")), "HSI:3", + and("_hsi_L2_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail")), "HSI:2", + and("_hsi_L1_pass", not("_hsi_L1_fail")), "HSI:1", + "HSI:0"), + meta => { "inventory", "attribute_name=Firmware HSI level" }; + + files: + # Write HSI failing count to its own measurements file so + # cf-monitord can track it as a time-series metric. + "$(sys.statedir)/fwupd_measurements_hsi" + create => "true", + content => "firmware_hsi_failing_attributes=$(fwupd_hsi_failing_count)", + if => isvariable("fwupd_hsi_failing_count"); +} + +bundle agent inventory_fwupd_updates(updates_src) +# @brief Surface per-device pending firmware update inventory. +# +# Reads the updates JSON cache and emits one inventory entry per device +# that has a pending update. Format: +# "Device Name: current_version -> new_version" +# +# Unlike the HSI bundle, devices here are iterated from the Devices array +# in fwupd_inventory_updates.json rather than SecurityAttributes. +# Releases[0].Version is the new version (mustache templates can't index +# into JSON arrays, so we flatten it natively in CFEngine). +{ + vars: + "_upd" data => readjson("$(updates_src)"); + "_idxes" slist => getindices("_upd[Devices]"); + + "_did[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][DeviceId])"; + "_name[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Name])"; + "_oldv[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Version])"; + "_newv[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Releases][0][Version])"; + + "fwupd_dev_pending[$(_did[$(_idxes)])]" + string => "$(_name[$(_idxes)]): $(_oldv[$(_idxes)]) -> $(_newv[$(_idxes)])", + meta => { "inventory", "attribute_name=Firmware device pending update" }; +} + +bundle agent inventory_fwupd_devices(devices_src) +# @brief Surface per-device firmware inventory, one entry per non-empty +# device. Format: "Name | Vendor | vVersion | [Plugin]" +# +# Reads the devices JSON cache and emits one inventory entry per device +# that has a non-empty Name. Emits via indexed arrays (same idiom as +# inventory_fwupd_hsi / inventory_fwupd_updates). Devices with empty +# Name silently skip via the if => not(strcmp(...)) guard. +{ + vars: + "_dev" data => readjson("$(devices_src)"); + "_idxes" slist => getindices("_dev[Devices]"); + + "_did[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][DeviceId])"; + "_name[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Name])"; + "_vend[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Vendor])"; + "_ver[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Version])"; + "_plug[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Plugin])"; + + "fwupd_dev[$(_did[$(_idxes)])]" + string => "$(_name[$(_idxes)]) | $(_vend[$(_idxes)]) | v$(_ver[$(_idxes)]) | [$(_plug[$(_idxes)])]", + meta => { "inventory", "attribute_name=Firmware devices" }, + if => not(strcmp("$(_name[$(_idxes)])", "")); +} + +bundle monitor inventory_fwupd_monitor +# @brief Track firmware metrics as time-series measurements. +# +# Reads key=value files written by inventory_fwupd_render and +# inventory_fwupd_hsi and emits three measurements: +# - firmware_devices_total (devices fwupd is tracking) +# - firmware_updates_available (devices with a pending update) +# - firmware_hsi_failing (HSI security checks failing) +{ + measurements: + linux:: + "$(sys.statedir)/fwupd_inventory_measurements" + handle => "firmware_devices_total", + stream_type => "file", + data_type => "int", + history_type => "weekly", + units => "devices", + match_value => inventory_fwupd_kv("firmware_devices_total"), + if => fileexists("$(sys.statedir)/fwupd_inventory_measurements"); + + "$(sys.statedir)/fwupd_inventory_measurements" + handle => "firmware_updates_available", + stream_type => "file", + data_type => "int", + history_type => "weekly", + units => "devices", + match_value => inventory_fwupd_kv("firmware_updates_available"), + if => fileexists("$(sys.statedir)/fwupd_inventory_measurements"); + + "$(sys.statedir)/fwupd_measurements_hsi" + handle => "firmware_hsi_failing", + stream_type => "file", + data_type => "int", + history_type => "weekly", + units => "checks", + match_value => inventory_fwupd_kv("firmware_hsi_failing_attributes"), + if => fileexists("$(sys.statedir)/fwupd_measurements_hsi"); +} + +body match_value inventory_fwupd_kv(key) +{ + select_line_matching => "$(key)=.*"; + extraction_regex => "$(key)=(\d+)"; +} + +bundle agent __main__ +{ + methods: + "main" usebundle => inventory_fwupd_main; +} From 950573f7dfcecae4d0a7df8dd1241f625e3aa917 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 25 May 2026 18:02:43 -0500 Subject: [PATCH 106/146] Added manage-fwupd module for firmware updates with allow-list and deferred reboot --- management/manage-fwupd/README.org | 111 ++++++++++ management/manage-fwupd/input.json | 33 +++ management/manage-fwupd/main.cf | 196 ++++++++++++++++++ .../mp-inventory-fwupd-status.png | Bin 0 -> 52939 bytes 4 files changed, 340 insertions(+) create mode 100644 management/manage-fwupd/README.org create mode 100644 management/manage-fwupd/input.json create mode 100644 management/manage-fwupd/main.cf create mode 100644 management/manage-fwupd/mp-inventory-fwupd-status.png diff --git a/management/manage-fwupd/README.org b/management/manage-fwupd/README.org new file mode 100644 index 00000000..3bbd59ba --- /dev/null +++ b/management/manage-fwupd/README.org @@ -0,0 +1,111 @@ +Ensure fwupd is installed and its metadata refresh timer is enabled. +Optionally apply firmware updates for devices matching an allow-list. + +Depends on *inventory-fwupd*, which surfaces firmware versions, +pending updates, and HSI security attributes as inventory. This module +reads the updates cache that inventory-fwupd maintains rather than +calling fwupdmgr itself, avoiding duplicate work. + +* Inputs + +All three inputs live in the =manage_fwupd:allowed= bundle and can be +set via ~cfbs input~ (interactive), augments (=def.json=), CMDB, or +host-specific data. The variable names are: + +| Variable | Type | Default | +|--------------------------------------------+--------------------------+---------------| +| =manage_fwupd:allowed.apply_updates= | string (class expression) | =!any= | +| =manage_fwupd:allowed.device_name_reglist= | list (pcre patterns) | ={}= (empty) | +| =manage_fwupd:allowed.reboot_after_update= | string (class expression) | =!any= | + +*Augments example* (=def.json=): + +#+begin_src json +{ + "variables": { + "manage_fwupd:allowed.apply_updates": { + "value": "any" + }, + "manage_fwupd:allowed.device_name_reglist": { + "value": ["UEFI dbx", "System Firmware"] + }, + "manage_fwupd:allowed.reboot_after_update": { + "value": "Night" + } + } +} +#+end_src + +** Apply firmware updates + +A CFEngine class expression controlling when firmware updates are applied. +The default is =!any=, which means updates are never applied until you +explicitly opt in by changing this value. + +Examples: + +| Expression | Meaning | +|-------------------------------------------------+-----------------------------------------------------| +| =!any= | Disabled (default) -- never apply firmware updates | +| =any= | Apply on all hosts, always | +| =linux= | Apply on all Linux hosts | +| =(env_dev\vert{}env_qa).Night.(cohort_A\vert{}cohort_C)= | Dev/QA environments, at night, for cohorts A and C | +| =Hr04.Min00_05= | All hosts, but only during a 4:00--4:05 AM window | +| =laptop_updates_enabled= | Only hosts where you've defined this custom class | + +Use =.= for AND, =|= for OR, =!= for NOT, and parentheses for grouping. +These are CFEngine class expressions, not regular expressions. + +** Allowed devices + +A list of device name patterns (regular expressions) controlling which +devices are eligible for firmware updates. Even when =apply_updates= is +enabled, only devices whose name matches an entry in this list will be +updated. + +Use =.*= to match all devices. Use specific patterns to target individual +hardware, e.g. =UEFI dbx=, =System Firmware=, =TPM=. + +** Reboot after update + +A CFEngine class expression controlling when the host reboots after a +firmware update is applied. The default is =!any= (disabled). Some firmware +updates (e.g. UEFI capsule) only activate after a reboot. + +When enabled, the module waits for cf-agent to finish before rebooting. +On systemd systems this uses a transient service that polls for cf-agent's +PID to exit, then reboots immediately. On non-systemd systems it falls +back to ~shutdown -r +1~. + +* Mission Portal + +The module reports =fwupd management status= as an inventory attribute +visible in Mission Portal's inventory reports: + +[[file:mp-inventory-fwupd-status.png]] + +* Behavior + +1. *Always:* Ensures the =fwupd= package is installed +2. *Always:* Ensures =fwupd-refresh.timer= is enabled (keeps the LVFS firmware catalog current) +3. *When =apply_updates= class resolves true AND a device matches the allow-list:* + Runs ~fwupdmgr update --no-reboot-check ~ for each matching device. + A marker file prevents the same update from re-executing within the + same boot cycle. +4. *When =reboot_after_update= resolves true AND a firmware update was applied:* + Schedules a deferred reboot after cf-agent exits. + +The =--no-reboot-check= flag suppresses the interactive reboot prompt. +It does not skip or defer the update itself. + +* Requirements + +Linux only. The =fwupd= package must be available in the system's +package repositories. + +* Warnings + +Firmware updates carry inherent risk (bricking, required reboots, AC +power requirements). Use the allow-list to limit updates to +well-understood device classes. Test in a non-production environment +first. diff --git a/management/manage-fwupd/input.json b/management/manage-fwupd/input.json new file mode 100644 index 00000000..5a4d05cb --- /dev/null +++ b/management/manage-fwupd/input.json @@ -0,0 +1,33 @@ +[ + { + "type": "string", + "variable": "apply_updates", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Apply firmware updates", + "question": "Class expression for when firmware updates should be applied (e.g. 'any', 'linux', '(env_dev|env_qa).Night.(cohort_A|cohort_C)'). Default '!any' means disabled.", + "default": "!any" + }, + { + "type": "list", + "variable": "device_name_reglist", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Allowed devices", + "subtype": { + "type": "string", + "label": "Device name pattern", + "question": "Device name (regex) to allow firmware updates for" + }, + "while": "Do you want to allow firmware updates for more devices?" + }, + { + "type": "string", + "variable": "reboot_after_update", + "namespace": "manage_fwupd", + "bundle": "allowed", + "label": "Reboot after firmware update", + "question": "Class expression for when to reboot after a firmware update is applied (e.g. 'any', 'Night', 'Hr04.Min00_05'). Default '!any' means disabled (no automatic reboot).", + "default": "!any" + } +] diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf new file mode 100644 index 00000000..a164ba2d --- /dev/null +++ b/management/manage-fwupd/main.cf @@ -0,0 +1,196 @@ +body file control +{ + namespace => "manage_fwupd"; +@if minimum_version(3.27) + evaluation_order => "top_down"; +@endif +} + +bundle agent allowed +# @brief Default values for firmware update configuration. +# Override apply_updates, device_name_reglist, and reboot_after_update +# via CMDB or augments to control firmware update behavior. +{ + vars: + "apply_updates" + string => "!any", + if => not(isvariable("apply_updates")), + comment => "Default disabled; set to a class expression to enable firmware updates"; + + "device_name_reglist" + slist => {}, + if => not(isvariable("device_name_reglist")), + comment => "Default empty allow-list; override via CMDB or augments"; + + "reboot_after_update" + string => "!any", + if => not(isvariable("reboot_after_update")), + comment => "Default disabled; set to a class expression to reboot after firmware update"; +} + +bundle agent main +# @brief Ensure fwupd is installed, its refresh timer is enabled, and +# optionally apply firmware updates for devices matching an allow-list. +# +# Depends on inventory-fwupd, which provides: +# - inventory_fwupd_main.fwupdmgr (path to fwupdmgr binary) +# - default:have_fwupdmgr (class: fwupdmgr is present) +# - updates cache at $(sys.statedir)/fwupd_inventory_updates.json +# +# Firmware updates are applied when the apply_updates class expression resolves +# true AND a device name matches at least one pcre regular expression defined in +# manage_fwupd:allowed.device_name_reglist. +# +# A marker file ($(sys.statedir)/fwupd_update_applied) prevents the +# same update from re-executing on subsequent agent runs within the +# same boot cycle. The marker's mtime is compared against /proc/1 +# (boot time) via isnewerthan(). Any marker remaining from a prior boot +# is deleted so the update can re-evaluate after reboot. +# +# When reboot_after_update resolves true the marker is removed and +# a deferred reboot is scheduled. On systemd systems a transient +# service polls for cf-agent to exit then reboots immediately; on +# non-systemd systems the legacy shutdown -r +1 fallback is used. +# +# --no-reboot-check suppresses the interactive "reboot now?" prompt +# that fwupdmgr emits after certain updates. It does not skip or +# defer the update itself. +# +# @inventory fwupd management status - INSTALLED, NOT_INSTALLED, or NOT_SUPPORTED +{ + vars: + linux:: + "_update_marker" + string => "$(sys.statedir)/fwupd_update_applied"; + + "_systemd_run" + string => ifelse(isexecutable("/bin/systemd-run"), "/bin/systemd-run", + isexecutable("/usr/bin/systemd-run"), "/usr/bin/systemd-run", + ""), + comment => "Path to systemd-run when available (empty string otherwise)"; + + linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: + "_updates_cache" + string => "$(sys.statedir)/fwupd_inventory_updates.json"; + + "_updates" + data => readjson("$(_updates_cache)"), + if => fileexists("$(_updates_cache)"); + + "_dev_idx" + slist => getindices("_updates[Devices]"), + if => isvariable("_updates"); + + "_dev_name[$(_dev_idx)]" + string => "$(_updates[Devices][$(_dev_idx)][Name])"; + + "_dev_id[$(_dev_idx)]" + string => "$(_updates[Devices][$(_dev_idx)][DeviceId])"; + + classes: + linux:: + "apply_updates" + scope => "namespace", + meta => { "report" }, + expression => "$(manage_fwupd:allowed.apply_updates)", + comment => "Opt-in via class expression (default '!any' = disabled)"; + + "reboot_after_update" + scope => "namespace", + meta => { "report" }, + expression => "$(manage_fwupd:allowed.reboot_after_update)", + comment => "Opt-in reboot after firmware update (default '!any' = disabled)"; + + "_have_systemd_run" + expression => isgreaterthan(string_length("$(_systemd_run)"), 0), + comment => "True when systemd-run is available for deferred reboot"; + + "_update_applied" + expression => and( + fileexists("$(_update_marker)"), + isnewerthan("$(_update_marker)", "/proc/1") + ), + comment => "A firmware update was applied during this boot cycle"; + + linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: + "_device_allowed_$(_dev_idx)" + expression => regcmp("$(manage_fwupd:allowed.device_name_reglist)", "$(_dev_name[$(_dev_idx)])"), + comment => "Device $(_dev_name[$(_dev_idx)]) matches allow-list entry"; + + packages: + linux:: + "fwupd" + policy => "present", + comment => "Ensure fwupd is installed for firmware inventory and management"; + + services: + linux.default:have_fwupdmgr:: + "fwupd-refresh.timer" + service_policy => "enabled", + comment => "Ensure the fwupd metadata refresh timer is enabled so the LVFS firmware catalog stays current"; + + # --- Stale marker cleanup ---------------------------------------- + files: + linux:: + "$(_update_marker)" + delete => default:tidy, + if => not("_update_applied"), + comment => "Remove stale marker from a previous boot cycle"; + + # --- Apply firmware updates -------------------------------------- + commands: + linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: + "$(default:inventory_fwupd_main.fwupdmgr)" + arglist => { "update", "--no-reboot-check", "$(_dev_id[$(_dev_idx)])" }, + handle => canonify("manage_fwupd_apply_device_$(_dev_idx)_$(_dev_name[$(_dev_idx)])"), + if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), + classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), + comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; + + # --- Post-update marker and reboot -------------------------------- + files: + linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: + "$(_update_marker)" + create => "true", + if => "manage_fwupd_update_$(_dev_idx)_repaired", + comment => "Record that a firmware update was applied this boot cycle (prevents re-execution)"; + + manage_fwupd:_update_applied.manage_fwupd:reboot_after_update:: + "$(_update_marker)" + delete => default:tidy, + comment => "Clear marker before issuing reboot"; + + commands: + manage_fwupd:_update_applied.manage_fwupd:reboot_after_update.manage_fwupd:_have_systemd_run:: + "$(_systemd_run)" + arglist => { + "--unit=cfengine-fwupd-reboot", + "--description=Reboot after cf-agent exits for firmware update", + "/bin/bash", "-c", + "while kill -0 $(this.promiser_pid) 2>/dev/null; do sleep 1; done; /sbin/shutdown -r now 'Rebooting for firmware update applied by CFEngine'" + }, + handle => "manage_fwupd_reboot_after_update", + comment => "Deferred reboot: polls for cf-agent to exit, then reboots immediately"; + + manage_fwupd:_update_applied.manage_fwupd:reboot_after_update.!manage_fwupd:_have_systemd_run:: + "/sbin/shutdown" + arglist => { "-r", "+1", "Rebooting for firmware update applied by CFEngine" }, + handle => "manage_fwupd_reboot_after_update_fallback", + comment => "Fallback reboot for non-systemd systems (shutdown -r +1)"; + + # --- Inventory -------------------------------------------------- + vars: + linux.default:have_fwupdmgr:: + "_status" string => "INSTALLED"; + + linux.!default:have_fwupdmgr:: + "_status" string => "NOT_INSTALLED"; + + !linux:: + "_status" string => "NOT_SUPPORTED"; + + any:: + "inventory_status" + string => "$(_status)", + meta => { "inventory", "attribute_name=fwupd management status" }; +} diff --git a/management/manage-fwupd/mp-inventory-fwupd-status.png b/management/manage-fwupd/mp-inventory-fwupd-status.png new file mode 100644 index 0000000000000000000000000000000000000000..e2f702d37ffa2687a8c23d1bc27c0352cedefba6 GIT binary patch literal 52939 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 literal 79973 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 Date: Wed, 12 Aug 2026 20:25:01 -0500 Subject: [PATCH 138/146] Removed the stale manage-fwupd inventory screenshot Changelog: title --- management/manage-fwupd/README.org | 4 +--- .../manage-fwupd/mp-inventory-fwupd-status.png | Bin 52939 -> 0 bytes 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 management/manage-fwupd/mp-inventory-fwupd-status.png diff --git a/management/manage-fwupd/README.org b/management/manage-fwupd/README.org index 009fd20c..8436cfe7 100644 --- a/management/manage-fwupd/README.org +++ b/management/manage-fwupd/README.org @@ -93,9 +93,7 @@ first. * Mission Portal The module reports =fwupd management status= as an inventory attribute -visible in Mission Portal's inventory reports: - -[[https://raw.githubusercontent.com/cfengine/modules/master/management/manage-fwupd/mp-inventory-fwupd-status.png]] +visible in Mission Portal's inventory reports. * Behavior diff --git a/management/manage-fwupd/mp-inventory-fwupd-status.png b/management/manage-fwupd/mp-inventory-fwupd-status.png deleted file mode 100644 index e2f702d37ffa2687a8c23d1bc27c0352cedefba6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52939 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

Gi$^Kvt)z)saTzdei$L`=yEj8YDE7WZn#if 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 g*wpQ-v)>IIP$}zEFR->V@C}Z Date: Wed, 19 Aug 2026 17:07:43 -0500 Subject: [PATCH 139/146] Added Makefile to replace much of ci.yml workflow for local ease of use Ticket: none Changelog: none --- .github/workflows/ci.yml | 22 ++++++---------------- Makefile | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7572f7cb..23d75c23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.14" - - name: Install python tools - run: pipx install flake8 pyright black pyflakes pytest cfbs cfengine - - name: Check the status with cfbs - run: cfbs status - - name: Validate with cfbs - run: cfbs validate - - name: Check the formatting - run: cfbs --check pretty ./cfbs.json - - name: Linting python code - run: ./ci/linting.sh - - name: Run promise type tests - run: pytest promise-types/ -v - - name: Lint policy with CFEngine CLI - run: cfengine lint --strict no ./ - - name: Check formatting with CFEngine CLI - run: cfengine format --check + - name: Install CFEngine community for tests + run: | + pipx install cf-remote + cf-remote install --clients localhost --edition community + - name: Run make check for linting, formatting and tests + run: make check diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2eabceed --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: check lint format all clean tools + +all: tools clean lint format check + +tools: + sudo pipx install --global flake8 pyright black pyflakes pytest cfbs cfengine + echo "deploy test requires cf-agent installed..." + command -v cf-agent + +clean: + rm -rf tests/deploy/out + +lint: clean tools + cfbs status + cfbs validate + cfbs --check pretty ./cfbs.json + ./ci/linting.sh + cfengine lint --strict no ./ + +format: lint + cfengine format --check + +check: format + pytest promise-types/ -v From e0504ff3f7971e8a18463d8ee20531c53438f0e8 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 09:50:08 -0500 Subject: [PATCH 140/146] ENT-14328: Added simple test to add all modules and ensure the build works with provides from cfbs.json --- .gitignore | 2 ++ Makefile | 1 + tests/deploy/00-promise-run-no-errors.sh | 9 ++++++ tests/deploy/deploy.sh | 39 ++++++++++++++++++++++++ tests/deploy/test.sh | 7 +++++ 5 files changed, 58 insertions(+) create mode 100755 tests/deploy/00-promise-run-no-errors.sh create mode 100755 tests/deploy/deploy.sh create mode 100755 tests/deploy/test.sh diff --git a/.gitignore b/.gitignore index e5e89d59..8bfd5e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ node_modules/ build/ dist/ .cache +tests/deploy/cfbs.json +tests/deploy/out diff --git a/Makefile b/Makefile index 2eabceed..69f1f719 100644 --- a/Makefile +++ b/Makefile @@ -22,3 +22,4 @@ format: lint check: format pytest promise-types/ -v + bash tests/deploy/test.sh diff --git a/tests/deploy/00-promise-run-no-errors.sh b/tests/deploy/00-promise-run-no-errors.sh new file mode 100755 index 00000000..53291da5 --- /dev/null +++ b/tests/deploy/00-promise-run-no-errors.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# assumes deploy.sh has already run adjacent to this file + +set -ex +sudo cf-agent -KI > log +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi diff --git a/tests/deploy/deploy.sh b/tests/deploy/deploy.sh new file mode 100755 index 00000000..9744b2b3 --- /dev/null +++ b/tests/deploy/deploy.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -ex + +echo "WARNING/TODO: This test, $0, requires all changes to be pushed up to a branch in order to test the latest. This could be improved if cfbs add could work with the modules repo and a cfbs.json file. See ENT-14421" +if [ -n "$GITHUB_HEAD_REF" ]; then + # github case + BRANCH="$GITHUB_HEAD_REF" +else + # local case + BRANCH=$(git rev-parse --abbrev-ref HEAD) +fi + +# workaround, if git@ url then we get Error: Cannot specify more than one version of the same module +# so transform the remote url +REPO=$(git remote get-url origin | \ + sed -e 's,git@,https://,' \ + -e 's,com:,com/,' \ +) + +if [ -n "$GITHUB_HEAD_REF" ]; then + REPO="$(echo "$REPO" | sed -e "s,cfengine,$GITHUB_TRIGGERING_ACTOR,")" +fi + +thisdir="$(dirname "$0")" +cd "$thisdir" +[ -d .git ] && rm -rf .git +[ -f cfbs.json ] && rm cfbs.json +cfbs --version +cfbs init --non-interactive +cfbs --non-interactive add "$REPO@$BRANCH" +cfbs build +sudo cfbs install +sudo cf-agent -IB 127.0.0.1 > log + +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi + diff --git a/tests/deploy/test.sh b/tests/deploy/test.sh new file mode 100755 index 00000000..9d37d240 --- /dev/null +++ b/tests/deploy/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -ex +thisdir="$(dirname "$0")" +bash "$thisdir"/deploy.sh +if ls "$thisdir"/0*.sh >/dev/null; then + bash "$thisdir"/0*.sh +fi From d251828b5c8ade5f5bad0af07f04a45ab0602122 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 09:52:07 -0500 Subject: [PATCH 141/146] Fixed typo in delete-home-dotshosts cfbs.json meta data --- cfbs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfbs.json b/cfbs.json index 9864d8b5..28a13649 100644 --- a/cfbs.json +++ b/cfbs.json @@ -144,7 +144,7 @@ }, "delete-home-dotshosts": { "description": "Ensure that ~/.shosts files are not present, as they present a security risk.", - "subdirectory": "security/delete-home-dotshots", + "subdirectory": "security/delete-home-dotshosts", "steps": [ "copy policy/main.cf services/cfbs/delete-home-dotshots/", "policy_files services/cfbs/delete-home-dotshots/", From 9b91840aff8666cdd932196e9c19f5de481a87be Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 09:53:45 -0500 Subject: [PATCH 142/146] Fixed typo in uninstall-packages module meta data in cfbs.json --- cfbs.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfbs.json b/cfbs.json index 28a13649..1a581dc3 100644 --- a/cfbs.json +++ b/cfbs.json @@ -497,10 +497,10 @@ "description": "Allows you to specify a list of packages you want uninstalled on your hosts.", "subdirectory": "security/uninstall-packages", "steps": [ - "copy uninstall-packages.cf services/cfbs/modules/uninstall-packages/uninstall-packages.cf", + "copy main.cf services/cfbs/modules/uninstall-packages/main.cf", "input uninstall-packages/input.json def.json", "bundles uninstall_packages:uninstall_packages", - "policy_files services/cfbs/modules/uninstall-packages/uninstall-packages.cf" + "policy_files services/cfbs/modules/uninstall-packages/main.cf" ], "input": [ { From 4828d095fb476e9e1d034d2a522afca2bf5ad12e Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 10:19:28 -0500 Subject: [PATCH 143/146] Fixed inventory-smartctl policy to properly guard against smartctl not being installed --- inventory/inventory-smartctl/policy.cf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/inventory/inventory-smartctl/policy.cf b/inventory/inventory-smartctl/policy.cf index 7aae3302..2358b111 100644 --- a/inventory/inventory-smartctl/policy.cf +++ b/inventory/inventory-smartctl/policy.cf @@ -33,9 +33,12 @@ bundle agent main ); "_sdir" string => "$(sys.statedir)"; - "_cache_ttl" string => "3600"; - # 1 hour + "_cache_ttl" + string => "3600", + comment => "3600 seconds is 1 hour"; + + linux._have_smartctl:: # Enumerate drives - extract first field from each line of smartctl --scan "_scan_lines" slist => splitstring( @@ -52,7 +55,6 @@ bundle agent main "_cache[${_drives}]" string => "$(_sdir)/inventory_smartctl_${_id[${_drives}]}.json"; - linux._have_smartctl:: # Rolled-up status: OK or DEGRADED (SMARTCTL_MISSING when smartctl absent) "smartctl_status" string => "DEGRADED", From c402ded750b631e2ff3e0be6ab9046e382c7d3bb Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 10:21:29 -0500 Subject: [PATCH 144/146] Added install-ansible module Added shell deploy test to ensure pipx and ansible are installed according to specific classes for minimal vs full install and specific version of ansible. Ticket: ENT-14328 --- cfbs.json | 9 +++ software/install-ansible.cf | 58 +++++++++++++++++++ tests/deploy/01-install-ansible-full-test.sh | 20 +++++++ .../deploy/02-install-ansible-minimal-test.sh | 16 +++++ .../03-install-ansible-versions-test.sh | 55 ++++++++++++++++++ .../deploy/04-install-ansible-default-test.sh | 20 +++++++ tests/deploy/test.sh | 4 +- 7 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 software/install-ansible.cf create mode 100755 tests/deploy/01-install-ansible-full-test.sh create mode 100644 tests/deploy/02-install-ansible-minimal-test.sh create mode 100644 tests/deploy/03-install-ansible-versions-test.sh create mode 100755 tests/deploy/04-install-ansible-default-test.sh diff --git a/cfbs.json b/cfbs.json index 1a581dc3..5f263a43 100644 --- a/cfbs.json +++ b/cfbs.json @@ -546,6 +546,15 @@ "bundles windows_capability" ] }, + "install-ansible": { + "description": "Optionally install ansible.", + "subdirectory": "software", + "steps": [ + "copy install-ansible.cf services/cfbs/software/install-ansible.cf", + "policy_files services/cfbs/software/install-ansible.cf", + "bundles install_ansible" + ] + }, "windows-openssh-server": { "description": "Optionally install Windows OpenSSH Server.", "subdirectory": "software/windows", diff --git a/software/install-ansible.cf b/software/install-ansible.cf new file mode 100644 index 00000000..f270da3b --- /dev/null +++ b/software/install-ansible.cf @@ -0,0 +1,58 @@ +# Steps here based on this guide: https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html +# +# host specific data supported, in the data bundle context +# +# data:install_ansible: class, if defined, install ansible +# data:ansible_minimal_install: class, if defined, minimal install aka ansible-core +# data:ansible_full_install: class, if defined, full install. This is the default if neither minimal or full classes are defined. +bundle agent install_ansible +{ + methods: + linux.data:install_ansible:: + "install_ansible_main"; + + reports: + windows:: + "Use WSL to install Ansible on Windows. See https://blog.rolpdog.com/2020/03/why-no-ansible-controller-for-windows.html"; + + !linux.!windows:: + "Installing Ansible on a non-Linux/non-Windows host is not supported"; +} + +bundle agent install_ansible_main +{ + classes: + !windows:: + "ansible_installed" + expression => returnszero("command -v ansible >/dev/null", "useshell"); + + vars: + "install_ansible_command" + string => "${base_command}==${data:install_ansible.version}", + if => isvariable("data:install_ansible.version"); + + "install_ansible_command" + string => "${base_command}", + if => not(isvariable("data:install_ansible.version")); + + data:ansible_minimal_install:: + "base_command" string => "pipx install --global ansible-core"; + + data:ansible_full_install|(!data:ansible_full_install.!data:ansible_minimal_install):: + "base_command" string => "pipx install --global --include-deps ansible"; + + packages: + "pipx" classes => if_ok("pipx_installed"); + + commands: + pipx_installed.!ansible_installed:: + "${install_ansible_command}" + contain => in_shell_umask(022), + comment => "The default umask of 077 would create /opt/pipx/venvs/ansible which is inaccessible to everyone but root, so use 022 instead."; +} + +body contain in_shell_umask(umask) +{ + useshell => "true"; + umask => "${umask}"; +} diff --git a/tests/deploy/01-install-ansible-full-test.sh b/tests/deploy/01-install-ansible-full-test.sh new file mode 100755 index 00000000..29acb7b5 --- /dev/null +++ b/tests/deploy/01-install-ansible-full-test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# assumes deploy.sh has already run adjacent to this file + +set -ex + +sudo pipx uninstall --global ansible || true +sudo pipx uninstall --global ansible-core || true + +sudo cf-agent -Kd -Ddata:install_ansible -Ddata:ansible_full_install --bundle install_ansible > log +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi + +ansible --version +echo "expect that ansible-core is not installed via pipx list" +if ( sudo pipx list --global --short | grep ansible-core ); then + echo "expected only ansible to be installed, but found ansible-core" + exit 1 +fi diff --git a/tests/deploy/02-install-ansible-minimal-test.sh b/tests/deploy/02-install-ansible-minimal-test.sh new file mode 100644 index 00000000..1386973d --- /dev/null +++ b/tests/deploy/02-install-ansible-minimal-test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -ex + +sudo pipx uninstall --global ansible || true +sudo pipx uninstall --global ansible-core || true + +sudo cf-agent -Kd -Ddata:install_ansible -Ddata:ansible_minimal_install --bundle install_ansible > log +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi + +ansible --version +if ! ( sudo pipx list --global --short | grep ansible-core ); then + echo "expected ansible-core to be installed but did not find that in pipx list output" +fi diff --git a/tests/deploy/03-install-ansible-versions-test.sh b/tests/deploy/03-install-ansible-versions-test.sh new file mode 100644 index 00000000..c777563b --- /dev/null +++ b/tests/deploy/03-install-ansible-versions-test.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -ex + +sudo pipx uninstall --global ansible || true +sudo pipx uninstall --global ansible-core || true + +function cleanup +{ + sudo rm -f /var/cfengine/data/host_specific.json || true # to make sure any version data is removed for other tests other runs +} + +trap cleanup EXIT + +good_version="2.21.3" + +sudo mkdir -p /var/cfengine/data +cat < log + +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi + +ansible --version | grep "${good_version}" + +sudo pipx uninstall --global ansible || true +sudo pipx uninstall --global ansible-core || true + +bad_version="x.y.z" +cat < log + +if grep 'error:' log; then + echo "Expected error found due to bad version value. Test passes." + grep 'error:' log + exit 0 +fi + +echo "Expected errors when installing ansible with bad version" +exit 1 diff --git a/tests/deploy/04-install-ansible-default-test.sh b/tests/deploy/04-install-ansible-default-test.sh new file mode 100755 index 00000000..9ecd442e --- /dev/null +++ b/tests/deploy/04-install-ansible-default-test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# assumes deploy.sh has already run adjacent to this file + +set -ex + +sudo pipx uninstall --global ansible || true +sudo pipx uninstall --global ansible-core || true + +sudo cf-agent -Kd -Ddata:install_ansible --bundle install_ansible > log +if grep 'error:' log; then + grep 'error:' log + exit 1 +fi + +ansible --version +echo "expect that ansible(full) is installed and not ansible-core" +if ( sudo pipx list --global --short | grep ansible-core ); then + echo "expected ansible(full) to be installed, but found ansible-core" + exit 1 +fi diff --git a/tests/deploy/test.sh b/tests/deploy/test.sh index 9d37d240..3c8d9c4e 100755 --- a/tests/deploy/test.sh +++ b/tests/deploy/test.sh @@ -3,5 +3,7 @@ set -ex thisdir="$(dirname "$0")" bash "$thisdir"/deploy.sh if ls "$thisdir"/0*.sh >/dev/null; then - bash "$thisdir"/0*.sh + for test in "$thisdir"/0*.sh; do + bash -ex "$test" + done fi From 80ba276023fc9610a3bcaefa3a19336f1604e892 Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Thu, 20 Aug 2026 16:26:43 -0500 Subject: [PATCH 145/146] ent-14328: organize install-ansible module into a directory to enable the required README for build-index --- cfbs.json | 6 +++--- software/install-ansible/README.md | 15 +++++++++++++++ software/{ => install-ansible}/install-ansible.cf | 0 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 software/install-ansible/README.md rename software/{ => install-ansible}/install-ansible.cf (100%) diff --git a/cfbs.json b/cfbs.json index 5f263a43..372655b5 100644 --- a/cfbs.json +++ b/cfbs.json @@ -548,10 +548,10 @@ }, "install-ansible": { "description": "Optionally install ansible.", - "subdirectory": "software", + "subdirectory": "software/install-ansible", "steps": [ - "copy install-ansible.cf services/cfbs/software/install-ansible.cf", - "policy_files services/cfbs/software/install-ansible.cf", + "copy install-ansible.cf services/cfbs/software/install-ansible/install-ansible.cf", + "policy_files services/cfbs/software/install-ansible/install-ansible.cf", "bundles install_ansible" ] }, diff --git a/software/install-ansible/README.md b/software/install-ansible/README.md new file mode 100644 index 00000000..d5f72428 --- /dev/null +++ b/software/install-ansible/README.md @@ -0,0 +1,15 @@ +This module ensures that Ansible is installed on a system. + +## Configuration + +There are three classes which can be used to specify whether to install ansible and which version: minimal or full. + +- `data:install_ansible` - if defined, install ansible (default is full install) +- `data:ansible_minimal_install` - if defined, install minimal version aka ansible-core +- `data:ansible_full_install` - if defined, install full version. + +Additionally it is possible to request a specific version other than the latest/default by way of a variable. + +- `data:install_ansible.version` - if this variable is defined then a specific version will be installed + +Note that the system in question must have a `pipx` package available through the default package manager as well as python installed which should come along as a dependency of `pipx`. diff --git a/software/install-ansible.cf b/software/install-ansible/install-ansible.cf similarity index 100% rename from software/install-ansible.cf rename to software/install-ansible/install-ansible.cf From f3ff07e31a4636a1ec114351cac510546c8a943e Mon Sep 17 00:00:00 2001 From: Craig Comstock Date: Fri, 21 Aug 2026 13:05:51 -0500 Subject: [PATCH 146/146] ent-14328: enable using any of the three ansible install options: default, minimal or full The documentation in README.md was not clear and the policy did not reflect the intention that any ONE of these three classes would cause an install. --- software/install-ansible/install-ansible.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/install-ansible/install-ansible.cf b/software/install-ansible/install-ansible.cf index f270da3b..b705fb68 100644 --- a/software/install-ansible/install-ansible.cf +++ b/software/install-ansible/install-ansible.cf @@ -8,7 +8,7 @@ bundle agent install_ansible { methods: - linux.data:install_ansible:: + linux.(data:install_ansible|data:ansible_minimal_install|data:ansible_full_install):: "install_ansible_main"; reports:

Gi$^Kvt)z)saTzdei$L`=yEj8YDE7WZn#if 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 g*wpQ-v)>IIP$}zEFR->V@C}Z Date: Mon, 25 May 2026 18:02:50 -0500 Subject: [PATCH 107/146] Added compliance-report-fwupd for firmware security posture via fwupd HSI --- reporting/compliance-report-fwupd/README.org | 137 +++ .../fwupd-compliance-report.json | 895 ++++++++++++++++++ .../mp-compliance-report-fwupd.png | Bin 0 -> 75167 bytes 3 files changed, 1032 insertions(+) create mode 100644 reporting/compliance-report-fwupd/README.org create mode 100644 reporting/compliance-report-fwupd/fwupd-compliance-report.json create mode 100644 reporting/compliance-report-fwupd/mp-compliance-report-fwupd.png diff --git a/reporting/compliance-report-fwupd/README.org b/reporting/compliance-report-fwupd/README.org new file mode 100644 index 00000000..6a2e6122 --- /dev/null +++ b/reporting/compliance-report-fwupd/README.org @@ -0,0 +1,137 @@ +Compliance report definition for firmware security posture via fwupd. + +Imports a "Firmware Security (fwupd)" report into Mission Portal that +tracks Host Security Identifier (HSI) levels, fwupd installation, and +firmware update status across the fleet. + +The report contains 49 conditions: 7 rolled-up checks (HSI level +thresholds, fwupd installation, update status) and 42 per-attribute +checks covering every individual HSI firmware security test. + +* Mission Portal + +[[file:mp-compliance-report-fwupd.png]] + +* Requirements + +- *inventory-fwupd* module providing the inventory attributes and + platform classes (=fwupd_cpu_vendor_intel=, =fwupd_cpu_vendor_amd=, + =fwupd_oem_vendor_hp=) that the compliance conditions reference +- *compliance-report-imports* autorun bundle on the hub to import + the JSON definition into Mission Portal + +* Rolled-up Conditions + +| Condition | Category | Severity | +|-----------------------------------+--------------------+----------| +| *fwupd installed* | Firmware tooling | high | +| *HSI Level 1+ (Critical)* | HSI Level Overview | high | +| *HSI Level 2+ (Important)* | HSI Level Overview | medium | +| *HSI Level 3+ (Recommended)* | HSI Level Overview | low | +| *HSI Level 4 (Complete)* | HSI Level Overview | low | +| *No pending firmware updates* | Firmware updates | medium | +| *Firmware status healthy* | Firmware updates | medium | + +HSI level checks are cumulative thresholds -- a host at HSI:3 passes +the Level 1+, 2+, and 3+ conditions but fails Level 4. + +* Per-Attribute Conditions + +Each individual HSI firmware security check is a separate compliance +condition. The inventory attribute name follows the pattern +=Firmware HSI L: = with value =PASS= or =FAIL=. + +Per-attribute conditions use ~condition_for: "failing"~ -- a host is +only marked failing when the attribute is explicitly =FAIL=. Hosts +that don't report a given attribute (e.g., Intel-only checks on AMD +hardware, or VMs without HSI data) show as "not evaluated" rather +than failing. The checks are defined by the +[[https://fwupd.github.io/libfwupdplugin/hsi.html][fwupd HSI specification]]. + +** Level 1 -- Critical (19 conditions, severity: high) + +| Condition | Inventory attribute | Platform | +|------------------------------+--------------------------------------------+----------| +| UEFI SecureBoot | =Firmware HSI L1: UEFI secure boot= | All | +| TPM 2.0 Present | =Firmware HSI L1: TPM v2.0= | All | +| Empty PCR in TPM | =Firmware HSI L1: TPM empty PCRs= | All | +| UEFI Platform Key | =Firmware HSI L1: UEFI platform key= | All | +| BIOS Capsule Updates | =Firmware HSI L1: BIOS firmware updates= | All | +| Supported CPU | =Firmware HSI L1: Supported CPU= | All | +| UEFI BootService Variables | =Firmware HSI L1: UEFI bootservice variables= | All | +| BIOS Write Enable (BWE) | =Firmware HSI L1: SPI write= | Intel | +| BIOS Lock Enable (BLE) | =Firmware HSI L1: SPI lock= | Intel | +| SMM BIOS Write Protect | =Firmware HSI L1: SPI BIOS region= | Intel | +| Read-only SPI Descriptor | =Firmware HSI L1: SPI descriptor= | Intel | +| Platform Debug (Intel DCI) | =Firmware HSI L1: Platform debugging= | Intel | +| ME Manufacturing Mode | =Firmware HSI L1: csme manufacturing mode= | Intel | +| ME Flash Descriptor Override | =Firmware HSI L1: csme override= | Intel | +| ME BootGuard Platform Key | =Firmware HSI L1: MEI key manifest= | Intel | +| CSME Version | =Firmware HSI L1: CSME version= | Intel | +| Part is Fused | =Firmware HSI L1: Part is fused= | Intel | +| AMD Microcode Signature | =Firmware HSI L1: AMD microcode signature= | AMD | +| SMM Locked Down | =Firmware HSI L1: SMM locked down= | AMD | + +** Level 2 -- Important (12 conditions, severity: medium) + +| Condition | Inventory attribute | Platform | +|----------------------------+-------------------------------------------------+----------| +| DMA Protection (IOMMU) | =Firmware HSI L2: IOMMU= | All | +| PCR0 TPM Event Log | =Firmware HSI L2: TPM PCR0 reconstruction= | All | +| BIOS Rollback Protection | =Firmware HSI L2: BIOS rollback protection= | All | +| Intel BootGuard Enabled | =Firmware HSI L2: Intel BootGuard= | Intel | +| Intel BootGuard Verified | =Firmware HSI L2: Intel BootGuard verified boot= | Intel | +| Intel BootGuard ACM | =Firmware HSI L2: Intel BootGuard ACM protected= | Intel | +| Intel BootGuard OTP | =Firmware HSI L2: Intel BootGuard OTP fuse= | Intel | +| Part is Debug Locked | =Firmware HSI L2: Platform debugging= | Intel | +| Intel GDS Mitigation | =Firmware HSI L2: Intel GDS mitigation= | Intel | +| AMD Platform Secure Boot | =Firmware HSI L2: AMD platform secure boot= | AMD | +| AMD SPI Write Protections | =Firmware HSI L2: AMD SPI write protections= | AMD | +| HP SureStart | =Firmware HSI L2: HP SureStart= | HP | + +** Level 3 -- Recommended (8 conditions, severity: low) + +| Condition | Inventory attribute | Platform | +|------------------------------------+----------------------------------------------+----------| +| Suspend-to-Idle | =Firmware HSI L3: Suspend-to-idle= | All | +| Suspend to RAM Disabled | =Firmware HSI L3: Suspend-to-ram= | All | +| Pre-boot DMA Protection | =Firmware HSI L3: Pre-boot DMA protection= | All | +| CET Available | =Firmware HSI L3: CET Platform= | All | +| CET Utilized by OS | =Firmware HSI L3: CET OS Support= | All | +| Early-boot UEFI Memory Protections | =Firmware HSI L3: UEFI memory protections= | All | +| Intel BootGuard Policy | =Firmware HSI L3: Intel BootGuard error policy= | Intel | +| AMD SPI Replay Protections | =Firmware HSI L3: AMD SPI replay protections= | AMD | + +** Level 4 -- Complete (3 conditions, severity: low) + +| Condition | Inventory attribute | Platform | +|-------------------------------+--------------------------------------------+----------| +| DRAM Memory Encryption | =Firmware HSI L4: Encrypted RAM= | All | +| SMAP | =Firmware HSI L4: SMAP= | All | +| AMD Secure Processor Rollback | =Firmware HSI L4: AMD rollback protection= | AMD | + +* Categories + +| Category | Conditions | Scope | +|---------------------------+------------+----------------------------| +| HSI Level Overview | 4 | Rolled-up level thresholds | +| HSI Level 1 - Critical | 19 | Per-attribute checks | +| HSI Level 2 - Important | 12 | Per-attribute checks | +| HSI Level 3 - Recommended | 8 | Per-attribute checks | +| HSI Level 4 - Complete | 3 | Per-attribute checks | +| Firmware updates | 2 | Update status | +| Firmware tooling | 1 | fwupd installation | + +* Platform support + +Linux only. Platform-specific conditions use =host_filter= class +expressions so they only activate on relevant hardware: + +| Platform | =host_filter= | Conditions | Detection | +|----------+----------------------------+------------+--------------------------------| +| All | =linux= | 25 | -- | +| Intel | =fwupd_cpu_vendor_intel= | 17 | =/proc/cpuinfo= vendor_id | +| AMD | =fwupd_cpu_vendor_amd= | 6 | =/proc/cpuinfo= vendor_id | +| HP | =fwupd_oem_vendor_hp= | 1 | =/sys/class/dmi/id/sys_vendor= | + +These classes are defined by the *inventory-fwupd* module. diff --git a/reporting/compliance-report-fwupd/fwupd-compliance-report.json b/reporting/compliance-report-fwupd/fwupd-compliance-report.json new file mode 100644 index 00000000..d4dd0219 --- /dev/null +++ b/reporting/compliance-report-fwupd/fwupd-compliance-report.json @@ -0,0 +1,895 @@ +{ + "reports": { + "fwupd-firmware-security": { + "id": "fwupd-firmware-security", + "type": "compliance", + "title": "Firmware Security (fwupd)", + "conditions": [ + "fwupd:hsi-installed", + "fwupd:hsi-level-1", + "fwupd:hsi-level-2", + "fwupd:hsi-level-3", + "fwupd:hsi-level-4", + "fwupd:no-pending-updates", + "fwupd:status-ok", + "fwupd:hsi-l1-uefi-secure-boot", + "fwupd:hsi-l1-tpm-v2.0", + "fwupd:hsi-l1-tpm-empty-pcrs", + "fwupd:hsi-l1-uefi-platform-key", + "fwupd:hsi-l1-bios-firmware-updates", + "fwupd:hsi-l1-spi-write", + "fwupd:hsi-l1-spi-lock", + "fwupd:hsi-l1-spi-bios-region", + "fwupd:hsi-l1-spi-descriptor", + "fwupd:hsi-l1-supported-cpu", + "fwupd:hsi-l1-platform-debugging", + "fwupd:hsi-l1-csme-manufacturing-mode", + "fwupd:hsi-l1-csme-override", + "fwupd:hsi-l1-mei-key-manifest", + "fwupd:hsi-l1-csme-version", + "fwupd:hsi-l1-uefi-bootservice-vars", + "fwupd:hsi-l1-amd-microcode-signature", + "fwupd:hsi-l1-platform-fused", + "fwupd:hsi-l1-smm-locked-down", + "fwupd:hsi-l2-iommu", + "fwupd:hsi-l2-intel-bootguard", + "fwupd:hsi-l2-intel-bootguard-verified", + "fwupd:hsi-l2-intel-bootguard-acm", + "fwupd:hsi-l2-intel-bootguard-otp", + "fwupd:hsi-l2-tpm-pcr0-reconstruction", + "fwupd:hsi-l2-bios-rollback-protection", + "fwupd:hsi-l2-platform-debugging", + "fwupd:hsi-l2-intel-gds-mitigation", + "fwupd:hsi-l2-amd-platform-secure-boot", + "fwupd:hsi-l2-amd-spi-write-protections", + "fwupd:hsi-l2-hp-surestart", + "fwupd:hsi-l3-suspend-to-idle", + "fwupd:hsi-l3-suspend-to-ram", + "fwupd:hsi-l3-pre-boot-dma-protection", + "fwupd:hsi-l3-intel-bootguard-policy", + "fwupd:hsi-l3-cet-platform", + "fwupd:hsi-l3-amd-spi-replay-protections", + "fwupd:hsi-l3-uefi-memory-protections", + "fwupd:hsi-l4-encrypted-ram", + "fwupd:hsi-l4-smap", + "fwupd:hsi-l3-cet-os-support", + "fwupd:hsi-l4-amd-rollback-protection" + ] + } + }, + "conditions": { + "fwupd:hsi-installed": { + "id": "fwupd:hsi-installed", + "name": "fwupd installed", + "description": "Checks that fwupd is installed on the host.\n\nHosts without fwupd report a firmware update status of FWUPD_MISSING and cannot provide firmware security inventory. This is a prerequisite for all other firmware security checks.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware update status", + "operator": "not_match", + "value": "FWUPD_MISSING" + } + ], + "category": "Firmware tooling", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-level-1": { + "id": "fwupd:hsi-level-1", + "name": "HSI Level 1+ - Critical firmware protections", + "description": "Rolled-up check: the host achieves at least HSI Level 1 (Critical). See the individual Level 1 conditions for specific checks.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware HSI level", + "operator": "not_match", + "value": "HSI:0" + } + ], + "category": "HSI Level Overview", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-level-2": { + "id": "fwupd:hsi-level-2", + "name": "HSI Level 2+ - Important firmware protections", + "description": "Rolled-up check: the host achieves at least HSI Level 2 (Important). See the individual Level 2 conditions for specific checks.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware HSI level", + "operator": "regex_matches", + "value": "HSI:[2-4]" + } + ], + "category": "HSI Level Overview", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:hsi-level-3": { + "id": "fwupd:hsi-level-3", + "name": "HSI Level 3+ - Recommended firmware protections", + "description": "Rolled-up check: the host achieves at least HSI Level 3 (Recommended). See the individual Level 3 conditions for specific checks.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware HSI level", + "operator": "regex_matches", + "value": "HSI:[3-4]" + } + ], + "category": "HSI Level Overview", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-level-4": { + "id": "fwupd:hsi-level-4", + "name": "HSI Level 4 - Complete firmware protections", + "description": "Rolled-up check: the host achieves HSI Level 4 (Complete). See the individual Level 4 conditions for specific checks.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware HSI level", + "operator": "matches", + "value": "HSI:4" + } + ], + "category": "HSI Level Overview", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:no-pending-updates": { + "id": "fwupd:no-pending-updates", + "name": "No pending firmware updates", + "description": "Checks that the host has no outstanding firmware updates.\n\nHosts with pending firmware updates have known firmware versions that should be applied. Use the manage-fwupd module to automate firmware update application.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware update status", + "operator": "not_match", + "value": "UPDATES_AVAILABLE" + } + ], + "category": "Firmware updates", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:status-ok": { + "id": "fwupd:status-ok", + "name": "Firmware status healthy", + "description": "Checks that fwupd is present, has discoverable devices, and no pending updates.\n\nThis is the strictest firmware status check. A host passes only when fwupd is installed, reports at least one updatable device, and has no outstanding firmware updates.", + "type": "inventory", + "condition_for": "passing", + "rules": [ + { + "attribute": "Firmware update status", + "operator": "matches", + "value": "OK" + } + ], + "category": "Firmware updates", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:hsi-l1-uefi-secure-boot": { + "id": "fwupd:hsi-l1-uefi-secure-boot", + "name": "UEFI SecureBoot", + "description": "Secure Boot is enabled and functional.\n\nUEFI Secure Boot prevents unauthorized code from running during the boot process by verifying digital signatures of boot loaders and drivers.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: UEFI secure boot", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-tpm-v2.0": { + "id": "fwupd:hsi-l1-tpm-v2.0", + "name": "TPM 2.0 Present", + "description": "TPM 2.0 device exists and is enabled.\n\nA Trusted Platform Module provides hardware-backed cryptographic operations and secure measurement storage for verified boot.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: TPM v2.0", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-tpm-empty-pcrs": { + "id": "fwupd:hsi-l1-tpm-empty-pcrs", + "name": "Empty PCR in TPM", + "description": "All TPM PCRs 0-7 have valid measurements.\n\nEmpty Platform Configuration Registers indicate that the firmware boot process is not being measured, weakening the chain of trust.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: TPM empty PCRs", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-uefi-platform-key": { + "id": "fwupd:hsi-l1-uefi-platform-key", + "name": "UEFI Platform Key", + "description": "Valid production platform key is installed (not a test key).\n\nThe UEFI Platform Key (PK) is the root of trust for Secure Boot. A test key allows any code to be signed and run.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: UEFI platform key", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-bios-firmware-updates": { + "id": "fwupd:hsi-l1-bios-firmware-updates", + "name": "BIOS Capsule Updates", + "description": "Firmware update mechanism (UEFI capsule updates) is enabled.\n\nWhen disabled, the system cannot receive firmware security patches through the standard UEFI update mechanism.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: BIOS firmware updates", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-spi-write": { + "id": "fwupd:hsi-l1-spi-write", + "name": "BIOS Write Enable (BWE)", + "description": "SPI ROM is protected from userspace writes.\n\nThe BIOS Write Enable bit controls whether the SPI flash containing the BIOS can be written from the operating system. It should be disabled to prevent firmware tampering.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: SPI write", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-spi-lock": { + "id": "fwupd:hsi-l1-spi-lock", + "name": "BIOS Lock Enable (BLE)", + "description": "SMI protection on BIOSWE bit changes is enabled.\n\nBIOS Lock Enable generates a System Management Interrupt when software attempts to change the BIOS Write Enable bit, preventing silent firmware tampering.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: SPI lock", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-spi-bios-region": { + "id": "fwupd:hsi-l1-spi-bios-region", + "name": "SMM BIOS Write Protect", + "description": "BIOS region is non-writable outside System Management Mode.\n\nSMM BIOS Write Protect ensures that the BIOS SPI flash region cannot be modified except from within the trusted SMM environment.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: SPI BIOS region", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-spi-descriptor": { + "id": "fwupd:hsi-l1-spi-descriptor", + "name": "Read-only SPI Descriptor", + "description": "SPI flash descriptor region is locked.\n\nThe SPI descriptor defines access permissions for SPI flash regions. If unlocked, an attacker could modify these permissions to gain write access to the BIOS.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: SPI descriptor", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-supported-cpu": { + "id": "fwupd:hsi-l1-supported-cpu", + "name": "Supported CPU", + "description": "Platform CPU has HSI test support in fwupd.\n\nUnsupported CPUs cannot have their firmware security attributes fully evaluated.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: Supported CPU", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-platform-debugging": { + "id": "fwupd:hsi-l1-platform-debugging", + "name": "Platform Debug (Intel DCI)", + "description": "USB3 debugging interface (Intel DCI) is disabled.\n\nDirect Connect Interface allows deep hardware debugging over USB3. If enabled in production, it provides an attack surface for physical access exploits.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: Platform debugging", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-csme-manufacturing-mode": { + "id": "fwupd:hsi-l1-csme-manufacturing-mode", + "name": "ME Manufacturing Mode", + "description": "Intel Management Engine is not in manufacturing mode.\n\nManufacturing mode is intended for factory provisioning. If left enabled, it exposes additional attack surface on the ME subsystem.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: csme manufacturing mode", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-csme-override": { + "id": "fwupd:hsi-l1-csme-override", + "name": "ME Flash Descriptor Override", + "description": "Intel ME flash descriptor override (debug mode) is not accessible.\n\nThe override strap allows bypassing SPI flash access controls, enabling firmware tampering through physical access.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: csme override", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-mei-key-manifest": { + "id": "fwupd:hsi-l1-mei-key-manifest", + "name": "ME BootGuard Platform Key", + "description": "CPU fuses are not using leaked or compromised keys.\n\nThe MEI key manifest validates that the BootGuard keys burned into the CPU fuses have not been compromised.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: MEI key manifest", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-csme-version": { + "id": "fwupd:hsi-l1-csme-version", + "name": "CSME Version", + "description": "Intel Converged Security and Management Engine firmware is not vulnerable to known critical CVEs.\n\nOutdated CSME versions may contain exploitable vulnerabilities that compromise the hardware root of trust.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: CSME version", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-uefi-bootservice-vars": { + "id": "fwupd:hsi-l1-uefi-bootservice-vars", + "name": "UEFI BootService Variables", + "description": "Boot-only UEFI variables are not readable at OS runtime.\n\nBootService variables should be inaccessible after ExitBootServices() to prevent information leakage to the operating system.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: UEFI bootservice variables", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "linux" + }, + "fwupd:hsi-l1-amd-microcode-signature": { + "id": "fwupd:hsi-l1-amd-microcode-signature", + "name": "AMD Microcode Signature", + "description": "Firmware prevents loading of maliciously crafted CPU microcode.\n\nAMD processors vulnerable to microcode signature verification bypass (EntrySign) allow arbitrary microcode patches, undermining hardware security guarantees.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: AMD microcode signature", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_amd" + }, + "fwupd:hsi-l1-platform-fused": { + "id": "fwupd:hsi-l1-platform-fused", + "name": "Part is Fused", + "description": "Hardware one-time-programmable fuses are blown for tampering protection.\n\nProduction hardware should have security fuses blown to lock down debugging interfaces and enforce boot chain verification.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: Part is fused", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l1-smm-locked-down": { + "id": "fwupd:hsi-l1-smm-locked-down", + "name": "SMM Locked Down", + "description": "System Management Mode save state protection prevents data exfiltration.\n\nSMM lockdown prevents SMM handlers from reading or modifying OS memory, closing a class of privilege escalation attacks.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L1: SMM locked down", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 1 - Critical", + "severity": "high", + "host_filter": "fwupd_cpu_vendor_amd" + }, + "fwupd:hsi-l2-iommu": { + "id": "fwupd:hsi-l2-iommu", + "name": "DMA Protection (IOMMU)", + "description": "IOMMU is enabled for PCIe DMA attack prevention.\n\nThe IOMMU restricts DMA-capable devices to their assigned memory regions, preventing malicious peripherals from reading or writing arbitrary system memory.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: IOMMU", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:hsi-l2-intel-bootguard": { + "id": "fwupd:hsi-l2-intel-bootguard", + "name": "Intel BootGuard Enabled", + "description": "Intel BootGuard feature is activated.\n\nBootGuard provides hardware-rooted verified boot by validating the Initial Boot Block before execution.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Intel BootGuard", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-intel-bootguard-verified": { + "id": "fwupd:hsi-l2-intel-bootguard-verified", + "name": "Intel BootGuard Verified", + "description": "Intel BootGuard boot chain verification is functional.\n\nVerified Boot mode ensures each stage of the boot process is cryptographically validated before execution.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Intel BootGuard verified boot", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-intel-bootguard-acm": { + "id": "fwupd:hsi-l2-intel-bootguard-acm", + "name": "Intel BootGuard ACM", + "description": "Intel Authenticated Code Modules are protected.\n\nACMs are signed microcode modules executed by the CPU before any other code. Their integrity is essential for the hardware root of trust.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Intel BootGuard ACM protected", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-intel-bootguard-otp": { + "id": "fwupd:hsi-l2-intel-bootguard-otp", + "name": "Intel BootGuard OTP", + "description": "SOC is locked via one-time programmable fuses for BootGuard.\n\nOTP fuses permanently bind the BootGuard key hash to the CPU, preventing key substitution attacks.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Intel BootGuard OTP fuse", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-tpm-pcr0-reconstruction": { + "id": "fwupd:hsi-l2-tpm-pcr0-reconstruction", + "name": "PCR0 TPM Event Log", + "description": "TPM PCR0 value matches the reconstructed firmware measurements.\n\nPCR0 reconstruction verifies that the TPM event log is consistent with the actual PCR values, detecting tampering with boot measurements.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: TPM PCR0 reconstruction", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:hsi-l2-bios-rollback-protection": { + "id": "fwupd:hsi-l2-bios-rollback-protection", + "name": "BIOS Rollback Protection", + "description": "BIOS prevents installing older vulnerable firmware versions.\n\nWithout rollback protection, an attacker with firmware write access could downgrade to a version with known vulnerabilities.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: BIOS rollback protection", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "linux" + }, + "fwupd:hsi-l2-platform-debugging": { + "id": "fwupd:hsi-l2-platform-debugging", + "name": "Part is Debug Locked", + "description": "Hardware debugging access is locked.\n\nDebug interfaces (JTAG, etc.) allow full hardware access. In production systems these should be permanently locked to prevent physical attack vectors.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Platform debugging", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-intel-gds-mitigation": { + "id": "fwupd:hsi-l2-intel-gds-mitigation", + "name": "Intel GDS Mitigation", + "description": "CPU microcode patches mitigate Gather Data Sampling.\n\nGDS (Downfall) is a transient execution side-channel vulnerability in Intel CPUs that can leak data across security boundaries.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: Intel GDS mitigation", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l2-amd-platform-secure-boot": { + "id": "fwupd:hsi-l2-amd-platform-secure-boot", + "name": "AMD Platform Secure Boot", + "description": "AMD Platform Secure Boot prevents non-manufacturer firmware execution.\n\nThe AMD Secure Processor validates firmware authenticity before allowing execution, providing a hardware root of trust.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: AMD platform secure boot", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_amd" + }, + "fwupd:hsi-l2-amd-spi-write-protections": { + "id": "fwupd:hsi-l2-amd-spi-write-protections", + "name": "AMD SPI Write Protections", + "description": "SPI bus write control prevents unauthorized firmware changes on AMD systems.\n\nAMD SPI write protections lock the SPI flash to prevent unauthorized firmware modifications.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: AMD SPI write protections", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_cpu_vendor_amd" + }, + "fwupd:hsi-l2-hp-surestart": { + "id": "fwupd:hsi-l2-hp-surestart", + "name": "HP SureStart", + "description": "HP SureStart BIOS self-healing and corruption detection is enabled.\n\nHP SureStart monitors the BIOS for corruption or tampering and automatically restores it from a protected copy.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L2: HP SureStart", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 2 - Important", + "severity": "medium", + "host_filter": "fwupd_oem_vendor_hp" + }, + "fwupd:hsi-l3-suspend-to-idle": { + "id": "fwupd:hsi-l3-suspend-to-idle", + "name": "Suspend-to-Idle", + "description": "Default sleep state is suspend-to-idle (S0ix) rather than S3.\n\nSuspend-to-idle keeps the IOMMU and other security features active during sleep, unlike S3 which powers down protections.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: Suspend-to-idle", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l3-suspend-to-ram": { + "id": "fwupd:hsi-l3-suspend-to-ram", + "name": "Suspend to RAM Disabled", + "description": "S3 suspend-to-RAM sleep is not available or not the default.\n\nS3 sleep powers down the IOMMU, disabling DMA protections and creating a window for cold boot or DMA attacks on resume.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: Suspend-to-ram", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l3-pre-boot-dma-protection": { + "id": "fwupd:hsi-l3-pre-boot-dma-protection", + "name": "Pre-boot DMA Protection", + "description": "IOMMU is configured for pre-boot DMA attack mitigation.\n\nPre-boot DMA protection ensures the IOMMU is active before the OS loads, preventing DMA attacks during early boot when the OS IOMMU driver is not yet running.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: Pre-boot DMA protection", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l3-intel-bootguard-policy": { + "id": "fwupd:hsi-l3-intel-bootguard-policy", + "name": "Intel BootGuard Policy", + "description": "BootGuard error enforcement policy halts boot on verification failure.\n\nIn enforcement mode, BootGuard stops the boot process if signature verification fails rather than continuing with potentially compromised firmware.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: Intel BootGuard error policy", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "fwupd_cpu_vendor_intel" + }, + "fwupd:hsi-l3-cet-platform": { + "id": "fwupd:hsi-l3-cet-platform", + "name": "CET Available", + "description": "Control Flow Enforcement Technology is supported and enabled.\n\nCET provides hardware-enforced shadow stacks and indirect branch tracking to mitigate return-oriented programming (ROP) and jump-oriented programming (JOP) attacks.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: CET Platform", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l3-amd-spi-replay-protections": { + "id": "fwupd:hsi-l3-amd-spi-replay-protections", + "name": "AMD SPI Replay Protections", + "description": "Monotonic counter prevents SPI flash replay attacks on AMD systems.\n\nSPI replay protection uses a hardware counter to detect when an older SPI flash image has been re-applied, preventing firmware downgrade attacks.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: AMD SPI replay protections", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "fwupd_cpu_vendor_amd" + }, + "fwupd:hsi-l3-uefi-memory-protections": { + "id": "fwupd:hsi-l3-uefi-memory-protections", + "name": "Early-boot UEFI Memory Protections", + "description": "NX and read-only protections are applied to UEFI boot memory.\n\nEarly-boot memory protections prevent code injection and data corruption during the UEFI boot phase by enforcing W^X (write XOR execute) policies.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: UEFI memory protections", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l4-encrypted-ram": { + "id": "fwupd:hsi-l4-encrypted-ram", + "name": "DRAM Memory Encryption", + "description": "Total Memory Encryption (Intel TME) or Secure Memory Encryption (AMD SME) encrypts all data on the memory bus.\n\nMemory encryption protects against physical memory access attacks including cold boot attacks and DMA-based memory scraping.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L4: Encrypted RAM", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 4 - Complete", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l4-smap": { + "id": "fwupd:hsi-l4-smap", + "name": "SMAP", + "description": "Supervisor Mode Access Prevention is enabled.\n\nSMAP prevents the kernel from accidentally accessing user-space memory, mitigating a class of privilege escalation exploits that trick the kernel into dereferencing user-controlled pointers.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L4: SMAP", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 4 - Complete", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l3-cet-os-support": { + "id": "fwupd:hsi-l3-cet-os-support", + "name": "CET Utilized by OS", + "description": "The operating system actively uses Control Flow Enforcement Technology.\n\nWhile CET hardware support (Level 3) is necessary, the OS must also enable and use it for the protection to be effective.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L3: CET OS Support", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 3 - Recommended", + "severity": "low", + "host_filter": "linux" + }, + "fwupd:hsi-l4-amd-rollback-protection": { + "id": "fwupd:hsi-l4-amd-rollback-protection", + "name": "AMD Secure Processor Rollback", + "description": "AMD Secure Processor prevents firmware downgrade attacks.\n\nThe AMD Platform Security Processor maintains anti-rollback counters that prevent installation of older firmware versions with known vulnerabilities.", + "type": "inventory", + "condition_for": "failing", + "rules": [ + { + "attribute": "Firmware HSI L4: AMD rollback protection", + "operator": "matches", + "value": "FAIL" + } + ], + "category": "HSI Level 4 - Complete", + "severity": "low", + "host_filter": "fwupd_cpu_vendor_amd" + } + } +} diff --git a/reporting/compliance-report-fwupd/mp-compliance-report-fwupd.png b/reporting/compliance-report-fwupd/mp-compliance-report-fwupd.png new file mode 100644 index 0000000000000000000000000000000000000000..06f318ff116931db71704fe440d625fcb6a8b430 GIT binary patch literal 75167 zcmeFYWmFu^7B)Hw2_z681Pku&u7eX8oM6F&I|R4DAi-UNyCt~01$TFy;LhMO!@%Wz z&pGS#Rd-icSJ&RPckSod6{eyjgN{mw3IG7mL9$Yx0f5&Y007dDH!q*> zBp!_O0|4&#$U6me=dZ zls9OWW|uZB3tCTE7nkeUl;_(Q)8ncJb4rPk`!T1!r4ny8`p)-*h5oCb-xFlS#$g=j z9z^t4_%>tDg`>KP_W85_mYT_+|5XRyyhQ(Rot*0p_kXK?qvVtSYrz>PhVi-Wq{b&@%a%Gd22(#LU@}>Nr#<#0WoaYUhG7rKjk32$5&)45`KR*1-CNO)K@hU5?&w7mDlziv!-o+ZV^@|JtlMOEeV=c*~KyG4C* zX>IZB(yFseRzlm?a}}rwkk91*iU(jP_3)rgdQjxBSYF~+xZcXd^yEp(qMhm{aISi5 z;5>gdO9`0N)cIh^?_1ecS0~Ka{vZQ!YAFy_RPjCQoPNl2+C9><6nOD(ZrqXIztu0RsXIeFog<3*54O8FH!9Yjk5Yc|sKGU=GmgVHlGgV4Ll@hgn;K<3* zJTb;zOgnXdE@8G!I$ay|K;i7xna160PPyw#dcyi*pvmgF6e!87sIGpp{6`5Bp>;|o zLwpPyzs*?qYkzwz`hG&5oTb~iT8R|S!@*&7z0CIZ-!To=?!~x?@OP|w9nXs+rE1;3 zHu4U56xJ0>WI%{ei81l*sy4`%k~k}Y^Dc?*lu9$HEELMaYvmwc)#izO&c^j{7tnpD>JI6=!dAu)YF?`0uSL?n z9+jNKe~wvr6Rzgy^ch|YA@z}{;fjbD^lKEC5v1xENG%+vY6}ujdk+0TN&#z?YisH0 zDVY8$C@>w4)PD6JO#q}7)A$D)K7c&rtUif*rioDqw%5hdhAmO^)<#-g4VZG9h>~Gw z?tcPNzNQNteuPSBTcrks;oZZjq;btaGRUm0detRtJ+NA{v1P93KI>YSpzYq9+r<}7 z+YT=Q+TI3oEyKo2kUbXYD|7(cRJ<6{)+070$<+HeP3?=Odw;)1FVKw&Gb_Zax=sB< zQS2+`l$7q%R5=BsAsg@^E)LGRw>yjs{AgxwZN0GYNmZ4Dn>iyn_DgGYd6cW6E)fk) zq0Pb*3*F8*0;bGzF@*%l$c~g=b8;Hr zR2CH_T~Q0r^Gh+~VFR@PrnN++@1NS1j1|4XDwLLyLVj=;B7cE|_96#KMGJZ=aEq9& zbLo7t#|8jWKaUoTSktC@lq23h+eemn+=D+V(zYSL?xpmYioV@bfOeDdgsFli4Bd+q zBGz~1@q8?;ZI3xtffO7d<;q`jjrInjWCD)^+0&>@$zV>i=^u4hS9=o?kaP}&_?N{$ zFC65N(9wkVXsBuJ;k!aU!1fqXo!QpLW_WBD2&C-dQm3Jz0Tz86=<4Z|b?0<^_+l=q z9Itk9c?sMb2Pt@6EG@HLUR=KYV>8u|{`eU10tZ_0v~d~NhJ!UQ+1ru55?V+!K%>X+ zG?`4-^DU`iOGbWAP&Fpz_y%>*dOXDxhgUGJnAcrJYBTzLH)8(g7ZTauIWb>cC5z6_RVT-{&`VZaG1m`iE!GSp4? zL1`mQNBAF7zNHcNrilEgJtv>n#yDGLR%3iZj4orJ4DXwtZEQT!g(_$4rwmMFi$bTR zom}1A8fy0tF`jwdZFEbsOiHlAa(9B=u-}a+BF=ny&H| zux?uR5lbNMv#?67LOSJI+SC4(c9=0<`FFZxT)Jnwj%t9Yez5IxLPtl}C9;8}`{w0Q z7MX4YJaLRU?aC`R?mg=t7fpD0_$U(C+~_b7Bm)*5Xa1tA?aS_tQTvDcSj*?JPyVLz z_vHTM*db2?vg>Zv(=C*1*FX6sU$wuqd|}^t&ILEkTr$Kq`%NJy?EMfhH66DF6A`(nS~8)3 z#gV(AUGFaUOL&}8JIuSoYm~`C&15^gX86ow}9>WQY7>#ju?~{jIU`+(i2%A z`MkWm5Ded6IXUF){CAn+NLFbW?@-S_?v4we#CYm0&5*MIfpm0@GMI|ym;E<-nwk~A zfB*TeF-kS15>yftQfyW)B=SI@_v^7rb<5ylPaZatc8oPzEHXB@UtBuEOmuiOaPCoW z(|`UUMGVOqm#wILczC$Nb=l17K>7OmD#grPYm`w21%6mRy2I{u%^4mZO#xuZwR=(` z+dxolL`4}7dVM?ZSb6Ab5X3~J&lLMpDBVM$5GWr`*89EOa}2;(pKhp-;^KKlFQ-E%+WEH#0(SGUb3p7t7PC?pU$inE=rr;* z=1JV6AmP@a;j;cnX41sd5tH7>BIG=cuH zaXg?Dy-Bv!t<3}ob!1B_Y7$d!k?OK8FDewuI zHfJYin@U9m1=wx>nDdh!HQ-rt);pW#<=NO?4E+A~+CkA3Glt~F-cfem?IpAblqCY3 zPkK$6i;?*Un0imw!(GE0LJM{`AgZ$u;gx08SNTEhV`F%1dXx1wT2@0LPnUIdRUbv& zu1au6QdGKy?2(e$5QBZiWS3vT7X@#p1vQg=ij*@~2l8&aUuQ*~2((igyOIlA_KroDavF@s#M^tdg}cT2V>`%7&O0zIn6q$1QwMU8rJvGzHN0u%VDBp78;j zAYrKC*rLn+QR!XCgduQfDpvl|m@;3Dfn?+n%9e3dpkEzyaJPddv+Q`8N5)y&E0`3G z1h=XU`|{oqKEJjYo+D>)h4`cUw2qN{WJC&Zc!i7mGZlA@Ae6c~Zn`b}JpTIl&JACY zDqqNj8wiVpf9n}1Q@+z{{_8xxVgrwNOe#U2<4V2WdY#R*p4~jFT`FkVBW>Puf_*)7 zq*71F?R98sF|)g?Y>iTWBl24(TDQgYd~^!g`e)@)C*3vEVD5sqv*4fCAG8fjRtzdj z4)%Alna_w#is#D4LThR`Y3@V*FtxSFNE+p;l*(Wl85y}Q_yu%Je4ZkoDED%UOHgYj z$3ZQ{?=|L#=riVZu$^c$8Sy@%SJB6rk5patEiS85$n9iE6_uS<%Q@4&KbjCccd|M- zn4j0Im$xl12XWE-D(K*{B!3Z0;g}U#)r^BJZCS@MmNzS)P?x)8XP30r%_37`@aA!) z{?x;`2cBcXb_($Z{#@!dy;#=RdZ&<4zl6oS;%LH5AMbHaR9X8ro+56w5#QmW&W?L^ z>aS90AC5Vl%W*0c+(LcO_opGF{kYbh_?O3*A7n9e>^zeco1to=C(T+@xBlhhcpOk< zEMVJ>w>9Cwcz0OAI_&87{kuzmrIv!8FKm`!bn6vHh?+jX(dtKdM}tgE=2^REsi0}h zeO7KyE;F6|QuQCz)bZ%(W_;OMg_AOpPi&3TS_ahz&|coiwCk#j7MG!~A0EJsf!OR! z0XXZ}u_#s@hAeNO;!e&6TYLT;g|#|L->ydNP2?c!1}qf(EwL_ol^EQ_{2rD!-dt>k z&6)_6&16F`*W^Nk@53T|p92U4(hh$+J7YtJ!NTRj_t^B}=RqJv_{rj16cl*Rx(w0E zxz1)U6^31YBI9Nz{eH6CC-LaG|kzg?i zvrQVE*ntO?uRIQP4Z*hNbp9`Xn$Spb_+olM_tR5vIE?!)C|^?be_8abSlNwBN{uHZ z!zTR3hN+ze*bXTvIaHwi!|wPpeMSsiWY8YT`@MPI`830XPRZcu_EURq1c2n_rOhcc z!TKo(;mPp{=3ZISGY#z^vDxET-R_=!l_E4{}*}J*Ge@rv#CZ0b2dF{-dnEX>~JFCqNVW?|!^c<)N zId(T~?)9am5gKh^VcfX=ydEBrmmev(rg=(AnqLQ9*QP~|GVdOCe5qab8T?ul`qDy+@L1BHh|u7vv!ImER6j+78AQaDwW+=FHkkuIye{^s+64;mpb&INdq8J zw*NZ>v1wH?D?su{z@4=}ylpAC{jdWmKk z>a74A9+pEKQi#$?vXY+{aH-X4NKU=Cxh(D74U+sh0}9Dq0-p|K^_~<; zR@j-|wExw|JFsYlPwS#PVgQ07xBh<2y<`1W1MK9pmFJMp#7JLyW7sq|vpz0XzKIa< z_SjpB{&;D9jO-DX{N0TZvTfFwur5T#xuv4sKaejX?M45OoHLGnQFE4-iQ&x>dPJ^$FsVLSmnA^X57Rk)V zW$2sD&|}urPNVS~WzL>JQ&I0H7Wu7&5G%9hs8@s)22YJ>rT%-x_xG*lqubMT+V~1rZ`4}1(%n*Zpl{L@$cfpcmZh=S7(QPFGkGANg4RBy5A++CJfbD zEIVRPyjg~UI_bH1gH9bR4tA{x8At@hRgZTuNN@{36@|i9Qg7UOd{hKnjQ{AG|1j;B zqScbZ{?gs+#}xJ=UiyTBX};aT%ErRu=fG^@<9qWs4E8U$IhTUZG3YO)?DLPOB0j^W zYHA5yHmQRmm-1(oofE;qLD+s6(VULi!1?vz*`+DD)LUqb_oH1I2-4$q5wOF`Gn1_f zqpe4FP%BDU@D-X+O2zpU6og;8BqZ3xw(tve-lvC~LY)Nyx#*(~4CtNw^D)nCbN}yd z+?F>5Jy2+WSlSH!*mF`RtF(U-T6bLMEl^jo^MK4WGIlwx)rP6brR94~dMUCk6;#NS zzO0R@bB4?CV4Kc16aTrviRd_I7!6HHW4^JWhFI7kkWjl}*VMQ?a|XSG?kCe+1~B>N zZ(-qOjrW&yLGKEkKf3pdIp3qoJ?_&l8Hbao^rrLk0ww2|NfcR7*z~kBZu%ONbqfp(MvR)7kuYL2THeO2xLcsl^*t8 z>4k)ZC>R+2+JALZvbSe_8m>QT#vQvj+@4k>5L7*JM82H+vmZLm8Kg$BGo2$DSW{CA zzQ5+1=BG7KsqDQiGCWDfq$1-moHi>b1#*@czkl9eCa@+ZCbl&t2)%H(=`?LHmLPp4 zW_}@o;Tg9CWqayu^l(RVSdZB*4CN1|y+N!7x0#euSy}5UXv5+>o|G4`m(R4p^MMgh zElW`x1h@Bi@RUJ?>J|+{?A>bfOXI_UL9S$zu_9fCxMS+L-K%QOuUO)L2}FnZu`(O+ z!MjT#1++y4Z+;LXdCQw?FtXKd5?ZMS`@Kh6aU>gU93Do!ZG9gv2go@I>oeX}ez`l+ z4ACW;2j>=PrXrXqDV^Zs(CKUu8bD@1TJg$k^>}>JXYqGz6u+9zA#TVRr^pylWk(yw z0y9d@{N3IsMcDCl)g>76jlM$jW6l>pz2Q`KWeVtTe4vv>mG{=XwIpB#EmA4 zfp5c(^pBAfdLu$Qo+|VDj0KxFhA*~upgm&0xgTFSqcij>jUe8*Rd%P=W4``##_l?dBS)f;b%xFsdz^SR^l&k zF}L+#<}h9=?NR=_Nv!AuXuj2$y%?1uaBRF-Y$?*(V_L=46*C4V zbT^y7mDWEdkKD*`&$SvP2(@Rl>SmXL5sdAcTw>c{wyrq&E>gZYs6B1$1^HV!5`UnPu5 zZ>zGKJk|>u%f0CFse|Q&X84%$!H$j%S5+<;Y~`^jj7%eI#OQ3^&IaWw1rEi+sLte3Nr>JSbl3)lt+g_r(tt)a7}?Ff~R`e^mZpvnbm@qC`?>+AJS^y8v-d z)8^6@ammYfb~|izoe7qdY1`WLL`H@|$5O_BNMjH`qyh%fRVDV9By%l}@GVHzm46pP zS9qxAV_lEnxQ&EBh3&X{`BL-z-|fuW_)hzm_w@!B6&~dspD$szmzAp<5keHHx61ad z;ETSK-eQ07Dds~0x5Brl?S%-X4}e&rUr!=J-n*^L;n(Ec$Bwpo?e&L;YrwsZ?6joQ zTZrAuNe*0ER%$daPJ-& zD^0WeM3N2sEX^-7N=!;o;}o>{3${9cf9nmRUGD8nS_6*_sN^Pd6I?r9m`D<}y$Dt4 zk*I0S3CSUM99Dw{zEr|0w!Z18N% zd#ArA!v@->ti{2P`aj1NH6YVs+MEz?G(ys#x8iLt zZyU>lqpGedj~~aRP~U*i(Ka<6v=woMZv^faWomxoKX0s^Q1%)XpCZkz|ewR2x(xght| zZC5vIVg`~@tH1zRvp)6}dgtW&0h}aPrz<)?q(Za`>WnwlP3K1m zp3(1=Y{PH+gKXL54~cKZzF)4LvMp4os;ldrFD?I#g-=^Q-9wBzLW$v9vsSwi02i@M z^(Q2F81=kZ1jh;h=^_LQX9k1D9Q&rCII*I9??UTnOYwB}ozSakwvm;)*MY!pYYYCwVDENd7 zj|yaIAs1(B2px&njD4&^hg3Asy^SY+|GI?ML5RKdL;f@)hMHb%KevEFrbRhym4XF$ zHVqY?>1P@d-Sy!ov_*tnUXVa;Fy+`(=1kV-0>JKN5LY@Do?xxxU4FL{y!tmqs|t|W zoa)TBv#f^&0Ufg-DY;Wfr8{|5$?B1AJkl2q9MO&^K`z9*{g(K>F6{e*P9ApmPRraX zXZTnNiwfS=n1 zH`Ue%y5Rzou;*drn&`2jY^-o2BQp-^!~I|fS^Il4Ka5=VhrycH>FumR_*YeA9Bx;k zk4b$~j|NN~SEvr!BCiBDj*~|k^uric>fOGX>&Pt`J+Ei&)B_fwuBXmF+-7R6jUlZu z`<2-w9@dC{G9)GxZ_$o#7pThdUnlH(zX>6k9h&_V0tjwg?3xix1mWpwW^@)hSxbF! zi8iH#_g)G$X0{~f9P``OJcqW$EI49T$qQ{lj{V`qS9vmFB|F-@(}r=H%6L>FjAK?61(q!nogl1n zY$O`l0(*G0%dQF+0U>)NDKV5C$p30Qbv)^p>;Kqa*%2BMA?mW|jcDQd?*Db714)7d+vGwRqM3z=YQ))5`M|)JIB2NM81-2d|T?VSEQ^UhGUi-aF zj&pN#%4y{KMOH0dFC!X>NuF`w?ry4-ok!O3QL4COV$cG;jr>R>#I1P!AqfJ08wM(% zJlo*-o>Iro$9MY>BtN~=6}UP&O5Nit*Si^!T*>(Y%j#Y>#s~(BKTH0?EOI;1*WEn~ zckXVg26|a?qDklFIped6fFJdAL_9a|YWLXG$=t8QciJQ0VJBogI38C1QsycMBJEj4 zM`wcXzIqO0F8Pz5apJbF57-Hh?T)ox>o(7qsq9rX+*AknY|`YzkyNpsCm+Pmjx5sJRmc)ZzBHsR<1HUMPwLsLK4T8F zm*QD<83O3E^Wv7B1)OmZ8fXVpj0fX+Heqxb{$Y(O!{m69Q@E_GX*n^a`dAo<5;56_$1YoptA`4i$@cM7>kQz~^PNT~`qt$B$+Q~oPwRNt>+2~D|FR$IY2PY(i{VMpqSemSQ0vk=`YW)mdUz_UcaPy)4+w}l4HC~PAG4E^wQx2iYr>CZ|>-O~z zXt`Vb)~Cx#R#}sNhUNb%(&BYGUFZV1Z=JxQ#1RASqBE@_YZ| zF3Il-6B9ccoUoc&=jWV^e~TZV<}<5ZAELCqCojk|ODZ^3CMIe8d77>BasF=!T5<*Z z$vs6N3gXB+p-ry0XAt?ec57wrTsZ4?LbO_=WzPNye0}`{x)hnMy{N6niE!Qjg}{;K zSzMV}9=@0dH>R>-q$7GNG7A_d|L|Ymrbb38{F^xe0GuJ)Y)rmY!^Wn~QSsjYk$&XI z%B<-eqSUstUj>CjQ-Y?arNU0*f?3yE0F zdqaFWe=)Rv2$#~T)Xx^Q>L&Z8)Y;Ua+i3rEd#!b-kSMKa2aRzZx0}&2lrzD$7)LnI zKt=yIh+V6ptE38tTS$kQnlPKu`Si~8MsiDwU`V^~@pR!x2pNka2t)&zd_~(|6fP(C z3Lu6zEGLhh@Zrt#^oe=f43%MH(?zO%{(NXM)#?=;W+WNHj(IO&d$S)$^#;I=&hm-V z4G>H4Ap1{JhQKq;-jnM5l$ey%KcD8h84Mi@K-H>*KhxEO;-uLCz~GzbESG0>$*>%n zd<%%h{b|p5sK+YJz-099HK6VdHtg5Irm%m`zBblB-NhoYfRNt;YPz}KP4YfpKfgHs zocHo??fEIeE5iR)#mxUF?&mcxp7WplkY{2T`TuO=17q{~^#6VmF!<8z|4x5$85$a* znH0RZynpqd=YJIgNdM0zN&x`>AF`_c|HjXH^8YF!O-#S+w1H}OEPLBl!VJYg)N5Pa zBPj*It%1@4{-P62RisF=@inh-yogb*Cg9e7X)Aqw=+q;rB?*s4Zi12WUrF2LZ@nVq zZ|MgL;N+rZ(~9DLu%uaLz0~x?*p|b)hASJ4kLz>}{i@|@%ei(*fy=DqAq^r21 zl~Q7mkga&!!D~Yn503~eYu^sFBF576yZ)m?If{ecIsP-=i&qTx6+YfJ^Y(zrhXG8a z%1i+;69z@dr{5_PkPg>2Uu+vXM7uo#>S#RFGqK*kn&#Bd5kt(qN!V-ie5 z`WM9vJRL4Cs2WP2eMy| zfEE@9Ldxl)`R<$Nz|N348xwcvQ&#{?HF!M8?pS!RnygBMy3`@?@SI|T{AlKe;a z{WK{C{E>$(w?iWE(RoY0g9g@wx5(tRW3s0WW2X0K9Jnuex+ZL(6OSP77X(;)eKc=F zeKmCBlp^Huv0|~3lC~mak`x;@<{1hX_3W^BNBTWyw=oaOWI8OAGum*o7bw#G*gC;q zmPtoC4YyV1U}NDlyl*x-v@bzyAox(_C6X1 zpY@gP#fFo;&GQ#1{|GNtjrs!A)8*W=z@v&Rfl6i|;JDjgp8(bmEem7%h-uJf(sS%f z_{x-qX@9@Mq$`XvEp(bRW3Bd9Fw1jT&V691bYU)rb5_P`B~qvE8qU*mtzAGB9|I3D z+pIRtw9t?WJYfg-G3)-+Db=W}vjHCc!e(3b>&D1H1Yg_XDLzBt%;M^O^^X<1H4 z7>x|J=Aodi{E;12Tli2fRFPvCXd8M-HR02^N&I5zvwO-BzR8-=V%zEx2{Ox&MwCmJ zCWkdb+k-s*RrjU;}|jtm&Z_pA$AE0`2wN3F0+9H<*25Tp?^V#a?yUuC60{26M#( z&*1w~ZEu6u+L#@a?>l^+jrQelxeI%xhY-=@b*N9RPrNunZPv{YdSY-NE&O)k)T+{O zZH#=xeFWBC&&6LNR1UkT6`yOzk?Qi+f}Y#u*p*FN9M?VaWD?YTzl9b@b5?nEHaf$p z6Yv{2Qfn_O1*T_0g)aIU2K|W)p%@Pr78hNOmAC!tkLE#I4P%L*9!L#4@02+gL5hC1p)pZ*d&|1s&B^d|(X;&OCoJ~yO0 zuI%4UgQ@iTOY}s?&PShu2w;fM0Z-W@Y_V!UV%$SRg8Qiatb6t z?okn;^Qx5TNNzBcuj9cO!$7WNwfMmEC)ZZ6BoW_j)94QP>R#rwzA~%a{_EE0_o(bl zH~8I)O8FYO7mGiHazVXsjfON~yBhGR2G=QzkR7e;f|jUKyYdnIAM0L|lef`@JM6w+ z$JCjM7cigZj|E5!pD;!l4ipWr7aZ4y0CWP_1Bd)lo8)B`eF7-d45qKEf?i@1i%`>e_U~*()w(AfDb1x;KxDUD55=i)@Mm z3#(f-SICtHovZEDrN6FFJbkN&C6#Fz0O{Z8xK%wLw_3yM*D36XED&i>C3oGuWe|7m zOP@SK)Pt%z{a_)@1?*SZ!bEccsoG{PC>1Q&wRxP4XkqSN>4QmuI`S*IziAf^3Fdp2k|p% zQ(V>6Ze)Bu>WT>(E8U+W=2RA7_?1fDe+yLWE5y(CFQ67aq|{hQM0;^uMd&5p^TvE_ zR#QaWOMI z-kIA+K3CAc)J2)Lo1Bi1$NtMpBCQR?=1=LtQd)}o9`0tZCz^iaxjW}ev%Fe*`paMGMq1&Okym{geF%GY}KU$F6ZZ%p9 z@|a0;YU#VT^Pk4x#?SoDE~7YKOyk+hnzc6WJSCwI6qB6eV~w#~!brV)jP$QZmf??a zpF(R(EI$!0Qj!W&YV|qnO%+nrHg-kZmE^Ms9by{Kr)QyObgil8RzoU;e$Dfxc|MWz zkB1P$+h(3rVZN7{=X}vSq8v)onw2Z6x>tV}mFWU$#+FpF^Am%5T5{)(W-3So51EE0 zl25=7zZ=HJo9EC)l2Fs{JMhA4H#ci!p(Qsl^=IoujIk?Kdyu_%HBQ8V3!YKZ~}!a|NYGr~A{bMRQ5JwYTv6TwI=u z(+n_i`oMZaLCuVRM|*mh+;)EOa+kDn1>($8XZpg7i@4740w>Le%eM~?`&u!bl|vD7 ziZ>?2I%53w(DwJFDUHQj(MPz=GB0Z z4EIjyz&~6@wtHtre2~^hv-4p6l%&aal4cvHOj#Q)i#D{2@g4mN2@3VSh`Sl^_~2m= z-73gSTaEg``E7r!ot4+I%+(>bu2hIm!>2Mr!pBz*yA<3xyRnmtta_6#)%6kSAiaN{ zQ&sDH!u~w{Ux+|Oa{MJaw`hCpReegwR`P-CFq$Nro@w5@c*5#Nd#T8J$N1>t99uje z_z0y{=4zx~WV$6M@o;YLkS+1~WH3I#Pgo8nS=O4Mh!F*tcF zST2nhF_&0sVwGw+hgz`jZ7aOG4|t!ju2UwXOlW0?C!Mv@aYh{BDQmyd>P{B1PTTJV zgTYo|EMFQx6QLdY4omtMeL(ODWO& zF??p9#wd9My%zKm$!DSBJ&wo{)pIt7&xx(={i*HiBzG`<>E3z~kq@3RtcDRJ!jQa+ zPewLGHtxz?QXWE5gKjv1*qV!2J2aP?<>RfUVCl+RR6IeS zsB@!fr#66n(alK4*_rr$uYO|CzMv~vG?BffH-nY6*4h~s?qI+bGeTK^6mct&wm5S~L6ok%}tmT&v@U))&orAcRyJdh^Q4C_m#` zk~8D(>e%eP$2BuU)46D8{ea71Bzj=4jT2wC%WKmrxER@<8FlHz-HuBcxQ^5 zfR9t6VVq>5?w=sYkEFXHce1U*Vst8rAJe2-)iGkH+|MT~Y)7YxbKPGq&2O?j4EaihJT?hS+jl49jTFyy2`@(#nTf0mOkZHbs}1!q;$H5RFz_zoA!V+9nXgBV z`7wKxuPSrGpEyeXhbmbpl}z9MeyWW1L|=Yv*WpN2njw?xRl zOD7h~6OH@5(e_U;C5q?rJlg_smB+1LUz9zb=Z*w5jLU^^v#g+r+n8!>UCc_$U12c# zdt#QXhDDW$jh3il(Tt=?>DD{1=;(ePp%GL#eAW~4%!yGZwTzw3*@#rx+1H3T8W`)4 zaugq?u!Y0zMzYK&=DQAgto!gU?FP4=a}uN5Q^uW&EIwKZnp9RzsDO1jRob-js`jne)e5ufcxsMm{;~lOff^Q1`Xyf zT=#!CNaHDVPalcSOA4K?&dJJLrvRuixRe#>QTpw>COm zLvp^pS>4YwcW^vNT{Oq|Y!og{puKPPGD_Ep5UMu3pVv`*TXC~xTGHQTbc{N)3tgQR zks%n(IdJ6FMaOB?v#V#;dNB0l8I7&9W72?A5;s8NLt0YuLp;{@da@wXFdG@M3o^)C z>?ZW$wpS&Zzuu{4IHmx3Va_7H0p4C z2Gm%x^lj$fO3(%Oy$!!0T{v{^`d&Fux%AuT;Vkax+0EkmSj03ZlpFe_*UnzsyyW!s zlulXw@Xz`(m3(}O{?6#42W9Bhx~a6)=;em$vg>Zm``%BW{y+GAulvK0n`f;)nbAlW zYKR^cNe851y|OJr7ZrhW&X{A{Xtye7UEw9yiD~ZNqg9VPmoA4U4#k?SgfL^974BkH zbhyxWCY*Euy?r)`6wv=Pq~1+MuJb5L%fZXB$k+|1)y$lalzl1Qv>qE`y|X-Ztd*F! zr(ISq&m#Aqb~mh>TT`Ph8Nd14V2Y+|c6qDyAtIDsNMFoaQQ?p4JR-1D7m*2;#)xZc@|!H9IAv2nV)WX^hfNz$=e5ykQyP=6wYGHl z3o>1~I;pqyKU-%9=0d3Ot@B|wW_oGTMu~5ft#J!|po)pqx-=-POR?%+J{Z`%{DIyl>MlaX6bOx!`;*@}@i zdRflmDt_KO>XW2ocQLd4%H{!Zdrnsn63$k~7zKmK#Hco6v|I(c?)K-2Hp`B4qxmX2=UCX^q0>?Ak5=E`YE*@D3ffNGvZg+7}O!k&1~v$?)3l ziepsxt*pbIIMN9Tix-h)v5hm6yM#qw5b|1!7m%F0eg8U!NeOeYRq*sC)JkojW!YM* zjz-$E7!)IcK(MZ&R<5!6g~=W3knD-NE#KwxHoy+^%9>EzS+fO*Vuwjrv+Gcc>>ln9 zf2Y>c-84?lRD8c1*Bi`V#)L_Sxl7FSf1FL@rRLbz+$>G+dcM=GCbO?qU`3r6c0`{( zQr$H+7V~Ax_4Lnzw~2I^MI+fzEt#lsPPrm9dEY9hH6ryXCDcy zwOhX&FOyLi!^W(``wqRSc@#Q)w(Hi@(x$*S;Wkks%>T^qnfd2$-`uGFOWLH#_dOsK zb$_L)w-;3)hQ(L#HWvjjW`R5mkF^if0p4`f~4>e=Ih1lSamnCJ{QdeJ} z_R<~966W*mf#8|`a}ECws{8)9LWtPB|G-JFCFuZSSygq!G79c|#SHI(7jG48bf~^$ z_`nwpF-yqCj=aBL`Ip`t65puC1nc>BEnS3v;;lFv-rb|U@F#kwTR{#-Vh*Y}m1h`A;7 z+J^~!-$+t)sd(z^9nfCrb>PXB+w`b9|#y;ld3ABYSVnpa1XK zHP8EL)9GNDX96q@nQ?JHtN);v)Msu$#oEyS*K=#m#04v<5Pkv*6!18D-Avo9Q31q= zK2Qx$0yv(}(La~T5>r7Ji5|1-#t}UYCc~ZnVC1`urx7-T#SEWVgPx8r0;=~gcOo{L z1=b=jWk|KF@%a$jd?tvtJfJ28ejSa~yq;~>d6xK3U(ZDhtxh63xPS;{;>Yg$F%g8` zC*Ozqqm^~t&Q8^Ymya6sa``gKQO0KfC1?R8(Z?Z5lik~)-bT03HU9Ih4k#zz1deJ{4?xTma`Z+>yvYg<1 zce!pSqTu;eu-@+4Ph6?}flb)+{2TG->SUKr!-hva+WFoi5w8_O7*d z@leTdOYju99%KAj-e6!^y_y_j-^N)_B%-gju0pF}i9rMS#XEvbWKU3YqLp6*$L%~3 zrQHM}x!HB=C#nn9LA%##{Qxa>jo5@Bp$vs!dzBt14-=i!00EB-oC>^Hyb-tTKoP|* z`48UgcV;(PNf7`&cJ1vS0U%kjd^jGD1FOW3;W^lU)30W%#RqEhP3}932?P)DShETH zGpo_Ac9iEAPS4k3yVdawymk^+|MSAmc|eNBTGFWIWABkmj{)$L@z1!7FE%oQVyMtC zVF3YoYWOfZ$cfUyfgvLFWCqbflRWQO08K5>q_*KJDvN*L_w6voBgz+oNJQh)NB$_HQ-#zX~p#D zNTj!M@)}C!l7UDk^QLUY z=9o(AbLN+ZxaY2*oaYInK9rsm~B^ulCE!tt$N&QCDk3jOr2IvLa2^j zoE=Fq;jN6)lEe&!1;}kqhKV7h8!*R>?N>Lp16TU83ObEeo8`naZ$C!ny-x?Yokn`8 z42dFbw=af0d>t91tW8haX#LsldE+&J$_8k9xYJ*0eO1)qTsBWLUF~i{G;wP9%NGFC zlU8AtSF;P-_@d_ zqjb4BY8EE}rBfjO-9H5QTUFQNH4Qz8sTD9m#VLn1Q!SJ+@TF@S8t#F2V-ek2kr}a$ zIcsZ2qFD}GyO$DfpLutA92}sb13)w>YD!ANbdf|lofe0~f`S2;l;{xk-w_UP?x@ij zF_;DTo=3;!(_(==a79gN@gQq!3S?4a!D8C$o!7@I4+A({c3FrB{hW}Y-L|T#DrON( z43G>Oy;mE{-sjJlZh5;m@M?}Zs;^Z@?zt;Il=+Ea25&tjmu6j(s!hovn(g_6-aR0U zB0>It`4x8~Z_2bI!=%2-mGK^pWZ1ul)pc3`H65_^jEOVJ$a25C0AO~p^kAmCD4-IP zn4Jc|+b%gyGzW+K!OqvI( z;??%}bNxdvC3a?HaM@C(Z*!h2Wo_6qW^xK4TehVHlH%E`>8?KAM&MK0bs$HFXD<+^L*((R4<7tYDQUJeWXq4p`_dmHJLwaJwhlsT=%k{gn3$26`kzKWA8DH zvGQffMDO9k?-|MA5YnrGXwExNq!$%g>W`(87qk5y;<1}!0S*H>KOR6|Ec^4$%|?m@ zGGxM$$GCHA4LA;m2= zkPSY4&uhaBsA_m?0@A)o!|>I{Z9mzR21-3CYwHZT?1nw-!Nov*x-#*2y`@3ktlz5b zv_CQ_*JP;g48gigmWVjJKNhQ8+#kZrgP^OxZxIaa}De0@_d z(LJpJ&UDd2I%s9msM;+^W{BW$Hd)>NXKQaKIqm)%UiQnYBzhn#VI?&6Jt-sF-L32fZQidanw0_qsuzx94e%sM{ zg%`rv;(FizQy3U0wn~}mmaHD3@Z^#OjucmZN%fjodbTTEbNJOoS`P9t=3X8~zoFy76tbo$jX*d7oB{3nVpq zFFo?$%zZ*Si}DeYyl1n&JC90Ni4+GVAxFse7H)tHa7(Y?$xfrXWFyiMT3FljYP9A! z%^nn?`8Or zW0kMwGUj)|LJc_`cd?2b5>mSHQXMlKbRB1;e@7cEjYl?LUXtB?G9Ow{HUP1JO;r?Q z$4U!{uGD-oND#meQ713dPmo&7zH{nPC|LSHgNmM)A3grZlya7Wl*lkOhIy2D^;g&R z?{Tr>#-d%BXCy!PXJ^k^;p^vmPpZ$Mqj+EQE({=#OEi`>Zd0+#vY1gaaVl+V9kZ&Zo4671RyQ`| zX9-#@4XKlZz)&Wa(|tkojG}bj{72>MdgbD`oYXXA4jK7AHTiyiC9C5Y+XA`6$Rp3V zu~PaKzY@5CdCQh%8%05KK=%4G*)yuNr!VHI`*CxR*^powRcE=sdikG77KLBvAPjin zsCV89R98JYHjsQ8&xKLgrYMTO*b`R#OdGe2{!=@;Of7zJr>P2$neHnjjSXA?KO@h;_ zc512%&0h$mkNkr$_S&Xg)mzzPQ@#q?Is$|Ruh25zHWv4=xtF>gY#f8vuAhF2%N6jY z8|f$ezF;vIEMQSwysjT|JWJ+2^7j3JMEO3g+m$lIv7+Zq8wqRA4PjtkJWuyd(GAT0@h#JnYL?5U9qV-`ZyP+$rjLX5Iv#cJV<9&mM%7n38K@W-nRMokr52Q^VsZ4t1kzRpLvMR4S4;NxzbPi&RuDBSH}Dm42P3NJ)ST$_y6{j> zaJKPlNa+{5-8Z}rsNvF@!mocLz4#6F`Z(Yc={t0G@-$1g_T%d_aT8zh;O_6;C-APW zT?_L0NYm8{JNbW4C7|HQE4$6Zey_U?f2iiVAk|KZbe$46mx4e<9c_F@0=1%q8-L{Q z$UW%!C`=KeSO2HH7Tf&pEAjyGF)+By6p*L;CwJhl3PI->UYvrFP@FH{A2`S<>_=@n z@sbbQ4KCpTDZhzvVBYZFrOtQM7X`ok^gksE^B$H({-Yt^y;Vl;PMD>u>x=V1t+j$^ zpF-s+U9Ta{Z>t0He_LY*`5}I~@3D&wFNwX0#$;{AporjwzyalI0mmbmrP|t?zj4B1 zD~i;65+eKyZ@q6w3qgDZL1;r8=U0q4EhUo^^Yx;{L$9Zw3mjN1+G=xG^-)tn78@}! zAjAfen?{WUH?!E1xBsT#dc%ulXyFfAaG~;2E0IF`$HmGD`ZNwcV|;~CK+ofCArO79!DztxK#Dp*H_jNC)b&8xo^^j%K~Bc*qf}PqB$kC!Yui_ zqxssAsY;&^F(y9BDP;~hW~RY`1zPq$Jv+Tsdp+*k&~w(IbL(b-*y0_>d%E_Fg-VKq zBKf75?R7Qi_%d8|Bv7{KhUHTQGi-O08okVP;m7s^R12)zi7k@UE2a`1aZK#}Vq_zW z&kXn^JDcB4YFSyDKrq?V0LfP314^WD-0yzn_v|x0%!T~&u0W>a?!DJKgNnTlYrQpY z)2UZiZOt9vJRAE-D{`TnoV}p&C@m>cNgwhL_!&##lsWV>Pi{2aY-9c}d}`4bPXDoM zy-E566=iDA-oU?8V{9f4a2*l+glJNTZi!jql%4qjCZKz%&^A#{7gk(c)~g`9uvlAD zLH+KkzdqK~nnz-Am}>g4SaQ!a2J{#xuEG82O|Lc{xUpgS*B@GwAu_F%1<)O{9{&A= z-%SAcpHuw*Z|b)K$#GHPVu9gLpyx@BhEC3U)k9x(EVk$^$*_p|4a-E{FS`7%QkUxp z$I=JfS(Eczs&!4E4f^vj82{scT}sXHEb{G3+`GdV7Nr76>zl2+?Oq+o)Fl3fye~cT zC=b^&dH(y+K83GM(sJ{4Etz@rJ!AE{LjAdZdQTz|l{)Kp$Bjq3N^o(O3qZ$X3BD(n(@&X3g5Z+8v->B`K1zq(5Hu_XM%grNud_ ztnr1TP2hE>%~d@{*99Dhq5Z8bi?*+8EM$(_ZIqiTj;{13<81vq?}Ug9kgDG~El%M) zG!x^|A1`a(j|iRa#V%*|h>%$=@+pZ(PN@C%v{l~J=q>JJ=zmp zfO^GM02HRCwn_#y4N3ch`d&xPw)c(4;Vi*F&mIVF-yvPg%X*PzKx4`U7=9 z{q3B50Lj0wN4czLtiX<_?g!@scDt1tdS#pM7A?})-zeL>h_^m4-l*5R;#}WZkM(Hh zcKkpM$xj{=t|p<%9yk3)pmrf##_YOM^za|8gzGymL+PeCUDllnvJaF^J{I1~`{VXY z2JcMXU2TjD70$tu0$h*2ROYfJ2_m~~UoGp_LXGM^tL}9X!>Ro3F7KJb|L|7-n%!@y z!a%owIs<;O|MlFe|EI+R{&ms+MC=EsKY%hzr5~Wy zDIW9Lbg{qVF&3PXPUk_Ft;zYjQtP;`%$$iqYHEDOnexv3{A3@hNhW$a@mAKKXP5C7LZ{zzUFN zz*(x1f`f#vF%G~EaI}a{4y%2Wsv+BU*V7f&fBb6wJIDa${cEf7nTgIEB|nz*nhT{Z zU53+Dp}k(kJ#<=@_!Hm&1FMAL!O~QCyfKrn&+IhrG27H6?JZ_R$Tc)TVTKeLz|y3e z?lGB?UVbRyzHp0@_!l^u^~HfpN1ZlZ=$0^Ht(>g%YBU^;_dKWQ zqrq@|V{w{@mF*`{8>FHL;DX|l*KnXZ!93|!Pm z)x;`N+3|kg>VAK9{4=0_q~GGi$!(Q6x`P7#(R2I7S6wvQ~2dk>B3VL(J?!?E!LUPy&IT1#w|TiL*i{L&4E?UEM7!7(cv-l_sdfml~^Ul39Lyuw+c<{8>zZU_qYIp7nVT*P)VL+! z&{unrDyN%tGmVl^fk|b;-qKzc$ssrk)C1)-dGxArtoF0;+s!^=*euP@y~fs_?s1v< z+P*+K%m?sGapnD`(k}THm=cozM#Yu@eK6%#WdqZIh^4So-)G$ zhf7DA?vd^+;NrIxd|SCLL1XVCz{pxT<(hPzC9Xeot*P(I+ zhS@XIzE}CD#6|;jv!quVN|xUUO)rQ{Om=-6n96bu5-<<=yMJQ^fF@;hO-+g2aOp<3 zudgW8$1PNJ=4ooZ;v0FBk1)A5tM5H*d~)lCzPb1pTL7H1LDSBuq_9%_x)X!!i}hhU z&%8`&4a-ju_@`~Or@q~;*;*?YM=Mpv4br%2@+kBKWU+T!9v5^P9sm%`MzXP|$}S}$ zLuE7K^XGF>tm1mDD^0Owa2%l$jbf0s>xmjB13rjtWtVXoy+CTKI2`O z-a9sDR1YQE16kQ+q<6jJY)=Nd82)2dLTp9JyFD<_avr03M8N{d8p$ce^IB@E`ap$h zd?hZiNetT95+95Y>k9LKyGs{bh@6v{GbmAY5=ZS*J+mb{ac)6!nDKC_FOIG;1HtWz z@#}#sqUOyBFd+f&Mx~V^VauRwp}r)A$%WNeWxGJxawZ#_L%wz-NH(kp9>+|h8Lq;W zP4Sh-sj_BiLO-e&l*%=9YVRw_U!kNk$CcS$oSl%3Kl+tZ#n9)X9VY9jelOEQQ~5la zpa}aSiEVjA58@IFXCeKo?N`O+pAy(}nm3yTMae1-R21)9+$&ud0zf01l8qj43w&{A z%+3yU7SL3VMZspd2Cb%(dsH%i(lO$?yma)XpDGn2A<)p!O1RGr33496d5mz^UGd}4 zDwa~Lr_5?4DK{mmH?H&Rk@n)utmmLVdUL;Z(QPRHIv}JBMG=b9gN&T3j1u;^IK7Xj z5QUNCE&Rua0IM8(0}e+;wHyeGRg{ zh){38J*Gsu1^3H=nJ&{_g5uhnwidY@!Qs^*Hw4G^SY!i)<{P$0zyHuNn`_C|F!?;P zl61hJx$w$g{u<-Q*P_$5o++tugMV^TBof*qTfTbjrhe%QC&LXg4IaG36^STmc~=AuQvfnc6~lA2mu?z6-C_`+^xN1?GC} z^A+E17}M|~Y-90fr=PFSI0~IenHXD_-B0k@uQQ{)>KIq8vz_$|t5A-cJG2^48NUpu zW77HY4casI2;z8C0WQcE@ZfXOQgGIf;5m;o#yr8Aud~ML&CvbzC=(CBd;(7_O0FeA zWK=)l;rkL zBf9Wk9L2oJe%T!E%cqqlze=)s1A5*ivH|#OyZiHxK|2c6K#2<`+42;7!pEEDKzqiIyu8&X z5PXm0lO{mS-7Ga7EQiOXklaFWy^M}?Rs{cWZ@1}ahR6BzPLngh2})-xB@r(zg@KKpnW#oS^Oc?= z>2KxU?(!4UY(y92cb*iz2Js1TCdS5mz=*|Wx5&EP?po8)6G*7rqvF)mK~q(bb zms(gC4V~{FHnKs6d}qo=B&W=gMps7X;&Km4;afi#9~U>(>SPKICo+1_xY7zb+nFd& zC7`b?D+}+7eRAjh+cf9cJ_R6DMau)1pLQ}oM6GmKFM?pthD*qVipQ7oFJH>yQAr_q z?Ck7cFFUoAN%q&Wa&j(!i{5xCDMNPV8X8-b+1G!axwZ|kE)F)^ee3$T>_MC9kmX;yfwB^6tHE{o8P=sv?!3c-cVn}=uJSc8 z8Jk>wLC)%NqDvlWT9TZ}|1}sf(ePV~iT^UH!hGMoqq7n%Sg^K`fB6v)<=76wa6I>K z7*(pBQyqB^E`~MT) z0>m>?`f_tp-eVW(*?z9bls&}L`RriBcs>I{8HFD2mKP;ukny0jpAadEckOlD7#xc|;#skYk$g4asxFnMy+H$Z4H zcVBVhYklI$)j8>@hiTFS8xeHp3S+Nr(Rp)f98>oGQlT6kZ=3Bp$paH;xwsiF4r{*l zvryy0hj7##3{JWi#<# zK7<6|Vj=bnkVT2Kdy$K8#hnMGa|ATSwvx+{@7Ps{pcQJ?{vOj+_Ljn-aj(X25>s7nY zqV$6?v;HP*{xXn9+lw>=)XlqM`&U{`=9_@al!CUJ!0gR2C)^;}498*1Tc1INi!nEX z%aaoo%Wf#F_dTaj=UW9_e%uD;W_v}yz;u+;7cPLWY{%Ipy~igSyv}vY;n-bpJC#Rx zyiXIVi9l}84Qr@BF2;(phu3*KH zygBdVsO={Hq$d|2RJZ{!`995z^vgD7DVbg3z{x##YGoXO7*)=ZujMmFFHYgJEJqCT&ET& zZa?f_u53i3)^;7Y^Sz)^lcf9yb3{2N=%QhE99s^wi2_`$4$s5TIi-FQ8LiyuKb}}J z3ifAtHrzL@zphm0at?fB`kMT3Qb+iVGsMxol~l!*D4TW_mUu(E2Cv>K2f8)m zVJ$()XfOQ3hzSni=xg|GHVkD8@vb|AwYO2JbEn>|^D_!{H;qP#8XEm6~WiZrlHRl|qZjaNfzn2wmzNlaBM`L8?q4%%BJ>!i>2M72^AF z+dSZ;$ubc|8Tq)Wv~SL~`-%N0CZDNqZT8uZO2T$NjMb$<5IA&(s1sun=j2%1>hO-G zNB#L+Z}&sbK{Dg+RO__!Gys>|Sa5Nl>ubn70?vihgEMeU;bVKL>Lft>`AK3_H+8O zd@Xm}79O`=$BIpDO1GgNp~^Cy&RV$|2k>M)RC@;scCj zY+5RGvmGCiM)}x{TaYFV3d7529W)rv4H5#UKU?l=kOtj}&f1uC9xZL?*{vsn?xS!K z!Kr1$ZlFKZyVq4{Pa~&ywI!Enhhxz;p|dM%5^NzMW?(mVx2FS{*fqHEg5Nlx4h~q6 zUS9!VBt24v?HrL*umcEgZ(1jG6_I8tf1=-y8(WKFH&@Y_%^x&npGvll$(2wP>~RT* zr)R}^rx|@0JhoNxKG2%asg$!HCthMFcg=h3ZUCC+-HXk-yr0@G%g6Hy-SE#q#UvzL zXYwrtQR6+q=@mm?m1C5RtGpx>ptGXGQwGFOPP3{5T_k&OBsnTac?=J>U~@;cvU00f z7}CU8EeC5Uv6POZr9G%h5mbD{3t`u46WN)Tp>fu-cbXQeao?BE!%AkyJnx&^kGeIU zzGilUXwT&jqP%dO4%^-NofTqdgL)1kG?NNaFKTGUYd<01z5Q`{sHekh4S`EGAU~9* z&ra`0Cu=pc4GRCYF)rA;O8ug)3RZLAn71Z>B!j^o?)k>(guAUh4uvLqq*cGqA}72L ztJ`e3Y^IZA#cLGz++4MH&_e0;$uLD@_BqZ0-B1&m~^e5%%klbbi~I1n*y@fL=IG`D|a^*glqE>?MNXtq{MH;z_5Vi0ibkEEj9m)hD_s8P7jV?3_5-|Rd;xW`r zJa9Cz^ya}dltY!d!Rj@WILMI4pz6NfU$n;XlPB%D=gWDlTQW0b0i6k!nzon_QW)SFVwDBFlgBSl}yZ^vEan zgd($EiMDTPzNHlCYMb9N@i$B2(n^Va1CIuGykXXvg)VCF6;{vDTBiNX)n>Z1f-&p3$CX$oEGWmwES>Z@G%g7>|$ul&nsWW zoYtEV(`wgpaj#jNkzg0F4pu)Jts+>jC*v>|u1u_)Dm1?6W93)MuFSn3u)3+hA38k? z&Q|iWH#hUacPx*_q_eaUtIq#9Rfol8=|eisSleK25$7XbgRZO-z4g`;(1g;ptKK|! zcxH2=SvyJ2!Lf$FuL(Enup(9nDMq4XVtuo8$JbE#dhD6YtljaXDNOm&=sY=89#>kz z2Y2&f5S8tAX-`eBNnKoO@icC8$k;+R81OF^U|G%-K>3FJ#ibYRL+o>fsNBgHIfB-;ah{)<%@0&-}HIzztWT5#AI5o%mDj%XKu_7@n7-$J6L^JLExyAEgobL8w8O(I0em@WUj<_ddLIABh*z4qMt0H-L0<+#^ zkS0B@>MZz9dSJ<=%FP%kA>iJ&<}lJ=sLc!VG*W+wNWj1}Lwi`+*p4)kmwh||>v3M8 zOAZ@Z;ZV{TW^-XoN5;pRt=w#Pqh7l9NkU>xzsyfV&~GhR9V#s8!SF542eX~0WSu7JSwer`3Jg^B2i?B-yPOpW{1^7j z|2Aqt@&G?X*t7_!NJvO1C`5aQgILLb!)c&%USzYqg6^lvR9|24Up?}xIK%0vm(f|F zq5VAr13O!20URzw{P`A?PEQa$pAM9>?@!_X*g`=`_#YQ>*7)qx{;lYLB|qM;gpOci zo@78w8BR#;V4Q9?5qO6J%QxOXe(x;5U-AESq33@bI@c8{Dki*`(@&f~3+Dtw(g}C0 zrB1lU+4UZcfy?HB;^%E(wX&cp`5`EnBjX2uCe^+N`xYk2ssY)<+G_!7(Hi95IM^7) z9E%k-(gV#{Kcn4`*av^MWlCdEb*<@b~ofr(g?7jMR|ct*;#Rd;qmH4i(f! zm@WFTtmm1h(OraTvzbp&q9zT2B$n5e)$dvPwGMI$hTM@*u0z#!47zG_Fghp#Uuk6J zWrD}u zWQf_5An2IK*}fknsun4>{3;IfwMpjW&6_b!^(E`&cYDlwLv{){dZ_7I7%>W}%7S27 zm$|_elT@cLr6hyD0wN~9kPfQ$MRz=wpTxqFL?2O!U^%NicvinIJVd|*y)=6+2lZq5 z&3bYe`Z}ELf93r-FTDw0?Kne-!)3WFg{dv2;|XxLAbQP9tmGH21uXIDaDDmQN2fc? zOWD&!U3hSJdVfWQB8*Y(yzR^JMElM!uLtM5Z&qf<65(7+l$+JF9Fm~KPJo0Iy1EID zdM&SaC702BAY>Rb6PpgHp&rfEmorXE&ZDz};eMP%NbCezOhi-%4k4$$2}noGpyV&i zl(meOomT{)6>w$}JBS0vBJtuhdGMT#=0#jRL}tCzD*m{$!@9aWSV}R5S96g{|Cs+= zWjC3xEPW+$BA2z8my@uQ9+aMT|=DGP#XNNqVAMfrF- zd0f9g9^u|xK$2TeT9{J5i}EBe*&Qi+KPgLkA)s_y4S=?Ms7&-9yI=2cLe<*k5-Qq*Qfnv8b_q+V5D>pqbb;i8*qoq`Ou5m6C{va1CNF^b`Tjy^4_T{^fB8|A{GO;zwcf0bsgPzS=06u6RzYy<6?1w8Rr6v;hqZ zAbg*el-W`tqQjppy}3p)lI|iSxCkaF|HQ=Xt~mCE+{S@*A7sI>NCv_sOT5P-bUB!BWgxC@cm`_CQBElOY|hZ zLsO1Jd4Jg5bpD34X#(a~_l=T+#RCWGYrt>@83&+CxDf&Y%0ZSorDO zSmfjG!_#+7rYNy*htF*tpzAuHt}E#++eZk_elQwQdXx>?svWv=tJRSUiF5i|x3^%s zKdFvyE~QCvbK>?-fMww+*f~@2L=Dzmn!myQo~u(7Y?RGTe5XCvr}D0C_v}6yGv^^z zY>YeeImjV_ZqCIdM4e&?mUADi7iFr1c;x+Vv`i_ydT-iF?IECU^ko$`k?!tg2#d@a z$^N=3(IzyB3=UOMRff|LbR1!_NXnO?LDYxHgapSK3eq*_Na2Y}z2S#I4UEhX-wGjF z?|z9KQUpKcP5@ry%|QW$j_3LbUyw8=I#VdpJFG_nUDiRDfTHz~t6M?rb~V(E7WA$L z4XCt6{0sxhckAL%`6jl*6;2EC2=)ih+REGk(g{`4KNuZAn@7wHiF;DIeX?52b2w;F z;jLQpQ%%nhtT?+YMHda1lA?oX4BmqQC?uN=WX*WFYUaH>vvq-n~iE2Cr3t$0>7J%v=v~E(5x@8vP;}7CB4bqqX`IHh@judaWaU-tXDX zdc3vo9{WHI2Yp|`)p#NaJ?NnF(_RIu zG^-?!J$~_6-5P1%n*z6C{le21TP}$n@CD2pD2nQ6cj+7Z3x)7Y%7P}R$+OrDE{y)x zmbG$Y=<{c%N6pG;*cue5a54^mxFKV@pm<&dz%jV7lXv)98&M zcucxKn1#SDHJ9*Z>0w%h0}D2&a!!FRih$--Hq`atX@Z?(KHEpnsd)4RZ@ zWPwm9@cFvrVt)CY`TB}nVt$xGIU-5DjY=-5LSNXFcPUuuKhDG|VomGZOL{Wyud%}~ zJfsX@2H_`c{P@jq@P-#T0f#f_@?(DGU18ae=fJ8v+%oHfeOh1r4*#ijVoPl;Rg7>0kI)GvK@a~l1&AvCgsA!-&yhvXd2nALg zZhc6L0^L~y2o!DwQ)zLc#s5M`MM-Q~aZ3VX&vU(J;pf6GC}=A%E#0!l$y=CK)`*i| za^Yl+3RU-8d3`8&L48O|6a&lR`G*XrJYHT2q5zUuqOvnf7?O7LVClnPJ-_JAM$kS` zW^y|NI-ni|$s3|nza7meNo|)-fluu3b=aiVa23^od7VM@p#$T06Ck(7piYg5M`;I# z@4zdN(CjaUYAG*i?~-YVnT4o&cgG-o9#IkObwa&%=YSA|qEa&vp)0NTt_3Ge5nN)z zR=Q498O+YxScO^7=tT;f%i*v92a1$k`x1g$=6|t(y>HFV%PAf&EtleP>#dgPu#6js zoXi{F^(#xqb5bU&^y=&@KHn}Vof~x9YS!78e7+4-!rU3!Q5Ae6r6%Slr09-t^vFsQ zOGX1%$YGfX%Sa^H!O!1B<3)ri#AtD{l_Ksunkh@Iz$Af*PrCWR^-$1aq_Z8 zdrb_g5#(rao@7>}e_}8~uQbVwTVIP;a(KFRcV$iag!LbwQI}QMk4W$pVzIISF_$oG zmhKENy0k47NTc&P(bjqR!CQL3YG{F;hvXU*si{XeMy|Opq<4DXK(~db^{V6_H6OXY z7H}O2%A~+J&%I>X|Kn|r;cm_#I?(`V-JL;!9Os4MkA5dSl%x|WsHEvw?2D6J`h|fN zffhVuNxNwg#9(8*W-&-Lr6*${3zG*EzrkQTtQiOlUb^eWA%9HUuukenM@j*H7|7FU z8gSS3|Js%xjbxie^zvb*x`8Qt7Xv!igDDs|IAkpKQ_JGaMc*AhXf)e-`q5~A!L5md zE5Z@4rsM)_d4oBVSt=KTU$!l39^C&+YR* z`xF;TF|LsL+1_?XoAgNBYx!2-Tgatrgi+xj>R)IO6C7@g*Te$gCMJ_GDa~Ti=GvIa z1arx`8S8cJ12yXGlH?DbMI2q%S*$tezw=Xgyu!KyCnDS%r7Hn`^Ia38k2kaXa&{OW z1N3~mnG#0kVS~|f-NR#cY~`2LCqF&W+-k^A)1Os;rr?gnlM`r^L>RLGMh4-R=f+)H zdEwZM!AWMGy*0g1&M&>#hOe2NVGen4s8?^?OIx3 zQe@f^I6$uj~PeXF2 zv)U?wSh2r*8iG}LZpJgq7=7vCltMq^t`(+1HGxXM3f=fnayZuS`TEw)YR&!4R-oB+&*l4x>uK2^c19oQYS36x5FayAJe;1J>D6--*DyG*EOe!>`gatJ*9PISu$zoXegQ9QS9NyW1gSGkm#)(-DXU-UWtKJCp z95D5ZEn!{RH0z@pf)mm^t8k>a0wcY5ZCaFX!Ln?pnGJ;dSAzrl4MnY(`S`&W`U5a- zQVw}S+*8?2uI>lb#0$rgD3?zS#*FSq(ZFL=93L!^KqueD z4WQdE4qhL(#d(J(>Ty^M?&7v#_RCivO=DEMJltMB$@xV+FGg|;f1={3>B@4`f=?+b z0AVW`p>1bth1v$>J5nbFC6kW0sUo^%N)bufAS_BnNzF-`3_KTcP-rrXDaWXOHVxpO zzQaL_mjMCx&C+5r5QSDcq65EQ_#+XXD47yApIc#PyU**jGPq6f_;UPXHRXitj=Aj% z2yn3!DhZuH>K-SC4oFHa$7^8eaNKAfbu38gY*UlXp}6tuiLxC9E{cOxG>CgNTmK5 z_lJ#IA7T^`Y5ZQ83nQKCl?#J|on99*f0Ay}tD{}_d!aiooK!6B4RWZ)T9G_o>Lhw@ zN*Z{rdir278Vl>8(2)*=e`t4pIrV-ETV!UbgWAWm{3*peM_`FAbaT2j9gUlqRz+1? zGeL!9go-M3ojMJuQhk#BkO03Zsgxb2J+v}msAvOncrG@W{$f7Qg?yjy6Z@OI*515G z#^-Mhy6e*)yQI}{1-@;=%;KYzu3zqU=F*n`WHmB=NawiJqhWM#_quUrCIftHNNPTN zCQJYfZ8Mvmm|)(d)-2U_%1(R2t>PI`T<|^Gd;^^=0iP24{26lSUpq{11FT9ZPj3th zpWJpv2czAA9cYcXc5lC-R!AbKa+x}0R1nrC6AVfXiXhZ714YR^k2CL0uWG+@Ppq^Q zQQ9v@{0R#3w^~f%!NqM4eR*f9+CBht-hoI&u++GG|q*j$XF)dYM z%<$c{zvzo+5w!mKUd@Z~$ISE^c;@Cw88ML8(`eL7`rc34Vp;6@VG;Z7O4*#@PJ65y zP)t&Z3H4m~ii=1<(CEA2!cYuRT~cN;*7M!X_2u5+7(4}JzY$8_b6jGg-jG`ZUv5;N z=KSSE_ZsYuRIjR%rMpdn3EPBc3&Iro9a**#===#(KmsUnD9SBP+2oZ>Z2RjW9x`=) zcI{88Rp}qCITUqdvdUM=9ihZ*MWu5-OMBtcCHCl3nVu`wQVm=xB2Vmj9-wW~llZ#} z2!-JHwt2~AFs3nmoe&dhqKU!0->np@Fx(SfOsUAuS@GP2Aqv~GnSe3KNCXM~QIj7@ z0!JNAdvOi&#Ovi&;qohVrj}tL*)sm{CcPae==uqiD5VzfJ>E5^KxN1v!qf^A5y`&X zZe1spQ22;PNv@RC<4Vm=_)G1XMT=9IP+=z}Tg1L46VJ9bE0K$)c|%J^KgvKM*;eN@ zOKRftg~?^VrhoFCh}UMSU{-`Y^cF<<&HhLfCEa`?VOzp$T3tkpUDtJ<@3%r8vXs&V zbC5`ocaDE8QBZQM_zy5%5Njpz8-nS?Nt&Pa*!#Mu6(k7B*wK~TAhcqO9{bHKyCoa` z!|SbY5z*JrJB-&Jwx0`RUu{qKVwG35J`lYrF5>ohv1{<+p0(BHYuL;7a;73IxQ-Nn zNs97s(dc}UC8^H~dJNA-J$>;r^YMv)6Gk$|;?rRGr9f9Ug?ZjD{4F}KUTh3Kjpd^dAM18UFEPs!klDGqvc@$?l2808!3d zY!GlsQl$}_cY9glL{+<_#mSc-AW6pkb=iqVz1T;rjs8qHFX!U)%?GPiMx_E+hQ2m0 zlSrq}COtuC?aq7=%%2CuGENSh{ODJJqC3pERwM zoh9Fx!EKXYB<-rrm%CWt9gshE5%e5z66h|%nb?_nhmB17PrQh?P$K3>a8(#DW!Wy< z`tlrls6zj}IJ?se<-x6dx-1kA`YEZi(IZpUM$Uk2?@VSb+BM(vYx~BTAsYlLI9`nQe9Pf#c(X6s!gFq6<~_zcE0g; z&8am39rY_WsT^JoZ2Z6gvu7C1d-KCjUbk-^{#L+KCk2FZR-z~YDC{q8_Fe$ozR4Yp7X(5%(dk&aeYCH2MI#kerFwB?mOE`FK4Q=3sTzTTr6F)yV7k!~xq z%Mzp+?jv$ZC@4w)2W4*^7G)Q`je;ns2&j~_fOJVWgVG?~Ez;eL z)KJnXDc#)+T_Ygf4MTT##|&riec$i=zVn^yT-VwEKDD1`KYQ=B)_t#gl@Ic3(*hCX z#+RbxcTQID7Os|b-UE(f737Saz9u?B@Fq^96M{n{>z9c= z5;Bo={OUmyD~@5DQ7Cc26cXiRVb=J+jt&S!&S;sL01PE{f{aB<1!@oLi%oini zN3b)IBTdbyn~%^3P2&MuDk&$QGnRG4)}VoYrd-+%^txMXZd)kWelx>xA7)AxAgRZ_ z1EyBm{h(8YG(XFKiIj?FU4UTMCZQsd&}$SB=_l#)mDNOhLmJ8tV{A=xh1hFn){PuT zLtnCGrao-d%)P>vRnba2xuf)xPt_ai7YX?2_C$D-BW1IMxcg>nj!KH zj}BFvT_0DTG}`bYHTU*4)_h~LMcOucZRS0d^=-6zOTUJemQINH+qvCyR!|f#v%p*& zEj=xVc~TdP@~N2dDXHn#5qYuu;$Xq9q@P@EmD70hlGF%ATSg2@gyTrVs0rniiY2x? zjLLcNl~CpeCV~Gq9AFp$<@sq}Uk>6co42zOR@xl=ZsM<$vwR>~+oIV+KIy81^oj9G0MX4bsUUU?CdH8n+nh8w zxscD?bHDZR$3hZwN>y{!*HA4k;DO`kxu$z;DNRJlgJ!ngboS%CWY6|Rxfl8M?$fx7 z#df1zQN%-7mJV z49Ka!evPwqJi-(r(s`r_=GFT1l%$Kb9KkF3-(iH0S+{RdSyGOYPUeyZkrQJxQ~gk7 zL)l>RZZ|JQIb^zF<2B=`0sE^6^AZW^DPe{xVw4M-U9|b8MPa{!ZxgLXE0PP>Udl`m z651;$>A^k4$#)dkWvng=}<6zQ!0~iW z^VLdvnw#8b7eexy;`}C&uAXVM#L^Y&wMEss_<_c@@hlE(MG5nR(> zpNTDnB;A&UHprtMtakz>r&xJ3Op;?If zNk&o#Xyf{n;y#-l2;%y!B3L3tG>TAuJ4p6A?k^ZWjy%DjMh~3nL{E`CuB1gCH7g*@ zHwQ3XlcNDJcxQxpSt$JWcE24a5X0PV%ozA=C;HBM*a-QyP_m;ILb?1;8f;{yWGNp< zsBg@_HNHgJ&WIfkLBOozw6Dkne+us{pRD!}No8u|gCB@QwKR9mqW@gf zNd@Nim?T;e7i7%jm~(2ClvFbsEO_N~1Q;Ux=8rZ5JtBClPm$K+9(TnPF$>^nY+C=< z=G{N6*~9v+2pCdXBoZ}B&{!9D>{c|=K~h4E`Yn6HZ^XW@6_XYHO`Wo4OA7Jx3G30l z5o;k!i;5^8=Sb&M=6}6}gD80FLyYm>ozWW8+_U}IYOD?SsUTG;RyJZ&M6?Vh1a4oI zUoyTcNGP42fFn3WMUp~GT_-xT>z72Q%k_`7XvXlI!+XExQ@@^5pJ2q`??&`W)y8GMz zUUg71`V$^5?u)0cU+R%>2$wJDT&xi#8d!Hk(*ShuOG{*~)Q(L4@!H<2&QcqFens)( z$#D`+6a7y$wde;zPrI3T%B|tV4GY_xPd|D`;ii!!jiltSAr`97@TP5`RmR|yhWljz z@JNtosKijawaz-*{rpnCg+l9o=gpQ7W$70w|Hn`XqCuY`5>yB*LrF1QnMnBB*xog` ztwHTCe*j@4KV=NQfff6?ZQhyof^LeWtFx)zxSZU49@&8-o8D2>hdQT zJX(&O&EUDXD;NxYfBTOc^nP;J`t$d3+8R)DZFPpfAfX1K39}ku`*F)2q&1d;xXf1acr2irmVDW27k&x5`k$7#TgHiNxoDAe!9*X zR|6k;@r=r2Y(*A9e_dRyG4?f(1?zP1ZibdVQ zN0Nbo1p&(|ga(YK#I5l>cr>lkN8SX1|Fr{^B$Ewyw0H?lB0SyR`^wj2I!>3S?7gX3 z(eWt8DsX#-K}ln2^9q~IY$_5OZ~Y9(p;$*#k*)RxQpOFU<|P3Glg)(>5`vpj$2ju4 zmW;nY4g2EGG#ySrdBYB0y(lQZDoGKT-IxM?3J5Q2y!reRwKlh#k!BUb-DTEiakkxp z-b#tB1_AVQZW2MhjCq8p6a$!PvDeCJ8enZ7nZ+<})&o6B6`Q|geq^WT`3?9`6MQ1m zH{!gaLh>M(t{0vcF3zae@DkOwIieV-tcoqDfE7wGROU4hSvPBSaA$uYdl=v<7cZ0~7z9Y*B!4_{9#K1#DJ z>L50yusA{63sQ`dLtUxR=ifn}dMT*~^y|h)hSO;{fP{O81HJXm3?VFdBjII5b=YOF zb#s%%dQMlx0WI#TnSZE39IV3MQ(2**jKqW#{NZcp#9&2KpqCY+2!(Wh*A@ zIo&x=ZLn|8r?;(;8LP@f`X1!YEnngOyM+2V85#jgtLsJin>0nmmIlRbfn#I~?)%m0LzSSWB+m6${1%OBqY z?{_OYByx^!28gdHoR8o$_Gl12C5)YjmFj=~8Pob!>l$(;5&j)Wcc%2~tyW3$XCI0)U2}O&i-x%M9d#q3ThZ!;whvfg4 zA$-;r@_!n_-9?!sRkDe+e&|OD3;2e+7tfXhiSIeq-neV~eG z9A~+C)AoND#_smzex-Z>l|)KM)~YF|npyldKxZ)0eKl$)%S5@fwkO67?7$-zqu!9@PN$u|+j}XQ9kbB5Cj>W%vbVe_|mup3XK2d_4=%(BUJEBJlT z5p2Gqm;ZvD4ucRd>mI`rIlnw5-`q7|S;Ape%`f)np-hqBD&_l%>*zLIHr}??E?&mB zEEqUEJn*{3NIbYvo1EZ-Fk1eH*Wi~?YcG(rB>$HnaSV54mNfqOpr52Ay{BGu{b;&V z2pv(}3}cLJDV!io-!U|nTo&awQJ~>Q!@u5pt2S({D@?%XjR=ykTu^`Pc&O^p^MVpJ z0ykd!f$g(|^4Cb)69^B)sHCJIM^qBHQvGd96A`ghadQm)#wS9YpPWl=ZXEp-BZv*j zn^>75#0?-oPNJW0F65l&^1hW03F+YTPo$nC9`Iy(sy(rjWPZlj?HctTUa|k|;0y_r z9zA;#MQ(Oxb6Kl7Qc3Zx+qz#M|BYG5D zt#nb;)>@aN(M8ev;!pgrvKD2GAmS{qMqg`PoAc7!>^6KfTL0o@HbIS3lqW2?@@Bs3fONWj|j z3*_`I5Ga{h786)QjohG&GbcVg7Uw)`J^o3UdInQ#x6In9w!Me=UmU4+-C-(NJv-FV z|HtkDU&NRSRdtm97vaN?KB$9lsB>h^A~@aQIT<{;Q9az!>SDk=PIGL8GC`IE!u|Tq zpkP8O(_IPYwd2+M0+peolXR?7W+lWy;v;T&#qoTtd_k5PZ#)4}Jkxf#s6B0J4WK8`?<;WG zfCybNjGRmlJj>DW^zrI<_AQF{OKnU8r+DQ#iJIUYZ){rbg`!LzKr*@RB(CIv}F zBQ9j`2*hqZ)Mrn;WScOM!fv~nBG3IWI6g9Ip6}G{G*RQ3?#7m1X>`W9TDz=^k05f6 zWW2QrOfe%y(n|KgOwBO#n!8Iqt(qhSb7TCHH8UVzDUgOnwg=MGdAd`RV(Ylz++2+{ zkamUWLTM*t?VEO)s^k~0GE$hS9P+yVEYef&=9I?9#p5mxQy`5PRB(j7D#S^zFjDN4 zQFQp-AQBBnCzV~8Hy?sN6A9z_F%-sQKeJ~cU^g`U?QMxNnWqN%7`!B8DB5PO7NF3sYBF5{e-nA5Uyo78|K=9SIrVjCN60WNB zpB4r>Jr^04Anrpz8yP=|RAPbXIjzKG>p_}7-PQW}d9~~VuWxKk=X#QCmayBG?b1UN zVQ3UBMUzSp7P>ca*1t4r3tZgphtEMgH$%AvIYbtvQ{KN{O6z>FT|yzP?O#nE;pZank}!ZhYV3dD>Qzy#dcHYV2^A3AIHP&y z#P>GmiIc)NA(r+8{1vj)hBGj7-mZZxG$`*sMoKD|SD5WbO`bo@dds4|kzy}`#6>1~ z)L3}&6&)sE?=u6F69a6e^Uk;E0PD;72ine`cAX0Z+Z-!aI%z<*(IZjgW?_8!z--w>f7VfG%+NYrSLvw^rwB5@XI)#&P2oFy+hNK=%bC(Eg455kA!ak46|mpcMR|~qw-XU$ z9+zZOnvGw#5~;o+e6Z~tbsYKMCHXNT9_*%!9~M91b`Y8Ap-k${KzJzmC=A=OP#ig* zcjjHcQTLj2Z_G^jsYH00#?T!sM|`~U<;Ey>dE$_N9xMKp9Nc;dAn*p+o7>x0!=~yR zvbTWZV>KZM`Ei%ks>uAS$r9ZzEZzp-QQ~*k!{Mm($Y5@U!i%Fq!G-8*Ym(>CtLt>l0AoK%X?Wla`0(8CEq@hETA(BcgY^-sh}#5JBwqELs^7V?W*inhB&?R z^z10-MYbcZ7R~9-mu0hK88&}xa&sf-Wwt>PZ_EmQ zM|#>(ULyNo7N)H0$?e~?Vwlq)sGszn9EmLUhv54r^DoQUB?UivB)QSc0s`&1v7N^j zuhp^pL;Ig22?u70;#`2ff97kIJ4>l$7qsi*euk8x>y#mk;$)R|T(W8)-se`7qg%<* zbn(u#jCD-(T|8ZVA_XF?hbc>#&UtT8wQ{*g;|c-61`~fzV`i*RcuW~WG$<~DBIe>y z{$Y2pj?zK+XKv1BNw-H_$e+%_XTwS_6TL!X&}Go|erlwS83Vx+U#~{DEwajH#9FDX zdwGcvnb&`BLiBi(kSs?rdk+^|3<~bp8OEKcmplDc2$A6@jUOQO)fM$0BAKye0!Zj- zX`K{vaVBYvJyq3!#uN#nkyUflb5wtC^t&GsyS1j@i!{p~)`@Mb4AN0jIs9zSs(%nu zUDeqq(_i(l@)x{8LgIa|?+M8qXJE>3ga^~SS8foJqG}j^{`WTC#E9%esFs0U04=6t zOy9uS`jfWsp{P=lzeoAd5YP{a5$uibt&Ry;rnOf(^pAX`IDPCtaatr^9|T^4(u)*= z-b42uCDBVXZu3zJL!B&+G0n`s|8+zjyBJ^HQi14XE;4}!=q%tXL9 z3jFgvvV5J4rH@W!ie6+#Ac%eUlCSQnkVOfw+muiBqM_*t*XXPpt?lzK;B&GdgZZ;`Q5k zW%R(VgM<6pT7^0NDl{RDcZ+w}bqFcYvGpuvi&2xua3J;Y$+X5zuzp18ExZow!Lvkb z>LuvTvBEF0LWi93scHyi^Uttr#omqO2Bv2|i@T-MMyKQ5T;RR6-hD{T!_QVBjmxR? zKsRTbCD-GW>shZ7RrnYo$Tr5P6$DB(b{3zpNqaLM{{e35_9Nd6hg*K3U~5tP(CR_j zoMmquR`CbBo&a(BPc@&wk^-AV>Vw@=5@8?LScSp*yQg4kW0u-<(<`PtT(@}qjs`TO?r zG{eaFd)R@kQ7E!bD%h)D46!XZb$;?Yn9G@;%W}tf0kQT{GPo+ZJH%<|rTZovo0aEX z;y$s|g#{^>KwIH)v77V4E1kgazrT$jg#Y%AN+U6jSUrO1RJvWjJp%6zumo5y+uR$0 zv2nPdgxg+;;;Udl3(QUBBqTu}8hzAEX$bGX!Y>EiPUfs0dL`Z6+Ev2VbdI*~`0|E8 zTr1r+__pn8s?@Iexe`X@5mabg%=(Vo5%u{2OOvA~hNq`uWcxRxiV__bQ_@vVp}Wq> zwD*_eVlqO4DpQ$94=t=~q^9}-cWI#zd5;HpNu)8B&d5YLmZQnWHvkR({0G}{`yhxg zEaa^hZ9n~6BoKZbCP>#i3;5d6@-+*P)?H#{s`KRv9=6Jc7aiTBWn zb?BAC%TIAOsE&yz2`4OKe797dL#gXx%Ji?gPylitYlxLSt6g&K#Oz`K`5(0bQl3z>+fn`b;g zkDaYt6bX#hme3NTBd^WYx|a3JdfxGB<6IkEKA;`GgiA}^bq7i}(Y(Iq%nz?Gt$>2$ zU+^@pBd}M?g;d1aDjV;4;}fI|)uvT#8e-!>fYiO-Jb~*{DyZdKyDftp%wvwVyJ7DT zvk@X2(KOt2T^vs41yY#DWq00PW*)N=lK^%DrCwM9H~S91FDKTcHTBvMv2m3}w#U}< z85oLJHZAFrusFuFTcc#zfDd$1e`)ROT~VhPAMPuqq*Rwzh?Gm1w4BZy_|E}gyHVAv zYxXFO6VnxaHTNM`UbiS%X037ZL9M+?yl7q$;N z0;ZQ><+Pi-ZNTNJ^}T`aRL#+!nIU&P_>rL(=yZ~$(+?X?yb02Ih^rKV=z(m;R9#JV z0mJv@RT0h&IU0gjqV<*%auIQ!CyQR^;hq|$+JW8~%(-`Cpn|n;dUzkLPM;1|#<7## zo&89(l=qkYb>8h=qz&jBTTH&um>-ZQ_M}-nCZg**T^LJ^-dx(S)RvNl9Zk-&@ONbPd zh<$BQqHEDP+5NC9RnvIkuv~NMq^v-1(fDpruX)DRP%OasvfuEI(PR@LhVV};;Kz4c z`CEtHWPLTf8TD=WB|Gr^_;Mh+*S|y2<2>IF4G3zTB2eie+~?8%)qH<}b#6Q~ik+w5 z7>r=i_pjSLT*JZvJ(w@krvv+2?|2N>up(^xJvJjR8cId*B~&^rgRKD#!|9_(wKgQ3 zSQ1`+Lf;DpSnCHIBp(id5u`x9c@-VXDchIBX*V@z+0bMglkMy?5dYm=a3n_Ux#5%F zxOPE?Xp3&JxZ#Lc^_IgC;BE6v-?MIrtNSi@ocg|2YVGjv7v%t_rBm){jy$n}dtFf84! zN;Z`2b;|Ic-_Gs7XyH>?OxB36106OW+zjc`*%r7$Gcb9zm*fDVj!p_+mOOy1V(%ncc`Th%1ZpqwE5C_{nK(aUJ%OiqaH$Cn27xJk`SUk9 z>mj+}-{qx=%n*pN*-Dv3SioD@R`AjiHyl&Ues@p*L4g|ZF!CNTtNiVb?gp~Lw@h$> zif*^h!U^sa()s!NespZwrLpz)V^J~dk6Z7zL=MzH&?WLD{X9NgWg(i+E3MBd@sYjH zw8&(CW)~YbOPpnRVZkM_@%(LqZhu3P2>th+uhBV(%62-^I7C$7XZf%&iwLSau8#(8(4oio_R(1A$)|0J3RfmZNuL$(b=@y6$ zeyOrL0V2fbrmH&g{!e7Dls?AJW=x8cLgW335MW77!vs5m3_HQ=Qde`ZBzpXl;TozF zHl*s^*>?FhmnwBaLAGy>R#lj}+q>ozl#vcHpGsbJsS9hY9v*VM_KyHby6=XD zclR4(9F?<=MD@-aVZuqS9Fxp;)~UUB zyk4Iguhu?Ud*Wu*&_CRS-FSM6jhZ9>j*~O?f~OCYy5A>uQsx4lumSC@j`HfZtXYHH zS?K7nNqpg_o9;H$;R>$k5NG=kHE%94|YWkO$<$PX~#odiEI+VE1||G zO%bvE)d|J&o4m)pyoP+hgZE+jjN0d7i#Mvf^KSxin2k1r)$<_voE}xjrScBDAbF&F zy-RS_+C3YxS$r9nSrqd%*4|zZa$gnpqTYwJ@AvRul`ScmzlZ!j6U)M{V}uaFDot{ZiLUJ6VgUWV5&JDx+pfu%qGmD-g*U1mv|jjXwZPS+cpo3T6?| z6m-20olb{mod#e^m`??Qv8&`KTckm{ciQnW$Lrf??0bwT%U5>%CcveuGbU4nLPUmU zqZB?Nxt(>2K(FTo^D^mJ!&yht6uVK!?5RFeuDo4wB9@UsMk4 z4bxD-FqnjAYRAC3pNM?S{$6Etvm+m?$*zRw(K)MUeNH%%!V{6kcRGCju#>&Zqv9#U z&pmpt8XK2j!<{;Big#}Kc;X1fYmSV>2B~=?Sl;o(Cz)oq1Z0uS=n(g0glTQ^R8r>~-9?h1x;R3b&ISjqmjkMfLKxFw3W^Tmk7o+^e@47g zN!Ipxuya5(i)`KE*4CR%Pw*U5mN~z+eeD;o)vfX~l5U%M$Mwham$fL&gu&`3;DxFk zyPeB)n}eghQFUjTxx8y-T?4Q-gifGg`hKs_;jeeeg#QUeD@ebexKlfviXUX*6{wVX zW-$$38?;XP1H&-|Rl)CNKh65A{SB>aA^W0M(?WPbO#M8}o(mIY;diOhS5ZS=&+9Mp z_Jn*pW<&JliR9s@ExG&4!TP0629G@&2|;`o7ws36>!LeWpa9 zB}{Yxd>NwLKn3wociO#M1-qnCAtQx5oXvu@QRc60cE^?4BXM zTszFIXg=QM%WZVt+~OtRpwYdrd9J^yI>Fh31yuJkc|lRbghjxZh0&3?hBxAYf(QSZ z-C)lXRQU+|yZx3@z=-m0W7LAjxb6XT;Ih?qUmk*Rs7K76Vwi~y-G5iQ<-1Z^R9(rt z7fFt=Htut==Btfywwmq|0gcit$=P3i=`6q0&-EIHE9u#f>D_D;T;GJP{b4b+#ze4G zqMb=U-o>?Et$GmjqQQ0d+XI(ar{Z^wQJ4)Ju9$}DxKyF1+K}B# z*J1jJqY0$JXm*Z0zxi@etk~=@%!(_{ehI4P;r?~qf|73 ztEqFJ)bfL0h*_IKm;I=<<{TZ&+>%G*A=EO(c!$-W8+vo!qFr^AdOFDfjL|7qV48?r%r$pT8{kd&K=XpcRT6~u}gJS}$!fvyeH^1>bRp_o=^(DQ>p6$&k z8Jo4+JpSIS}^lT+o;6f|x zSGM|(rj?k1XlB?hZ9G%iK&80)9#5L`Hz^C2H- z8K54>)ISRfOF>5RRtrOjK6w`_>qRHjFtAw451qIUUn(vY*-l7fO289rTW^Qg(OEh% zn#MZ+xch-<52(f-Vsz>GCR7J>-R+H)P$FC}G0x`F(t%~le#!b~>CCyB07=Vueb<^? z{Bk91j*0Qp%K$5hrJ&YBl86KCn6gp95t`3I_u<_HbodD2#V`a57pb1&0EH|OHoPUV zEx-^+Znsy`RA(Eg+_eU|hXy(KM{bbNjz^G=5A$OBx=&|RAPx5+zmbDQwGO`#aQLDx zq2;Eh+0x0p$rIgdl6<{Z-A&o379PAz{L0~!_Nuz3H!kvbbBG0t)`kNUz*&GF;yL4) zRP|+zmF{BO!&Y=_?H1i;!|Vk}s2?1Pj8y;R+zhe4hNSet?*Zw8m)Go}5`8JzL_vn` z+%1B(p`Yg6rKA$KU!Ny%FS!?Np24b8E*u{kU<<~nH1fadPI^zPs@BXc*T84exf>Dp zC`}QOX|!+0PE%j_P?kr0bLKW#V-pm#(?Iy_FcRzDnzyv%%YLq_KK#VtCvY0f zBV5_YwSC)$OZ^AkeI6sg+^^G)Kyj}|PALvACKHLLOR?02R0rtZw+NIS329!^)V9^t zOs{|U88V`HmH5AcRXPEGg}@NQGGOc*&}Qw`A`dT?PhMK*UQjXh8Po5Kge+FWCzt^6 zJ`~2sr60-cfbb4b^MZ;#!LW8t`O={igIA-d+WnmDKag$pHw-sPI+gE$XB>W4-pN$%wPcc7N=Lxtk8Ml04FjvL8VIn|a6j*a zBm;!#iSC2i)&Ds<;qpw7un(&YOzb2#zpr_j-0=zc_nAm`KN-l{E0vETvfa~V^|aOA z|8s#wQ+wNc!6UUU!LiNX{M|8_XB__ouo36KAcQWh+$2cMSH(-xpJ(n|r) zvd(PNeM-t|MEu32Wf3vHh^D?pt!pNliaudI)C#inH>++wP1fVtp;llwup<927SP`t zLzTt*v(T3k^@sTceXh<7lZmnmG;Mw0vqW~hnzC5{Kxlx{y?;ocJP6HK%;7@Jz57^# zl!$Q-8^X0Pmg9@2IXn}LwYMI?DKov*a03wQX>V}JogoTrBSJ-h0^on zQfEC$hWo%U;sRBjk22i*l`~)c?KHwrV-yOie^OR!Jz!V@a=Rai2;Qg@L>x0^cJBh% z0<6sz`A`FH`u(Y#JF7%v&Xlw_$zWF}nY$mu*&NF^*kmj#^_40I@_{+x>sE3$w`BhO zR$Qa>y?na?;m_4)!^D%NbxF8-R6ez&E)ZRQD49{cDUDGLkZofXK}S#08pe4X5?QI1 zPa>nU$-^nI(t|~t)CEBv#>p1FC#q-qic0D8M zg`d@PMwWK^IM9M^WG$~PPa4d^#`EF2oN3(ViMoCg=g2QXlh4=kj9*OZO5#8zsKZ8k z9JmlwXA1M-NmJB^6_U2GS~k$-`KD0rq6p$Q**^(@WpgHxA;m8U*I&jdQCpcPD?0O& z#HR=r8o>QL{XRFd)j6U)dmKHt`7yB#^(8iLd*zH0Mfpm`hl4s`sZGBBUKQ$*dpgL# z$_VS?02$xoq8U7$v3)EjQ8o55sf{Pr<<5N6DutPC4ldy=QQjl;X+ahF>QzwiJ~Ou| zHK=vzy1zU@+byl*Tcz}La|hXWIh=EAF9*hTF=EgL$)mDip8=J+H?bMd1a%)m#`m!D zBZah$CQ!*4)p9yLm77^LR`KYVIjuGxR7&4dBrH_PWs3 zgmf?Kwb|UzqB~u|d-oziFW4+7CO^Lltj`@e=C?!wR7p^AIm4^H{|we$%nhxQ0YV;l zO?rL`#g=P9yBv<^j+h`*NiOrN&cjhYg3!5_r-P8^g>L%JS8@=-^P7B2Yv>vE+;D`p zKue?3i(^_T9)Y_a^t+n^^?laTz#74;Zv2&2K*2Lzd`*H@;+6X+$yk@UA265XIut)=Y4SiU{CE|AiGhkolAxo<=YJ6o0BOJ7q> z&RHs^nzp7&IQ#MjICn?=MGLRCCA5G*Xy@nGIPuaibhSG5=HzDt0w5i) zIC=%T{x=s#GQS}6(G4b@`Ee~T_;$sGh>3{bM4QxS=nHRXD;_Zujf0umU@aUzMNa00 zO1LHQym$8NvndRi1?BLp-OZ%BlKY%88tJYagxPMthyQa>RMnMt;<4*l)P? z5)Er#G>oYqP&zIWQO2M*RO>vhOTxO$?C;hu+3O_rv*_sHmusGzXi5cFze8fKb4(!xD#As=^Am9Rp=MNelQF_= z@h9rjrf-dA?U^x$-b(}S9MfOJRD}K}TVgJg_X9~cn1)GhCCXAKJ_!RVB(G-5dJHcpdRDJ`VcE+6E%#Vwrl=vlA~YNzg8XzxV=U%i|xifuRH zQmf0H@)X#jcba*bGcUT)d~Wky{|CkQf%9c8?FzJlxVb_9)z!MJG+h@Q6Vs%QY!x5E z4LF7S*I$u3(j2ewBnCsAr}c3;W(Ekl8q?T|Ub&g6{OGfpNj1D0J3oK>xlBpYo#INt zT=i1Gu@bb#$I9qdp_Z+zDr9QZe@n3tq!C_ynk>Qc4KyN6;tTzUW|*VzcgH@_6&^+rdF&V7c}`Jpx{++R%Y(SeAyD~2YL;I`I}(H zMSi7sKqG>|+AI#kp7%Rdyv2op*-wp6yg0O&bTf$9LidM8U#?%^S)WtYb>?tuTj*xa zg}yAa+Li*dQz)9(v>}VTXC%zqd>Jl-4NdDpvV2~~?JoBr6L7#JESP_h5etgPC03j( zXJ;!wc9w2sIni+h4bkX+4F2j?K!|%HoQ!EkGTR;6yOpyBTb{JEi#i&U-aka>J%k`` ziH&FA`Hm%-pT>oaa8+6qfpt}MLOm^-ElMWO(py~AP~FK9m%#uE?FuDbhKi_OBTR|G z`>*Dp3(u!*gmGUGd+^rG4>o(_6tZ!dlOV85E|*FvjtjUU1Ah35Ef=B$Rf9HeXft$& zsazzl6XiC~i!*~+NH3dfq$aTfN|N-ib}#m;?8ILX@N!w(jd*|JiW|tAPG+o8F9E|* zmuO{%hB(#44ql0sg@<6MX|cuB<`SSYTco#p9rtAN<$lapSuFo|s7yZjIo`vN!Bdz# zfc$A}do1`hLT)&i5JUzxfUr`iVGEdKv2|;h;l9+b5m$Ak6yE6G#9(RpG2r$4efWsF z;Mkbb*Ec9maOvz>_58Q-f?_q(6^XUE?^HxMVd>xbet)Hn7BZeLAPyKTo+d2X!~(4r z3j58L9jh0ww?{WHfsZX($Ju1}|3FYTHF9=ql$uE5Ct?|8U_SmGN@n&?i=8RE`Ucey z6SYvKk8spE)5mkxKp}kMPsD>|#^|>MhDnQDO4GAl*@pN=wQ{cnYgacvDjt=Ml=L)% z>Qwzo){E)U<dAKfe3Z8*1ng{Cg(WRZd-8Ct3(m+a%THpqW0M7J{KM(J}Q zF|SND4F4%yqiC#6$m>6?ZEiC2G$}OWf)*b=?B%ew@X2^d5qE=yZh-e&0m@mLN3qGK z5C{aWQ{LP#z6jNZS^;xfHhm!SZnQnRDfGN9e~vq$&iVxMoE@I_dr;u|F9T281W>D7 z+!V2NW%GOwkho@7DYjtnD*xqKwOn83J+<6<`zyI?MN;RQ%1sKcsEHT1ur>JUIFE}7UPc$!txF9C^ z?RZfae)SG;|D1%7(_C%xYg7O_028P&HbtX>CiIcJhPDWR_8N%c+uXB<}zv`lP!ANfm1k z{UKhC>k|!$gLe7??t&aj$*^+Mhjhh8->a5~*+N4%g2lzHscM%$_8!C2XpQGZDP>2p zHeHZ6NTBj$r_485FHR%ej6o?BC++!iI-jRJ_vgwbaG4WN#2u7h5z3`~fY*;22^!J} zmBbKDG3us^1bQKvu|X}xhi}}svj!?%ly)lN4c%ri1bO#K?(22S_epJgggvA zLDMFU$~LKYa&9&=g`9g4fNS*&d={1|piMmgY91-;-4XIF%D3eTciSU{=oagnxd&V|r)XCEfg1+UEuiKG~M(!7%~6Q!Nc$tR;m9l6QJBn_&W%hyBTslV{XN=76fs z`y`ZlC2n+twYb+e*y8Bz9!w4?xpls&Y~E)Z{ylV9xFIO7rDcjS%4L{JJ90#UkR)q3?LM$P?rR1FqJx z)WO_~^rvn)H6y6+Webncy1*Nc&vtnFC{(T9Id}z!$d7F>dI8-(vBQFXnYzyuem*xyfBwO{UYt^^c5Wwj%W#kvF9ISVO z_QIx!X!YQWPbeU)L}_ds(kW%v+(@5-^4eveN)Zv&vcl46N?=D*dF!|6gvgcTC$GqBp~)`I$sf$c*$C)q zxy@e>;=9Y(9_2fOIcG@nX-Hi4nX7Ej%dr)nzm~ew0OM*oZkyP)A8y zIq!H>EIh(}bq8NB4F)Ztp%HvrTFkCve=w)5Ot})$5n>sB0qe1)GgH-=4;=Rmn8$mMAuM32hiL`{+&!Q%xGts8MQl(kXgsvkHJ-$gkwc2VyWyee<9t=AWn@bvg7NjjaFwuME!lWn< z+xog?DPg{u^z1~R#CD2bRfZjQC^@KC?NJ4uWzSLDI?{ePEGkjX7X{BHCrUGj{)+{~A2BvB=fkv`$wT`1{x9a0OP~2UEySs;+K%Zy7d!O_E_^#{w_WAbu#l>2g zYt4}{XU4e4eUF76ws0E_SVK_2PsmGUaS5TPg^ElowgGUZFB33sMrfXEyfy(o9%1^C z>O_!izCfg7VgZ{z!Jj-ZMv0A_(C*{q8gK~AUPM+13FL4-pNRN{%HaK>ANY9UmGIuY zbDd-Xl2C;UH?1TTy`||iFb=swq;$sCb;^K>{TjcGQw0{b!_8R zImvL<&P0qsO$DeA^lDGG00+Hq++ZJK;3Z&|nc=^F9fPqp7Ge~@G@)lXK@LO3sUiX( zKE^sTSmpH2kdRtXL!&GtrK;-2F_3?b4XB!-s42{8bZJ4&lFuCbD&altEbw;zCWSeq zToa(7)^VLoU0s3AGVyL(ko`T_*q5%7X9gKul&BE&XCX(n7R5W~yzKeRE@l%y5$6)jSm6-q3?B#L?&5^;~{0t?k=&TU?Nz2lHLFqf8IO?Fc>t$Kkflg`Lv+)@_m z?A3&tvSgzm_-b|}DPAr{WD%;R2l!a2b|~~&>}~YjRoNIX2fSZC+vse?zkn;_2f;eUk_tz`sNDkK>?woz9 z=lKWLKb?v-pbdwdcJV{)-6(+=AX$dOuWr0ZI;N<_P|&A6Y^zuaoym}|S+6!Q7RWLF zwm2|bl){0QDik~P@dr+eDjto*r<7)lhL2UXILAk{XWjBtFyaBm-Iv_)F;uJF_#Exo z2a$hgSP6yy%CL$;ADc}vDxbKyyGM7mL{!_y=xXI)C3C&e%04`cF8P+t&NZ1%Nru-ZPW?UzkGTL?^H} zNVuH@-?AvNy-ZhI7gT{zYiXIpbUlVUVh(W@`Z`O|@k(SJWOz9-`b5hH`nHo;g2r$~D0; z-rpH58k^GaNF1AzzfZkpQ*W&-cxuzjZ3|?)G+V^%YIPezr$rEF-ea@x47pDjHEU6t znhIiPD*4gJ-uM zuCxHk5H$g-R~;;6m^GpfJE@VTM6kq`$U3)x8FhdtxTxnbdC`Qn!E6zj8S8c}#-Ykr z-F82*#VbE$^K-p7oX`cZWXP^szh$cn3;8;+N;{=W2P#k!B z!2NqRBQU?5ih4+=&+<(wo^|+YK)ur9dbGF9G;)sByL?}P;D?Zy1K)zVmvao+)wKZZ z*eNv+8PO7nSE}Y*_!Tg!&S|&ug0P~<&a%|PHW;AVE8E!m zRlI^#)v^L)&UFQq)UX#jHQR2ezynowlN|7t_}|il=h9G#mDd)!4TG}-OafR15eX9q zUf5h(+qT(S5c_4+3C3~TnyKy8yO`69OO$t8zs^H7<5vzACBX3~jDd-zOe0yc2E z@FX)e$%4L}Xf@@tPvIP@)_eO3s$UeWeA1gXpHd1A?FvX1PDEImUT1sw>{#sYlxaeg zFqVWqP3)tASp`7q5u1m5thaOHn(e(`{Vw1Y$fJ1*Ad#q@{bbBkr8y}+>2nEJy|mE^ z6DrA@s0p0W`e|IDNv59IQn3{k5>FV<;Xbndiz$B8qJu~4i|$Q~DkI1rU_M5-qHWJ= z5wZ9Kz$b1Sqav}=(brR35Vj1SinXj{R3Qz~dWHVj?YY{B>+*X9bAnTm^n_{2TP7#z z4JpVPBEk`LG34;Wl&($*f{hvD*sk9vt6geVw-J~ytsp?=T1W18r99M&Q=?>-)i)8)abAtTsyxA{fs&-R=1_==|7|Ch=$y1Ii!!Wz#_x zJn0Jgsu(pT%j=jQQ2Zp6B#r4WY;W)cW)BZL1g7@Nx5e@enUb<29a+bp(`+;RHrO^> zw@^1~wDj8&B-5vwPrlD*-FR*=_8NJ^BO~> zDo?W?E6xhIf0@3oo)Tnhp}BwAMn%3@3pQ<&0Uq8WqNoB)Y*C41OPIji+)sU*4)@() z*Vn7^{oNG(y>geODf_~7TWQC;pW-mv#So|~OZ%l5n+nzQ zQu%I$1osnI^@RM`9tEw11+WvncpFM_x%>AkfLY;tq_Jfh~$-u&L z>lk1?Rf64yzZ&a}&HnLEwzcU?_-3xI9Mdl4N!39MirXL(D1~Er)t?dL3LGV@@=DsG z@X%`?ne3+{ucd~g`X!?dlPOC=^>*Odr&P9P$Ky}wV%3k@0Gt~)0u(4$E&d?5IcL0m zBronfUZdvlAtDAvZCyGGwWRj&=)!S7YEYONRIe?FGi%^Pb$?-D=36p>#NboFDdt|i zDul-~*DZZDUZmWn11ES*CwsPD2_dfQI8xnixEw}y${u0{>4l9!(~pmL4%mBj_Ah6| z%^PDDAGQCoY809hIjiclIddWMf*vgO;|A7!&iP28o&0`==fx(^bl>QY{MM|y4H0c* zrOTD8V>cLBCRntpI{6h>a8{j$|1@-WYv=9xuMFPGHOvSt1~&tV)DB(vsk=+(CdFQ~ zbf6D2VZ9ogzk6hSTn7P49<&TzV#;`47lmT;NlW8UI-J%xuNUD|S}iSAtvQkJ z=l)Gab%u=uV|8!GwVyuOILs{fZ(d$0_DW;_BXmf!ROcnmvKvynZb?r~B7z5o?qb-m zoV6Dy5=+Ylc-kIsC_c0hXt6mnOm!<9V@prig{g5Cocf%5Cc^Kh3ARUGgIoRnL$O2R~9(N*=qp~h1zgMwbiE_)OFemm%PiA#Jn!f^1#h+xToXmlQfdLO=k8+m65Sw?!){h+?el}kXJK2~&XJb@9huc-9wm7m>nK}7mL4%@w z3@I#+2ji_b5f6jrEZJzYz9F>1n6BbIc!foj(=lRqH;6w%PI)e-0;<3yK%pj zERxY`D<}L0#_775JlJ<%$n&zNC+r^r9kHoXmi3o4kvr%PjdbO9v#vgnNs0DRwEO$P?yGq!6F_01)7FOss;c@m}&_1{46x7vd?sDJ-=egU2g~1Nx z)r0)Agao{sI9FAvZ0DckORN2hrv3kH4A6uQ2p%h5)gRT8Rw9|#pUoT11V#F~yTrTu z>7S-dXmQl2^8f9@CiZ{S!?943u-oI`ZT0`H12tuT!HDTn*Rbv3v1#~_czUMF=%LDl zD|5NVNGF-8mOrYc3NwwOA}c&9B^%~t(J7-V*SLGqBHC{clX5BfuUdbkH=HYBKr8#QuH9olVVr>abTpfEH+nK(HK*6yZrY-#%w-Y2a}oK&q(Bv0=dsL#o-Xe zOXo>UE8&T|8c#2Yh!{9+;FyeqfkqqrM1?t-Xw?xIF&Pc%pGhvWt6G>bzTv1*(sY2& zl8=l}&DA`>fYZu{{wVa!%HGo=mvD`4fIzo!i^P0}Cw+$rtS_J;Dxeln7y# z(t+;7PcFBn9TpNw4c#luV5^<8xrc7nWARh_!~}KF27Nd*nG;L{u`j(or;TGT*fHRwa`*og@zYc#a-Iu-xv-9@RSd z>7ALXNkzmkUu|6sL=Qrh9(Lbk+pU4R077NyGz^wwb_VtsiX~gnxVcG?W09RAg)OzR zq#*fON7JlX_S4Gy9!l3huo8P?r)YWz=`&6S1_j8$$x}yj{glD9x!RMc=$Tpx#pwJN ziFdELG{9qjId}TP)jn|%5D)WMT&h?RVk_V#1H7d3Bu|)!QJCisd9Yo0TabU$JE4?p zQ9Q)?`B#4E)Obrx;@Ew$l+Ngc$O_h071aouZKR<4tLCNVP0^fsn1}R=%CFUYc^yP` zT40&3@^4wuDWER?AHV&I{@zY9fro6G!U0Wvsw=Ny@Ec* z4T79yV{E;j)d>y_wsYkP$P77G4=i^YrRUu8QXH3z1xacwl%)vt4{5BEoX zrSu{OqXLJP=A4avz8c@rD7?*QSO)GpVEw=l^wI(KOV7ju+XD`8hEJg<*;t}QWID`- zc?$%jXHq6|MFVf#yX>IK{y}P9HntU;*I0g*hjv6+SVa3I94w+hb^8~?SgCx>Q&pES ztOgrkOy#2eH&44f4cY=+M_@B%w1#(IdR)4sn9NJ%hk0f?(6ubX#MaT01P)u4W*NK; z!BjeDbjiP%hLk~~B&QP%sqvQyxvoIva+n)DL~=&{ zN(v&uD62F~NapS!U~;hjJ{5bu8(Dxu>Uflb4Ybg;&F1IeDp`FNKb8cz3gHQ`Gj)CT zhg>2|i~366+t!GB-o%VrmIpD%m3wa5fte&2iU_ck2U}t&0yEq zR~I29s$aiZ8)3*dBBkwDXvC$WCF0A*Y1|(M2Ny81Y+tJOH&!&d);};**p`x;&8bZ( zW*iTWr{vBug4VOz=>g-f1g&|9!zF@?+s3lXq?Qt8guTy`Fd{Z#cl+Z+L z)n0G`EG~I)7r<6n{)NpNf%sZE#K^v|0+Y_EeGxSsFh&iJt5k0V_esO}tQ>4eC(p9M zM!vwt*J|ZEfv!)=l}WgYj(+8!NssJYY#DvP^j)OiluewO{>oP4um{S2Y2I>Qb@9wb zdxl;VgMB$fL%t}yo1Mvm6IfjB;}^hxyy^5SqR#!7KqopBeDq{Q{CBXI>tGo+*(`bP zl1S>U!_4y^o0vodaylAmwU|o?M?Y%C+Ns&`k!kl)$>SDO6Wbj<#xI#pvJr7`|1=Hz z{2`;X=nYokszstop`*TES3E!0=cWgBy`H;-Pm2dkqa=xv>J}C;_+;x=-<2}!4qFLp zl&C=t_Z1$RcoaN}90>8Wsdd@xt}C*{|FhSLuMZAzXey1rBJGD^5uXetwSQxCUiRsM zA(tnEW;ILJR~Dw2ot2hruz#McMWKtQwPi}BrX;AH7?w;yTvUhsMNx(C7%>Tn@u>_^ zvYGNeW5o%(G1eIq6c4tDj2HBIVAN3%h1~kP#-CYgT+AlLrKVo=%zX~%pHMiuabC~Z z++m}(lC8Rhz|opqP{v3nuSsQrlM6b%7d|l;@5n6n;_4_7Vr3pRIZ%+qq9jz|Uj3$i z?t5Vu!(M-2g@|#mV33`y;8asb%zp^mr=Qb%9#L&4|5X-G(B#wixRR}L*#1~S0)Mh! z`+5fWk>f~6A~F=|Pob%1sDSMFF3heNeK?DW8OgTsT)$hl?xhGqBB_W!_A&A4Ohpoj zoulr-dhlk1d~ZDQOQOp9k>qJ*EOAi+4u;PqwML~nG-eHNVV^tBbM+Lqdu$j0g>HhA z3Ej?j1T9)!j6j7w+$c&s)2LIGl`9aWySmRz=f~ z@+Yy8_rg9}huY;=S*!awBY~0I~Q>h6zsoZdWEzAnq3rl^YM0_eeh!#auQU*4IiZ_K za&p7~t(ZaI$^G!6QDvTD%~HXWi~|Bz%_m4_EH+S$=idm8BgoAg^ij@sj*%bLPF39$T$gTZQVOXvyI`-O({jeOMcFuE97CCLH%p@Qk#Ky$f zd&j9mOM7zb;Z`t2z*8!NqC`F`RTmVr0Q_) z$7J2w?Uw7VQnJ(CX)9)CaDtwi8ePNR1Dcv>`G|}eL8opGnwdRyJlsy#Q_9W~3%-)Q z6pvk$7n<7yA4cS0GPQf(B@<|J?}xET3z90dA#S6M%X zhEj>@8{QtxI}KIDl`R#n8QH^hSmmhrk6RLtX4%UA?AT}ne{sRzkfxk(ofkxls0f}O zo{Z}j*6MujV`f*&#)_l_SjOqz2}RHbxVYey*dEO-YzcPbI!;o^0+m^)u{3)&+P7bS zG87Yx1j{gg)Q#DbmA{mt;sM}J{7^TZ_}?$~O7PA>RoSE8wxFM#?qquj%~sc(W@Ji+ zZJlU3)-82|1kC&6bot3c3seJxWd8XYZ#_yPDqk$%sh(a8_1VD9$gx-bzVv&TU{@%G z9afD}nsu9HTo=m^k&Tttl}M+f8rS zZ3MiSe;=_Dr{~GYUTl(9FPi>X;Yx^m9gDu&sov6npV|t1Eafg(_f|0Fr zz}Vxub9IIJ#-Nz|SNey)p}aJxsV#t1>-pck#IzD~xQSoh`LD^?|7U5QB_V56ol_Ot z?jM)dR+meY6bmRJIm$g%O>)~|xMpKq@CkzfwDw5LcD-CzY8cvM(TlyNL2aBZE7OGa z6|Z2Yhm>Ir@0_G;n#-*`Jyrg!u&tcV8UBN%zB67U4f0(kdUH z&Hum(#oM1|J3XqqV~nAB=cMKTg&kUUU5L2M6m`FvvjDcEE4$6?4l9xu6bpS@nn>RZ z{ebM||Jz;dxu9=Izsx#G!pG(4Jg?JBG&*X-Je&}R>i!q1Y&8JO1+THb(9@!NhsB)} zq>Ej&-VQgsPO|?e5zGeRHO7{7GXUBZy^GS&njFSd{sZ^A^xL`e#T5iBS4S2$0j#%| zOFbH7aOjEz=c^Ho#1FsSdC&Yq6Eg#@j%YyhUf2_=tKa)GHB`unbtv5J{pq12xwaxV zQx=Q*7-;wDAO9>Wr{k6IIk(a(>P74vDB_+Bqi8KH#-aEFTg|tbqDCEA zthYhIXdIffluY`}%yy4qDS+C?ceb@&;);JmI)nJ;y5&yJAs)8>$YA$rW$*)`7`|;G zO^b|i?*&}^9DFA%SUa#Ia7(x0e7>)tmHI3s^+38^9>!FMxS`hBWc_2pqIfz^i58He zmm6Jy+0d!MW3D<-fn~?SG-&cmGM_Hst{P*)oKWB>yi!81aedTG^Gla5wY|x942k_xJAT_gu5A zIx3#`$Nk}+T3jDMme?e`7lUqMyEwbr_OG{olYG^hs=k7|^m}(S{z{=g3e`aW^EvG& zg4v)SQw)1oS4fvnS~M`?Z{a ze(yVm26+P3?S|XcgNBf7ZdL}Cnq>V=Uo&@c zpR6y>D1BI1i2e<6lQmpZv#>T@ngv_zeZNP2ZzzRw3Ng8A6>O*qG20s7`=$7xg2``s z123RaVCbWhyt*mgr7EVnaok;>Lz9iiq}I9u#U+YP_B$`o@u5bp$IjNx6sivh>_=OJ z&oKBi55cu1XSJNG`qMwocq!SKxiq`>);Cu-c7`i^pZIxIb*&`VI_?dq1rpW+vWO}( zU9ydR-Oj!X%~EZ;Sh}UacS!_0!60{0X-_!FWQl;ssH=l=ry#b5k^f{}h`MN}3|Fwb zp@-XztiMiD{zf@XQVv4l{>@+hc(>wuPRX3TszUMIh*;Yk>V3z0Do!&(mCcL6)bGhs zJq_qv&((Xa#`7gj=JuTFL{)uhJznD#gj<9bu>mlCJpx~{X&wi^)p`oNnAr9o&^;!uA# z02bxt5IH|O+J7{LaRM1y-pCBV?0>ANKUIFFP?Z)~zun@OVP1UC$;q64=DOL{C3h`u zY}CIJZt#dt*?;%~iP!`VoA&QET%qUl`(ZcIv$9`ktfZ(UP1IRVMl@YX9OH&}B})O% z3_t#WR4CYcSu3c9DqOyD<-;Y7Q8)b*FwrO2Qq^Po>OrpUuEUguLlK64}rRymL3sn6o^Z+cXvO-6_kHrzul`i7(Vws-eg%E+f4hF z(6Cd^ZufD{HD{Q!<-yEv)8So+o>BCy)~c$@qpyk0I+rA6F_bs?6;|}}eqmG7{gRr9 zw1-t}fKeTJH@-UBq}P;CJ}`t~rq1?ewxD&Rrm|8V$ykzXvb8a(7CkojP<-CRxO!px z>i|?8Ry6c}x{CvTzs_mOt+2$bY`o58S1Wdu zc>XBpw=!Ncz)#*OS$$fmRR^1;x9UgRTLhvqM31rp%BQ`1GGZ;nJJ{BsLzccjc&W-W zQEz+_0K~{Ld39ECxFIxp+;O`lc&mA*r5l2|iNE+k_HA)JS*l`^^K+%dTjPg^GVJbJ;ZIg;JzhB@y9?dKl~I>cIwRKS&mUFCp`wZ(rxv<=ir95Y3445c%pw? zU>iF23IW0BUU&S_o{U^?WwTf;=UqghMSHtxHU4^`Q@ouk7JEpk1HZoiAWNMd$Lk1E z7pC_~K*(#&uWCoiGUBMVF2pIdbZK{QwVsJx$YZLvWf-y`&D_Crqw?|HZ+Mzk_KzTgQU}SD=*o`K(BTPuC%+^19hamD%xX zX`&Pame9{5XH0)Bsb}A&HZ?G`In7xV2zgE4PiUVjY9C09AgY2tIwbhzQ*Cu?BH=C3 z0krYAwAoK88)JCiCb7MBb(EKUd;sN%75{=oUO@K z=czwFdm9C_I|(e78|xxk@zVsJX**Kg?tN+EHUCl?4irESFvyU{QeM3J!|Zz1m#=+z zZkL0}MYY3y>- z|0j_EwZ_uOMge#03hm#Wz8m%bP8Dm_=N7PV%O{vm+Z}#E;8NET|E%LMW?#;+k4;E zx|X@Er+!!DTWzo45_r?e5RgAxr{*9O()0M%cseIKshnG}UaZ-rkpGq*Rds7pE!Qd= zMimZ*R4#XA%iAM!#r(xhoeH~5RkS8w<0zmdV1M%;Yp*t!Qylo&k((nPuRG(S*tm;# z)6r5Ikxp&qR|4d4ER@$sSiY{yBNeoo^hNU+vV2CGsZ6-R%kJ*v5n(iwbudUxnX^nF zvkEG>E|e$WH$C+I#ZQI1z%aBYt5IuF*NN_>a<4+kaZTu6gyKT{mQrubBRrX+8*M?Y z_Ir{fN2`mb{07a#{l#yjRt%@w30YiK;;-AH`50RrsL~2>Ec3xU6QTyRF;{ax`RCpZ z@X;IY>dkgw#Gl**Mf6<})Klt|tSCk_EGEN$$;%Ism~E;bI}w(EoGqo@jR5WZaa_-Z zj|{7T*<$g91(HetV@(d^&mVZ7X?@%QzR(xu$efr1pdHAeuaN%>AFhHMIQJycDd_b^ z8Z~>@)c&u?^XRT4epT3qD5{_cBWd6uk*nV$7ZnH%+We({u)kVI8Peyhl7P`w-i4_R znbBD7n683RZtqxU1`K`oWg?1`{$X+*^Mn{tAr~Fw!yA`%i5?fXUlrDMp6x{zbx7|Y z@*$%gz8S_dg_EWHnzOO0PsliTP!7b|W?GX?+%(FO3(?N3LVc`Xvr^w}$e)|4CzaQu zx@dE3xfb`W-&IwPreBVi#!9-}s?dueJ^bwtBSgIJ-iME=-e_q4tae0L+U*=!1KT7~ zRg(xqf(!+|woi?k0EzPv-paG||Y@%ElNl6LsgNCLKi~g!L1)Qpv+Q&WZpL)|ueNh;VtFTGcr&F7O&9`wUlCb;)}3KCVf z1iPQx#%oXP5Xz)@sxRiKs&~@_$t*lVRAn*++M3 zyg7qHYuaj!#%v^1JuFwlH|-u~+ipl@96k>X=AzBjL$hT0=C9VBqgVIE$5OKz(kn++ANqT`I{ud0qi6~o! zx|xU4#@;txGl;jR(?Q;AkBBN`HW-OQNQNrp$Bb1NUV$@wFT1*ZNJk@&zJgiZWLy=$ z*drw+CLGE#C_a#!gyp+6kUdHJ@m7d<8GRGbZM#g@cw&)ZC!OwLahh?^A3Q4mgLj0x z{Mo|S(a%s!P2rzf5lXUSX!8$02m6mn>2h=UusU5Vs!qnSK&P`*W@v@Mhmk>T=ZzCNWvC3?9yJc2p>4F0~1r=_@ri$07NRmiIrD>!xycNot(E zG$Q1&Z+escieJ)^n0Qftvf&k&=~4COgXoss`g7a?3G5$OqpCOspSmY&>V(hzCI9-~ z3OO6m4k<9ppn*cb$mpIFf6x;slF(!XolOW zfrd_=7>|$twUYD*cBjNWH1+O?^25>moUadPTI>_jZ5B9;t-g(%kg1_)&)3(muMa-j zwB-QA7Y23Pbf#_1Y`wvP!X%DG7YxordF!U-S>LwP{=ouhPT7B^^AcbLF!HtHU**9% zCgUwEUZY!U7fakX!Pyk(E=$0kjD0315wgW9M?@S+#0~^mcZgGPn&Z+avjfKt(f2xX zWxqe60Murp6b;f%7Jt_uQ7^^Wh5RaIrx zQTLlp-iM*W8dC99D*pFG{LaxZhAAj@Nc25n%hrRgZzC1t0R>(v_Gu+M<3K4A zG;QNsf+bgAfR+8sZw0r$qVw!-C011A@uF^2s2TmeN0^a*5j){Gy=O51Hi~nwxQIi7=jMZ{R zFM~hDP3N???fg|h_g~(zzG?TyV`Vp%n5&3;aRv*yhK*2^CN7&(D!kZ+q!)4>9tIMC zW8qvLuW&o@`xS3_F)P?Gb~=#2YTK66EbJqR%OG<(Zuh=|v$Zf>)sX~x6w#u}Qmnbq zQ`&|bU)l?*Z`Zk^Uu!(Z}^LcCL#gTeXCc4V6tFVtPnncm5k1?Ar3qCL4j8`{6VtC4pVtn*;cR*y&7Hlm&1_#F#S`oIAu7S65fe%PnVg z?zQs8x86r9dfxsNO)jFPWOLFck)pCd;e{zMX9=PHumCDZ!+AE;i~$eS0(Nf$J*BW%FKGWT>3IA=hcFg0toDGUEWR z&DXGZ{A#2;@5QLP;+NOUKjluzfx&7U>Cr-5U9eAE;JSXp+SOw}tDcw!J^{!OTDqyIisr>okXwi3~$`R9V|Zv+f=F@|S21hU+Jj@Hlvh zj#UBpQ~N9}ZGB<<0b>qxu}DrxFir&7VjREn!ym0Ll6@{xw+LS-2l#=rhwGCkN4fcI zl^hM<9e$C@%E?q!?xz`5N9VzX!laGPMwzzJd5T4F1A+j>JJr_1vB$nK8x6+d?fzED zQMNH(t>n7)Kbrkw?;AJdyW%C9^ddylB5pG=0R(aiqNq^WZh^kH#E;XVBTOgH)LdRo zfV(}DpTgytdbh9ku-+0aK2x%mK6lPavRNK~&6plaOi|6n*s_avxGuvPaQSwv%e76; zi%o4!IlN5Jv$co@*K1CqqDPDGlS!$@*AdfN*?{8*T{@$6d9su&Pws& zx(a1;>;Z}lr}jnDk3m}J6;x>UqP?FjT3SE$@SEzi?J{*sN)q3jymYrm1b`)@-q6v` z!^%SGiJzw9;{8dAE_O?*++5Dr&hqEs0|6#d56>>(djcS>eJ~EuHqRpJ~JXT zE!4Y@Drk6=cP(p7gTFtG?LnBTR(;G`n-qKS%|nHTrAe&TEDrtsCAfDJja_n9D@vTp zUfNPlhi4DoFf`ZxzQJ@>K;9nv>Xyd=K+XH@Ut*w-GQ`l{4 zz%B65tg+*(SUm;hDQ3JBj#1>9#NJ{$VmAKmjtU?Dhh1{!j>Hz~iar5K+4y7R$`RI# zsC*m6;cvmyQAs4Cw!4>)`XyXLi}cBKM0wd1;SP#bfW z&=(Ts)y7Jmsv>@mOQD02pCTek(ll8y*$<~l&7c8Fj09NZuut9RN1Pt2((G4%{HyaI zou{~)(vDP{0j;eps=FV4cd z{YJH{=+Vq0qD6dN^P)KvwHxl?#>53Mbg{6o9OhXm_P&i2&G|uzN9PDL+^0AR=3h&O z)xVKC+&4e=~WhV({xv3M{)dmm1N=q%?)MxguV6EL>I zwld%miRWYGKl&K`PD>|)+JrdOgkIgu%q=Czpc^h%n`VC7zU(~^z!E>4pZGEO6?@DK zm5Cg9b(Nh{_;Gus?x7C0)F-_zD(6Iey_#|3I5k{2?|K%S-ET9srf3!}qt5+A7B)RMxALg=M zd4WaANArHSW1_5`55wT&kfXu-?X<}wf-OAd!h1fHIDE*$Z00mnMu8FopfhgNg;X|m z5oa*jYFSnG^m^y{RWp>zk zEp<~?OD1)0S5`c0fSxXzoFCI>`~_!Lzfk0ug&h0Su30hq<5TM|Sy_5d89H2XNLJko z3^N+fnasllc}fGH#=zn_IL0S&BzM=yh7F9~_q&KU8U_5i!0Of~P1l2p1QL>JsC;<# zjz&$VdLg@k5ZHb{XrRF33m=UFW8jck9~yx)R7oR*Fth@VRAOLYYBJc=W$deNbGTW> zlSEX{gs5-++ZA1jKS7$^b6Pn`hZ@__X%pW7WLSQp9ydRZ9Wy^rUM$P5HJSTesk6ja zL2adIOk@6)cFe>kn=w5Vp-5u4WUTEB@AYy#6Hc=R1Ab<@F}1d^0iIQRiMZGam1) zkXwWKyR1$32d2OA`Sv+iQijTN0<-KnQL%R8$+rMFgT2@zFuP~L3c=_L@>r}WRyvl9 zDr7F1>XDnbi~{EGg9SEt+5hsP37 z$%ACJEr-|!LMxyMDeh?A9Bq_G9_628RCG%|6Q0vAnqj!_W zL`Ea*-~SFt8HCN=7uuUOp*BR*VnN~7C{cD5DP~~;iq=THSU+4)mUz&Ik^{(v(pW2~ zq`iJQa)r7<`HK5~S9xXb;gl}?Q34k4HhjAVJW05l z1Y4`0k@?8#Sa_*d*k}db*xq=95Q( z>-YAM0wcLa;(m2=7vq(9miHZ$j){rMhBK4jB6LnG0g|LFQxcO~H|Y&-&dh<8t;M|z zkUwe>k%0kzReZz38}-R#omTnDfQ>%um?_s2tldF5>aOndS9c_oqiPAW$xzRugUUV7 znqFKgBfsLsW*6<*Q6IW1^^GIR*YPX@gIArB&nKd;#_5vlsd8<7Q+3BC#nJKZf`bA=6 zzMqjNSlZmw?9n1%GAB*;4Rt=X0t>$~#g3bjd-%T9f>1bh(cgRd}>uravw>xq*J_U3>p z5$7!PdT>|qCh7E6-_HOL{p_4`b>YAlMpz~|F}F&7nui|$2F?`cv^muv24vLQN0Brp zLRVu_p6$YEZ=AjI)@pWK9P5nFUlvy$JYl?Ws7en}pZLj0>sA5QTFqG>$wc~AS9S}Y zvsB4yKMNDsrP3((`-ozaq$fM`QfHTBgvi56qds553y6*f915VJZsV?6S8@Qd{3@uB z3pR;RQE`E*2^MeIM;!+*a;4AjB7Zwa-8!ijIjVTy+lyR4CLURj+Si!2ED~G=kzQiK zAvScAcV{4G?&(6TFP}>siP;)eG+3_fxg(cG4Evei@Bpo>%$%PB~aB zsHC>;$k(aAX*cj1q8Gt&YY~$lC_2%s=~C{&lZ{c$-;hxeA8xm4W4+a+#uM3GWB*6h zKZbMQKv)k$1GEQx82cBmU)?e17iPejxsFlV8F?ZWzZqJ(Tl{Pbf zAT68(@>+J5K1uH{{>kyC5Y>(JK|_q_mbxa)rGFE*3SVj*QIi`ZkIc`J5k)`%2233g zh2sJXVVB9lda=I57x-TvY?(r85%;D|7vOymNN$yrz)}Pv@eE;qZ2bAI&dd5-M66rQ z{B;KgaAC#&FNXh0F=_ob6chf2|4cCvcpOc*s*VjFK&_wDbf0F|%MX5CkZgh-QVTJ% zvr1L;8GPMjd?*IvyJ=~m8g=fbU#QK=ukv+1PgUja?&!%h2Cn92?KaHaoSi|(%ba2| z6|%KI?}Ko)$K0J}o!~2e=XF-5IvuR;{NRos$>o6(QD}Mih}7y%$_KEl0BUP5&>u<_ zeZV9UTxjugDIca{>Cf+hC6Bx_^rl|fL0N1z3?{4}opM+PSE-n%I zC5NtB3p;b2d~3(@bGy==H#It8Uvd52c&#riyZbv>u`y2UF~5@t&6?{Oc_yYTh%8QH zZw>X~4k#$Pba?*L zWEu%%E9mU2yZC;Z-I<(r5mZ&@T^6g`qZ1sa?yAW{%q=Sr5V>Bh=%hw|!FFY>ujs)0 z&~FX0LmCl$I}PEZ|HM|Q!uo&1RvEd-3rgUSv<-_jAF&i2G+6&lA|~$QlB`5aV z5HQzG7OZfaPUldxIR{&X9oJWZD{O_&OT70n$Esz`nWc|{KAs&|&`(|9UPpPYWCE&2 zho=m$1h9jTkL>YM9*l~t74##D^Y0kR!ZLQ{XxfWCgEN`A$#^S-OM_*BfvI+3Dms#J zgrwj-+Pt@A=B-n3f4siWE7>{6R&u%Q%BJF`V{PXvdZg0kLl34StORu%POKrN*i0;s zo>zIQdvxkv>MXYHTsXYP&OItWzEJg1jZW>jwx5)oQwimDq4QeXB+yGN9K|fF^z>NQ zj$#|03QRqrD!$-4`VO@<&+VUJV%jv{>3nXWCA}Jf$Gw#*Xk#)i$;|UbFU(Er*4*>_ zb^oHhSR24e568Q54Kz5_k#?X^x!WtyidZd^@Ba9AaSE0Jm$q2&pPtnwpxf~J_V`z8 z?f>7T#D5;-|Eai{HN30+u5%Ka7O&lcq76!R_-BCsER!C%56|0ubNBy$clyr<{_jj3 zRj;57OfzK`noBvpsd(L5CqdmNu+ zd9saNN-1SDBv}Wc!Wg@3$@-*0A@n3M#@03!4P_Whl8l)wvxqE3VJvB6F!qsTIY zjBU+^cQ?`xmeV%Mi{ufm=xlAZ2_Sd$b?!$&Z;JvP&4 z*!fHD>U-l7U02X!xAb=PnEMPdLY!0+L!PZ!l=crWxSisWL(qPr&=Jt3a$t)m%x$*I zE8}HH{n|ktb6ZNL87qwYy&o)B>nrFIyt~{F?{rmf(V>OGl0%yP25}1*>vQ&QLRKCN zxcN?ORlF{e!|+BR`!m%|VrBP3G9lFi1rDHr6qAsS@s1(=*(mp(gu#0UT|O;Hxdeu3 z$kY$Rx6ft(QjiMmMh}E!ABWUTC<5tvlzhH-3jFBBDwiX@RLPTLcnVfj%t;;Sn0$eZqq7drjKcdCQ0)-@FSzT;UAv!@Th*h+YUh%5Cm`5V@x|jR zv~$|wE>jodO1i9Zs-pee=sysT;xE!!0mJskRtduS8RaKtTBF|9h^O<+vINFS`B)OC?qPCCW?`D$5F>FuczO&1f*sCx(caSXLu@0j=7|jVOjR@>J*p5nMdzny1>%4AT z3yGne`Y~X9^K;j{f#Nm$oQ-o2bY0jb^9?Zz|PQQ>?nt3>U2} z`Q?^M5u$Szv10gNK>5o@pT_;K&l9zR?33P`!!9s~MM!ssmrq;|7~T}Jiw!1QLS+7% zH)}bxYqWr;lv6%_rIeJx=oJFs{=cHAnOw1 zuW6H-0D~@Me6f~BOj0Os3V-8bNL1FOZ6LTy5dFTq#NiF4SY1B8Y(SayEnG9n#Y-x4 z3tbC8?3F(6*nO$b=FYpW-IYuCM?cE<8=+x`_g>V@k(4C(JjGciihnK{ygIOf{Ge~Y zWZq=fr{O1rh2T*zbUbe0kQNEyjh_8QI?h=6>2VX&)s0cNaJlnaW zgmX-Ea~h>dp{0npR#t^G*z=NF*8CpW?DZbC^-yAHN15QVxJXafhf0i?*y0s`@`y_~ zGe0uu&<mLrRc4IMk-3lb%gBs(*ysoeUctleeTCntrUz>;z^~3OcR5d9ckLl z<8VUkOPdz`0M`V{5%wadBAj@lvw=nS1@0GM3VhnW=XcA96wewborK<+pp|P}u7D}f zOdI?mKOY3uuO23|_#QtT@GLDqR522qN&4#cH@S+l3ruwOgkIUWsWH*%K2z#!R0{m` z5ukM#DCkgAjT7vQi3{|1y}U5ZhcXe$)ol_SWXga*{IS*+r(LUL%s&t$&9Bw2ATVp} z3c2ssd|#sbq7qB$FxBi20@%9KjcFNKL9_c5*j`TpvAu1S?E6sC8lxZWT2}(71Ny;& zx5&9w(^He4C8@^&muc$qzhiKMU2D=S8l^+8U#}uXr-T!sbJz8$&XEjTgj7Z*fFczP zH~Fq0C3RnV>Tj}D_FfaY&}2@Ohi6ozKbjz6h0Et)@8*xoyuXE1wG`5SZB*JaPCgvq zv5`XOd_BE37d*+Y(9q|6C*^*+3T?zILh*%+!3m}ILY>()ut7?0x{m@O@9W8{w{0jd zNgBg{%Bu9b`|j^8!smj$Lx;Rr$;yQFliGADPWGD>qbR{{WNyR?WZ%qfJfL}I-UELK zwn(`s{lrE18?r(kn-BUIWV?C65#?ziOAK zG2Z$Yk3fjy`*(O|WP0o7+1Kki%`YUy8eD(a>OQ3;Wms0t@ICLhTLw9$T6qgn)~77sCT+ z-9{JnF&e5twNBAuIVQZRzqFj1p4-P8Sb!SDPNibbcK89Ei^|bktDMyKqs9M0P{$$# zBB!XRIaw_fJ*%zz#v|u~Fe`mwP>p9wZU)UkPfSrlfz%_470EP2OBYf{%Px$B&CKw` z6t5)d=zoI>r>@TA9%vh@dazqN*gHJvb;HBD=%L_S=Epc$9%zZ|n4xmKW;DVyz|3y- zOVAk0ugCSkO=EtLcQ|a;><~W{i&M4b2h`@`m;*#LyoIV2@^syIdh@%D{D#P_Xk(X#FQM z1~4{m=d|;kjAIEC5DYuwdW)tHK3Eb+f(}ty`AP#D2+|mX@Y*&T@4Bm8hnr4-cn198 zX#UaYSIp%9H7H`_tfnS@$BHSBUo8~XNdEGRmz#crssDM)jkjaG*T Date: Tue, 26 May 2026 11:16:56 -0500 Subject: [PATCH 108/146] Removed un-necessary create attributes --- inventory/inventory-fwupd/policy.cf | 4 ---- 1 file changed, 4 deletions(-) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index d77249e4..e27fa0a9 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -156,12 +156,10 @@ bundle agent inventory_fwupd_main files: linux.have_fwupdmgr._updates_stale:: "$(_updates_src)" - create => "true", content => "$(_updates_payload)"; linux.have_fwupdmgr._security_stale:: "$(_security_src)" - create => "true", content => "$(_security_payload)"; methods: @@ -276,7 +274,6 @@ bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, temp template_data => @(_merged); "$(_measurements_path)" - create => "true", content => "$(_measurements_body)"; } @@ -383,7 +380,6 @@ bundle agent inventory_fwupd_hsi(security_src) # Write HSI failing count to its own measurements file so # cf-monitord can track it as a time-series metric. "$(sys.statedir)/fwupd_measurements_hsi" - create => "true", content => "firmware_hsi_failing_attributes=$(fwupd_hsi_failing_count)", if => isvariable("fwupd_hsi_failing_count"); } From 95f7cb0344c56d37b3e04178a76a3d45229fd7a4 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 26 May 2026 11:31:19 -0500 Subject: [PATCH 109/146] Improved consistency of HSI ID vs IDentifier --- inventory/inventory-fwupd/policy.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index e27fa0a9..dbc851c2 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -1,7 +1,7 @@ # inventory-fwupd policy # # Inventories hardware firmware versions, pending firmware updates, and the -# Host Security ID (HSI) attribute scores reported by fwupd. +# Host Security Identifier (HSI) attribute scores reported by fwupd. # # Implementation notes: # - Device list is read directly from /var/cache/fwupd/devices.json, which From 0e922231641b7705c65f79b034aedfaaf46968cd Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 26 May 2026 11:33:05 -0500 Subject: [PATCH 110/146] Moved state files to common directory --- inventory/inventory-fwupd/policy.cf | 27 ++++++++++++++++----------- management/manage-fwupd/main.cf | 14 +++++++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index dbc851c2..7e1c90ab 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -33,10 +33,11 @@ bundle agent inventory_fwupd_main fileexists("/usr/local/bin/fwupdmgr"), "/usr/local/bin/fwupdmgr", "/usr/bin/fwupdmgr"); + "_fwupd_dir" string => "$(sys.statedir)/fwupd"; "_devices_src" string => "/var/cache/fwupd/devices.json"; - "_updates_src" string => "$(sys.statedir)/fwupd_inventory_updates.json"; - "_security_src" string => "$(sys.statedir)/fwupd_inventory_security.json"; - "_cache" string => "$(sys.statedir)/fwupd_inventory_cache"; + "_updates_src" string => "$(_fwupd_dir)/inventory_updates.json"; + "_security_src" string => "$(_fwupd_dir)/inventory_security.json"; + "_cache" string => "$(_fwupd_dir)/inventory_cache"; "_template" string => "$(this.promise_dirname)/fwupd-inventory.mustache"; # TTLs in seconds for the fwupdmgr-derived JSON caches. @@ -154,6 +155,10 @@ bundle agent inventory_fwupd_main '{"SecurityAttributes":[]}'); files: + linux:: + "$(_fwupd_dir)/." + create => "true"; + linux.have_fwupdmgr._updates_stale:: "$(_updates_src)" content => "$(_updates_payload)"; @@ -254,7 +259,7 @@ bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, temp # Measurements file: one key=value per line, read by # inventory_fwupd_monitor for time-series tracking. - "_measurements_path" string => "$(sys.statedir)/fwupd_inventory_measurements"; + "_measurements_path" string => "$(sys.statedir)/fwupd/inventory_measurements"; "_measurements_body" string => format( "firmware_devices_total=%d$(const.n)firmware_updates_available=%d", "$(_dev_count)", "$(_updates_count)"); @@ -379,7 +384,7 @@ bundle agent inventory_fwupd_hsi(security_src) files: # Write HSI failing count to its own measurements file so # cf-monitord can track it as a time-series metric. - "$(sys.statedir)/fwupd_measurements_hsi" + "$(sys.statedir)/fwupd/measurements_hsi" content => "firmware_hsi_failing_attributes=$(fwupd_hsi_failing_count)", if => isvariable("fwupd_hsi_failing_count"); } @@ -446,32 +451,32 @@ bundle monitor inventory_fwupd_monitor { measurements: linux:: - "$(sys.statedir)/fwupd_inventory_measurements" + "$(sys.statedir)/fwupd/inventory_measurements" handle => "firmware_devices_total", stream_type => "file", data_type => "int", history_type => "weekly", units => "devices", match_value => inventory_fwupd_kv("firmware_devices_total"), - if => fileexists("$(sys.statedir)/fwupd_inventory_measurements"); + if => fileexists("$(sys.statedir)/fwupd/inventory_measurements"); - "$(sys.statedir)/fwupd_inventory_measurements" + "$(sys.statedir)/fwupd/inventory_measurements" handle => "firmware_updates_available", stream_type => "file", data_type => "int", history_type => "weekly", units => "devices", match_value => inventory_fwupd_kv("firmware_updates_available"), - if => fileexists("$(sys.statedir)/fwupd_inventory_measurements"); + if => fileexists("$(sys.statedir)/fwupd/inventory_measurements"); - "$(sys.statedir)/fwupd_measurements_hsi" + "$(sys.statedir)/fwupd/measurements_hsi" handle => "firmware_hsi_failing", stream_type => "file", data_type => "int", history_type => "weekly", units => "checks", match_value => inventory_fwupd_kv("firmware_hsi_failing_attributes"), - if => fileexists("$(sys.statedir)/fwupd_measurements_hsi"); + if => fileexists("$(sys.statedir)/fwupd/measurements_hsi"); } body match_value inventory_fwupd_kv(key) diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index a164ba2d..c9d5e7ae 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -35,25 +35,25 @@ bundle agent main # Depends on inventory-fwupd, which provides: # - inventory_fwupd_main.fwupdmgr (path to fwupdmgr binary) # - default:have_fwupdmgr (class: fwupdmgr is present) -# - updates cache at $(sys.statedir)/fwupd_inventory_updates.json +# - updates cache at $(sys.statedir)/fwupd/inventory_updates.json # # Firmware updates are applied when the apply_updates class expression resolves # true AND a device name matches at least one pcre regular expression defined in # manage_fwupd:allowed.device_name_reglist. # -# A marker file ($(sys.statedir)/fwupd_update_applied) prevents the +# A marker file ($(sys.statedir)/fwupd/update_applied) prevents the # same update from re-executing on subsequent agent runs within the -# same boot cycle. The marker's mtime is compared against /proc/1 +# same boot cycle. The marker's mtime is compared against /proc/1 # (boot time) via isnewerthan(). Any marker remaining from a prior boot # is deleted so the update can re-evaluate after reboot. # # When reboot_after_update resolves true the marker is removed and -# a deferred reboot is scheduled. On systemd systems a transient +# a deferred reboot is scheduled. On systemd systems a transient # service polls for cf-agent to exit then reboots immediately; on # non-systemd systems the legacy shutdown -r +1 fallback is used. # # --no-reboot-check suppresses the interactive "reboot now?" prompt -# that fwupdmgr emits after certain updates. It does not skip or +# that fwupdmgr emits after certain updates. It does not skip or # defer the update itself. # # @inventory fwupd management status - INSTALLED, NOT_INSTALLED, or NOT_SUPPORTED @@ -61,7 +61,7 @@ bundle agent main vars: linux:: "_update_marker" - string => "$(sys.statedir)/fwupd_update_applied"; + string => "$(sys.statedir)/fwupd/update_applied"; "_systemd_run" string => ifelse(isexecutable("/bin/systemd-run"), "/bin/systemd-run", @@ -71,7 +71,7 @@ bundle agent main linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: "_updates_cache" - string => "$(sys.statedir)/fwupd_inventory_updates.json"; + string => "$(sys.statedir)/fwupd/inventory_updates.json"; "_updates" data => readjson("$(_updates_cache)"), From 50e597c4a03ecb404198a334c796038eec213acb Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 26 May 2026 11:33:35 -0500 Subject: [PATCH 111/146] Comment cleanup --- inventory/inventory-fwupd/policy.cf | 16 ++++++++-------- management/manage-fwupd/README.org | 29 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index 7e1c90ab..b824c349 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -182,7 +182,7 @@ bundle agent inventory_fwupd_main "hsi" usebundle => inventory_fwupd_hsi("$(_security_src)"), if => fileexists("$(_security_src)"); - # Per-device pending firmware update inventory, emitted natively + # Per-device pending firmware update inventory, emitted natively # from the updates JSON cache. Mirrors the HSI bundle idiom. "updates" usebundle => inventory_fwupd_updates("$(_updates_src)"), if => fileexists("$(_updates_src)"); @@ -297,8 +297,8 @@ bundle agent inventory_fwupd_hsi(security_src) # The rollup mirrors fwupd's own algorithm: walk levels 1-4 sequentially; # at each level, if any non-obsoleted attribute fails, stop — the result # is the highest level that had at least one success before the walk -# stopped. Empty levels (no attributes) are skipped without advancing -# or stopping. +# stopped. Empty levels (no attributes) are skipped without advancing +# the level counter. # # An attribute is failing when its HsiResult differs from HsiResultSuccess. { @@ -370,8 +370,8 @@ bundle agent inventory_fwupd_hsi(security_src) int => countclassesmatching("inventory_fwupd_hsi_failing_[0-9]+"); # Rolled-up HSI level: highest level with a pass where no level - # at or below it has a failure. Checks highest first so the - # first match wins. Mirrors fwupd's fu_security_attrs_calculate_hsi(). + # at or below it has a failure. Checks highest first so the + # first match wins. Mirrors fwupd's fu_security_attrs_calculate_hsi(). "fwupd_hsi_level" string => ifelse( and("_hsi_L4_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail"), not("_hsi_L3_fail"), not("_hsi_L4_fail")), "HSI:4", @@ -393,7 +393,7 @@ bundle agent inventory_fwupd_updates(updates_src) # @brief Surface per-device pending firmware update inventory. # # Reads the updates JSON cache and emits one inventory entry per device -# that has a pending update. Format: +# that has a pending update. Format: # "Device Name: current_version -> new_version" # # Unlike the HSI bundle, devices here are iterated from the Devices array @@ -436,8 +436,8 @@ bundle agent inventory_fwupd_devices(devices_src) "fwupd_dev[$(_did[$(_idxes)])]" string => "$(_name[$(_idxes)]) | $(_vend[$(_idxes)]) | v$(_ver[$(_idxes)]) | [$(_plug[$(_idxes)])]", - meta => { "inventory", "attribute_name=Firmware devices" }, - if => not(strcmp("$(_name[$(_idxes)])", "")); + meta => { "inventory", "attribute_name=Firmware devices" }, + if => not(strcmp("$(_name[$(_idxes)])", "")); } bundle monitor inventory_fwupd_monitor diff --git a/management/manage-fwupd/README.org b/management/manage-fwupd/README.org index 3bbd59ba..9d731270 100644 --- a/management/manage-fwupd/README.org +++ b/management/manage-fwupd/README.org @@ -63,8 +63,9 @@ devices are eligible for firmware updates. Even when =apply_updates= is enabled, only devices whose name matches an entry in this list will be updated. -Use =.*= to match all devices. Use specific patterns to target individual -hardware, e.g. =UEFI dbx=, =System Firmware=, =TPM=. +Patterns are unanchored (substring match), so =UEFI dbx= matches any +device whose name contains "UEFI dbx". Use =^…$= to anchor if you need +an exact match. Use =.*= to match all devices. ** Reboot after update @@ -77,6 +78,18 @@ On systemd systems this uses a transient service that polls for cf-agent's PID to exit, then reboots immediately. On non-systemd systems it falls back to ~shutdown -r +1~. +* Requirements + +Linux only. The =fwupd= package must be available in the system's +package repositories. + +* Warnings + +Firmware updates carry inherent risk (bricking, required reboots, AC +power requirements). Use the allow-list to limit updates to +well-understood device classes. Test in a non-production environment +first. + * Mission Portal The module reports =fwupd management status= as an inventory attribute @@ -97,15 +110,3 @@ visible in Mission Portal's inventory reports: The =--no-reboot-check= flag suppresses the interactive reboot prompt. It does not skip or defer the update itself. - -* Requirements - -Linux only. The =fwupd= package must be available in the system's -package repositories. - -* Warnings - -Firmware updates carry inherent risk (bricking, required reboots, AC -power requirements). Use the allow-list to limit updates to -well-understood device classes. Test in a non-production environment -first. From 9f8ed305b9f9e4b1fa2e521a8de61da8a73f3831 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 1 Jun 2026 13:35:33 +0200 Subject: [PATCH 112/146] cfengine format Signed-off-by: Ole Herman Schumacher Elgesem --- examples/gpg/password-store-sync.cf | 20 +- examples/rss/rss.cf | 40 +- examples/site-up/site_up.cf | 3 +- inventory/inventory-fde/inventory-fde.cf | 179 ++++--- inventory/inventory-fwupd/policy.cf | 497 ++++++++++-------- inventory/inventory-smartctl/cfbs.json | 4 +- inventory/inventory-smartctl/policy.cf | 150 ++++-- .../inventory-windows-services.cf | 7 +- management/autorun-bundles/def.json | 4 +- management/autorun-inputs/def.json | 4 +- management/autorun/def.json | 4 +- management/command-dispatcher/main.cf | 21 +- management/demo/def.json | 24 +- .../disable-automatic-key-trust/def.json | 4 +- management/disable-recommendations/def.json | 10 +- management/every-minute/def.json | 10 +- management/manage-fwupd/main.cf | 74 +-- ...ckage-method-ansible-galaxy-collections.cf | 21 +- .../package-method-winget.cf | 63 +-- .../package-method-winget/winget-installed.cf | 24 +- .../powershell-execution-policy.cf | 24 +- .../windows-capability/windows-capability.cf | 47 +- .../windows-optional-feature.cf | 56 +- promise-types/ansible/example.cf | 4 +- promise-types/appstreams/init.cf | 4 +- promise-types/git/example.cf | 4 +- promise-types/git/test.cf | 33 +- promise-types/groups/groups.cf | 48 +- promise-types/http/example.cf | 23 +- promise-types/iptables/test.cf | 37 +- promise-types/json/example.cf | 3 +- promise-types/json/test.cf | 174 +++--- promise-types/json/tests/to_modify.start.json | 2 +- .../json/tests/to_overwrite.start.json | 2 +- promise-types/symlinks/example.cf | 6 +- promise-types/symlinks/test.cf | 65 +-- promise-types/systemd/example.cf | 4 +- promise-types/systemd/test.cf | 31 +- pyrightconfig.json | 4 +- security/conditional-installer/main.cf | 25 +- security/cron-access/cron-access.cf | 2 + security/delete-home-dotrhosts/policy/main.cf | 29 +- security/delete-home-dotshosts/policy/main.cf | 29 +- security/dirtyfrag/dirtyfrag.cf | 141 +++-- security/enable-aslr/enable-aslr.cf | 19 +- security/etc-issue-access/etc-issue-access.cf | 2 +- security/etc-motd-access/etc-motd-access.cf | 2 +- security/install-aide/install-aide.cf | 18 +- security/inventory-selinux-modules/main.cf | 59 ++- .../inventory-unshadowed-users/policy/main.cf | 14 +- .../maintainers-in-motd.cf | 36 +- security/uninstall-apache/uninstall-apache.cf | 22 +- security/uninstall-bind/uninstall-bind.cf | 22 +- security/uninstall-dhcp/uninstall-dhcp.cf | 22 +- .../uninstall-dovecot/uninstall-dovecot.cf | 22 +- security/uninstall-packages/main.cf | 3 +- .../uninstall-rsh-server.cf | 22 +- security/uninstall-samba/uninstall-samba.cf | 22 +- security/uninstall-squid/uninstall-squid.cf | 22 +- security/uninstall-talk/uninstall-talk.cf | 22 +- security/uninstall-xinetd/uninstall-xinetd.cf | 22 +- software/windows/windows-openssh-server.cf | 8 +- 62 files changed, 1279 insertions(+), 1040 deletions(-) diff --git a/examples/gpg/password-store-sync.cf b/examples/gpg/password-store-sync.cf index 99132cab..c862c1d8 100644 --- a/examples/gpg/password-store-sync.cf +++ b/examples/gpg/password-store-sync.cf @@ -15,12 +15,19 @@ bundle agent main vars: # for gpg_keys promise, pass a keylist data similar to this proposed standard with the addition of the `ascii` property # https://datatracker.ietf.org/doc/draft-mccain-keylist/ (from https://github.com/firstlookmedia/gpgsync project) - # "keylist" data => readjson("/home/user/keylist.json"); - + # "keylist" data => readjson("/home/user/keylist.json"); # Or assembly the information using some policy data manipulation and jq (https://stedolan.github.io/jq/) magic - "fingerprint_files" slist => findfiles("/home/user/.password-store/.pub-keys/*.fp"); - "keys[$(fingerprint_files)][fingerprint]" string => readfile( $(fingerprint_files) ); - "keys[$(fingerprint_files)][ascii]" string => readfile( regex_replace( $(fingerprint_files), ".fp$", ".asc", "") ); + "fingerprint_files" + slist => findfiles("/home/user/.password-store/.pub-keys/*.fp"); + + "keys[$(fingerprint_files)][fingerprint]" + string => readfile($(fingerprint_files)); + + "keys[$(fingerprint_files)][ascii]" + string => readfile( + regex_replace($(fingerprint_files), ".fp$", ".asc", "") + ); + "keylist" data => mapdata("json_pipe", '$(def.jq) {"keys":[.[]]}', keys); git: @@ -28,6 +35,5 @@ bundle agent main repository => "https://github.com/user/my-passwords"; gpg_keys: - "/home/user/.gnupg" - keylist => storejson( @(keylist[0]) ); + "/home/user/.gnupg" keylist => storejson(@(keylist[0])); } diff --git a/examples/rss/rss.cf b/examples/rss/rss.cf index 6b70de55..1cf6f4c6 100644 --- a/examples/rss/rss.cf +++ b/examples/rss/rss.cf @@ -1,4 +1,3 @@ - promise agent rss { path => "$(example_rss_promise_common.path)"; @@ -15,29 +14,36 @@ bundle agent example_rss_promise # @brief Promise a files content to be the =description= of an item selected from a channel in an RSS feed { rss: - "/tmp/ISS-oldest.txt" # Contains Item description - # REPAIRED if content on disk changes - # KEPT if content on disk does not need changed - # NOTKEPT if unable to fetch rss, unable to re-write content + "/tmp/ISS-oldest.txt" + # Contains Item description + # REPAIRED if content on disk changes + # KEPT if content on disk does not need changed + # NOTKEPT if unable to fetch rss, unable to re-write content feed => "https://blogs.nasa.gov/stationreport/feed/", - select => "oldest"; # Default to newest + select => "oldest"; - "/tmp/ISS-random.txt" # Contains Item description - # REPAIRED if content on disk changes - # KEPT if content on disk does not need changed - # NOTKEPT if unable to fetch rss, unable to re-write content + # Default to newest + "/tmp/ISS-random.txt" + # Contains Item description + # REPAIRED if content on disk changes + # KEPT if content on disk does not need changed + # NOTKEPT if unable to fetch rss, unable to re-write content feed => "https://blogs.nasa.gov/stationreport/feed/", - select => "random"; # Default to newest + select => "random"; - "/tmp/ISS-newest-from-local-file-source.txt" # Contains Item description - # REPAIRED if content on disk changes - # KEPT if content on disk does not need changed - # NOTKEPT if unable to fetch rss, unable to re-write content + # Default to newest + "/tmp/ISS-newest-from-local-file-source.txt" + # Contains Item description + # REPAIRED if content on disk changes + # KEPT if content on disk does not need changed + # NOTKEPT if unable to fetch rss, unable to re-write content feed => "/tmp/iss.xml", - select => "newest"; # Default to newest + select => "newest"; + # Default to newest } bundle agent __main__ { - methods: "example_rss_promise"; + methods: + "example_rss_promise"; } diff --git a/examples/site-up/site_up.cf b/examples/site-up/site_up.cf index 6de97f10..8f50db3b 100644 --- a/examples/site-up/site_up.cf +++ b/examples/site-up/site_up.cf @@ -14,6 +14,5 @@ bundle agent site_up_autorun "http://172.28.128.10"; "https://cfengine.com/"; "https://cfengine2.com/"; - "https://unavailable.com" - skip_ssl_verification => "true"; + "https://unavailable.com" skip_ssl_verification => "true"; } diff --git a/inventory/inventory-fde/inventory-fde.cf b/inventory/inventory-fde/inventory-fde.cf index 323e2482..063af89d 100644 --- a/inventory/inventory-fde/inventory-fde.cf +++ b/inventory/inventory-fde/inventory-fde.cf @@ -1,22 +1,20 @@ body file control { - namespace => "inventory_fde"; + namespace => "inventory_fde"; } - # Duplicated from the CFEngine standard library so this module can be parsed # and tested standalone without loading the full masterfiles. # _tidy: lib/files.cf body delete tidy # _in_shell: lib/commands.cf body contain in_shell - body delete _tidy { - dirlinks => "delete"; - rmdirs => "true"; + dirlinks => "delete"; + rmdirs => "true"; } body contain _in_shell { - useshell => "useshell"; + useshell => "useshell"; } bundle agent main @@ -30,15 +28,13 @@ bundle agent main { vars: linux:: - "_dmsetup" string => "/sbin/dmsetup"; + "_dmsetup" string => "/sbin/dmsetup"; "_cryptsetup" string => "/sbin/cryptsetup"; classes: linux:: - "_have_dmsetup" - expression => isexecutable("${_dmsetup}"); - "_have_cryptsetup" - expression => isexecutable("${_cryptsetup}"); + "_have_dmsetup" expression => isexecutable("${_dmsetup}"); + "_have_cryptsetup" expression => isexecutable("${_cryptsetup}"); # Flag each dm device that has a CRYPT uuid "_dm_is_crypt_${_dm_devices}" @@ -47,26 +43,35 @@ bundle agent main # Classify crypt type per device "_dm_is_luks2_${_dm_devices}" expression => strcmp("LUKS2", "${_dm_crypt_type[${_dm_devices}]}"); + "_dm_is_luks1_${_dm_devices}" expression => strcmp("LUKS1", "${_dm_crypt_type[${_dm_devices}]}"); # Classify each mount: real block device? (starts with /dev/, not a loop device) "_is_real_block_${_mnt_idx}" - expression => regcmp("/dev/(?!loop)\S+", "${_mnt_data[${_mnt_idx}][0]}"); + expression => regcmp( + "/dev/(?!loop)\S+", "${_mnt_data[${_mnt_idx}][0]}" + ); # Classify each real block mount: is device in the crypt paths list? "_is_encrypted_${_mnt_idx}" - expression => regcmp("(${_crypt_paths_regex})", "${_mnt_data[${_mnt_idx}][0]}"), + expression => regcmp( + "(${_crypt_paths_regex})", "${_mnt_data[${_mnt_idx}][0]}" + ), if => canonify("_is_real_block_${_mnt_idx}"); # LUKS1: flag enabled keyslots (slots 0-7, all share global cipher, all use PBKDF2) "_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}" - expression => regcmp("(?s).*Key Slot ${_luks1_slots}: ENABLED.*", "${_luks1_dump[${_dm_devices}]}"), + expression => regcmp( + "(?s).*Key Slot ${_luks1_slots}: ENABLED.*", + "${_luks1_dump[${_dm_devices}]}" + ), if => canonify("_dm_is_luks1_${_dm_devices}"); # Summary classes "_has_encrypted" expression => isgreaterthan(length(_encrypted_mountpoints), 0); + "_has_unencrypted" expression => isgreaterthan(length(_unencrypted_mountpoints), 0); @@ -79,6 +84,7 @@ bundle agent main "_dm_uuid[${_dm_devices}]" string => readfile("/sys/block/${_dm_devices}/dm/uuid"), if => fileexists("/sys/block/${_dm_devices}/dm/uuid"); + "_dm_name[${_dm_devices}]" string => readfile("/sys/block/${_dm_devices}/dm/name"), if => fileexists("/sys/block/${_dm_devices}/dm/name"); @@ -87,26 +93,36 @@ bundle agent main "_crypt_mapper_path[${_dm_devices}]" string => "/dev/mapper/${_dm_name[${_dm_devices}]}", if => canonify("_dm_is_crypt_${_dm_devices}"); + "_crypt_dm_path[${_dm_devices}]" string => "/dev/${_dm_devices}", if => canonify("_dm_is_crypt_${_dm_devices}"); + "_all_crypt_paths" slist => { getvalues(_crypt_mapper_path), getvalues(_crypt_dm_path) }; # Build a regex alternation of all crypt device paths for matching "_crypt_paths_regex" - string => join("|", maplist(regex_replace("$(this)", "([./])", "\\\1", "g"), _all_crypt_paths)); + string => join( + "|", + maplist( + regex_replace("$(this)", "([./])", "\\\1", "g"), _all_crypt_paths + ) + ); # Extract the encryption type for crypt devices # UUID format: CRYPT--- "_dm_crypt_type[${_dm_devices}]" - string => regex_replace("${_dm_uuid[${_dm_devices}]}", "^CRYPT-([^-]+)-.*", "\1", ""), + string => regex_replace( + "${_dm_uuid[${_dm_devices}]}", "^CRYPT-([^-]+)-.*", "\1", "" + ), if => canonify("_dm_is_crypt_${_dm_devices}"); # Underlying block device for each crypt device (for cryptsetup luksDump) "_dm_slaves[${_dm_devices}]" slist => lsdir("/sys/block/${_dm_devices}/slaves", "[a-z].*", false), if => canonify("_dm_is_crypt_${_dm_devices}"); + "_dm_slave_dev[${_dm_devices}]" string => "/dev/${_dm_slaves[${_dm_devices}]}", if => canonify("_dm_is_crypt_${_dm_devices}"); @@ -114,7 +130,10 @@ bundle agent main # Parse /proc/mounts into indexed array # Columns: 0=device, 1=mountpoint, 2=fstype, 3=options, 4=dump, 5=pass "_n_mnt_lines" - int => readstringarrayidx("_mnt_data", "/proc/mounts", "\s*#[^\n]*", "\s+", inf, inf); + int => readstringarrayidx( + "_mnt_data", "/proc/mounts", "\s*#[^\n]*", "\s+", inf, inf + ); + "_mnt_idx" slist => getindices(_mnt_data); # Collect all real block device mountpoints @@ -135,12 +154,16 @@ bundle agent main string => "${_mnt_data[${_mnt_idx}][1]}", if => and( canonify("_dm_is_crypt_${_dm_devices}"), - regcmp("(/dev/mapper/${_dm_name[${_dm_devices}]}|/dev/${_dm_devices})", - "${_mnt_data[${_mnt_idx}][0]}")); + regcmp( + "(/dev/mapper/${_dm_name[${_dm_devices}]}|/dev/${_dm_devices})", + "${_mnt_data[${_mnt_idx}][0]}" + ) + ); # Derive unencrypted mountpoints as the difference "_all_real_mountpoints" slist => getvalues(_all_real_mountpoint); "_encrypted_mountpoints" slist => getvalues(_encrypted_mountpoint); + "_unencrypted_mountpoints" slist => difference(_all_real_mountpoints, _encrypted_mountpoints); @@ -149,8 +172,13 @@ bundle agent main # dmsetup table format: "0 crypt " "_dm_active_cipher[${_dm_devices}]" string => regex_replace( - execresult("${_dmsetup} table ${_dm_name[${_dm_devices}]}", "noshell"), - "^\d+\s+\d+\s+crypt\s+(\S+)\s+.*$", "\1", ""), + execresult( + "${_dmsetup} table ${_dm_name[${_dm_devices}]}", "noshell" + ), + "^\d+\s+\d+\s+crypt\s+(\S+)\s+.*$", + "\1", + "" + ), if => canonify("_dm_is_crypt_${_dm_devices}"); # --- LUKS2 keyslot info via cached JSON metadata --- @@ -163,22 +191,37 @@ bundle agent main string => filestat("${_luks2_cache[${_dm_devices}]}", "mtime"), if => and( canonify("_dm_is_luks2_${_dm_devices}"), - fileexists("${_luks2_cache[${_dm_devices}]}")); + fileexists("${_luks2_cache[${_dm_devices}]}") + ); # --- LUKS1 keyslot info via text parsing --- _have_cryptsetup:: "_luks1_slots" slist => { "0", "1", "2", "3", "4", "5", "6", "7" }; "_luks1_dump[${_dm_devices}]" - string => execresult("${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", "noshell"), + string => execresult( + "${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", + "noshell" + ), if => canonify("_dm_is_luks1_${_dm_devices}"); # LUKS1 global cipher: "Cipher name" + "Cipher mode" "_luks1_cipher_name[${_dm_devices}]" - string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher name:\s+(\S+).*", "\1", ""), + string => regex_replace( + "${_luks1_dump[${_dm_devices}]}", + "(?s).*Cipher name:\s+(\S+).*", + "\1", + "" + ), if => canonify("_dm_is_luks1_${_dm_devices}"); + "_luks1_cipher_mode[${_dm_devices}]" - string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher mode:\s+(\S+).*", "\1", ""), + string => regex_replace( + "${_luks1_dump[${_dm_devices}]}", + "(?s).*Cipher mode:\s+(\S+).*", + "\1", + "" + ), if => canonify("_dm_is_luks1_${_dm_devices}"); # Build per-keyslot summary for each ENABLED slot @@ -186,7 +229,8 @@ bundle agent main string => "${_luks1_slots}:${_luks1_cipher_name[${_dm_devices}]}-${_luks1_cipher_mode[${_dm_devices}]}/pbkdf2", if => and( canonify("_dm_is_luks1_${_dm_devices}"), - canonify("_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}")); + canonify("_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}") + ); "_luks1_ks_entries[${_dm_devices}]" slist => getvalues("_luks1_ks_entry[${_dm_devices}]"), @@ -197,12 +241,15 @@ bundle agent main if => canonify("_dm_is_luks1_${_dm_devices}"); # --- Inventory attributes --- - linux:: "fde_enabled" - string => ifelse("_has_encrypted.!_has_unencrypted", "yes", - "_has_encrypted._has_unencrypted", "partial", - "no"), + string => ifelse( + "_has_encrypted.!_has_unencrypted", + "yes", + "_has_encrypted._has_unencrypted", + "partial", + "no" + ), meta => { "inventory", "attribute_name=Full disk encryption enabled" }; "fde_method" @@ -225,7 +272,8 @@ bundle agent main string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_active_cipher[${_dm_devices}]}", if => and( canonify("_dm_is_crypt_${_dm_devices}"), - isvariable("_dm_mountpoint[${_dm_devices}]")); + isvariable("_dm_mountpoint[${_dm_devices}]") + ); _have_cryptsetup:: "_keyslot_info_entry[${_dm_devices}]" @@ -233,23 +281,29 @@ bundle agent main if => and( canonify("_dm_is_luks2_${_dm_devices}"), isvariable("_dm_mountpoint[${_dm_devices}]"), - isvariable("_luks2_ks_${_dm_devices}[keyslots]")); + isvariable("_luks2_ks_${_dm_devices}[keyslots]") + ); "_keyslot_info_entry[${_dm_devices}]" string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_keyslot_info[${_dm_devices}]}", if => and( canonify("_dm_is_luks1_${_dm_devices}"), - isvariable("_dm_mountpoint[${_dm_devices}]")); + isvariable("_dm_mountpoint[${_dm_devices}]") + ); _has_encrypted._have_dmsetup:: "fde_volume_cipher" slist => getvalues(_volume_cipher_entry), - meta => { "inventory", "attribute_name=Full disk encryption volume ciphers" }; + meta => { + "inventory", "attribute_name=Full disk encryption volume ciphers" + }; _has_encrypted._have_cryptsetup:: "fde_keyslot_info" slist => getvalues(_keyslot_info_entry), - meta => { "inventory", "attribute_name=Full disk encryption keyslot info" }; + meta => { + "inventory", "attribute_name=Full disk encryption keyslot info" + }; files: _have_cryptsetup:: @@ -260,20 +314,29 @@ bundle agent main canonify("_dm_is_luks2_${_dm_devices}"), fileexists("${_luks2_cache[${_dm_devices}]}"), isgreaterthan( - format("%d", eval("$(sys.systime) - ${_luks2_cache_mtime[${_dm_devices}]}")), - "86400")); + format( + "%d", + eval("$(sys.systime) - ${_luks2_cache_mtime[${_dm_devices}]}") + ), + "86400" + ) + ); commands: _have_cryptsetup:: "${_cryptsetup}" - arglist => { "luksDump", - "--dump-json-metadata", - "${_dm_slave_dev[${_dm_devices}]}", - ">", "${_luks2_cache[${_dm_devices}]}" }, + arglist => { + "luksDump", + "--dump-json-metadata", + "${_dm_slave_dev[${_dm_devices}]}", + ">", + "${_luks2_cache[${_dm_devices}]}", + }, contain => _in_shell, if => and( canonify("_dm_is_luks2_${_dm_devices}"), - not(fileexists("${_luks2_cache[${_dm_devices}]}"))); + not(fileexists("${_luks2_cache[${_dm_devices}]}")) + ); methods: _have_cryptsetup:: @@ -283,41 +346,35 @@ bundle agent main useresult => "_luks2_ks_${_dm_devices}", if => and( canonify("_dm_is_luks2_${_dm_devices}"), - fileexists("${_luks2_cache[${_dm_devices}]}")); + fileexists("${_luks2_cache[${_dm_devices}]}") + ); reports: - !linux.verbose_mode:: - "$(this.promise_filename): $(this.namespace):$(this.bundle) is currently only instrumented for Linux. Please consider making a pull request or filing a ticket to request your specific platform."; + !linux.verbose_mode:: + "$(this.promise_filename): $(this.namespace):$(this.bundle) is currently only instrumented for Linux. Please consider making a pull request or filing a ticket to request your specific platform."; } bundle agent luks2_keyslot_info(cache_file) # @brief Parse LUKS2 JSON metadata and return keyslot summary { vars: - "_json" - data => readjson("${cache_file}"); - - "_ks_idx" - slist => getindices("_json[keyslots]"); - - # Build per-keyslot summary: ":/" - "_ks_entry[${_ks_idx}]" - string => "${_ks_idx}:${_json[keyslots][${_ks_idx}][area][encryption]}/${_json[keyslots][${_ks_idx}][kdf][type]}"; + "_json" data => readjson("${cache_file}"); + "_ks_idx" slist => getindices("_json[keyslots]"); - "_ks_entries" - slist => getvalues(_ks_entry); + # Build per-keyslot summary: ":/" + "_ks_entry[${_ks_idx}]" + string => "${_ks_idx}:${_json[keyslots][${_ks_idx}][area][encryption]}/${_json[keyslots][${_ks_idx}][kdf][type]}"; - "_keyslots" - string => join(", ", sort(_ks_entries, "lex")); + "_ks_entries" slist => getvalues(_ks_entry); + "_keyslots" string => join(", ", sort(_ks_entries, "lex")); reports: - "${_keyslots}" - bundle_return_value_index => "keyslots"; + "${_keyslots}" bundle_return_value_index => "keyslots"; } body file control { - namespace => "default"; + namespace => "default"; } bundle agent __main__ diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index b824c349..7b68cf6f 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -21,7 +21,6 @@ # UPDATES_AVAILABLE - one or more devices have firmware updates pending # NO_DEVICES - fwupd present but reports no updatable devices # FWUPD_MISSING - fwupd not installed on the host - bundle agent inventory_fwupd_main { vars: @@ -29,41 +28,63 @@ bundle agent inventory_fwupd_main # Public API: referenced by manage-fwupd and other consumers. "fwupdmgr" string => ifelse( - fileexists("/usr/bin/fwupdmgr"), "/usr/bin/fwupdmgr", - fileexists("/usr/local/bin/fwupdmgr"), "/usr/local/bin/fwupdmgr", - "/usr/bin/fwupdmgr"); - - "_fwupd_dir" string => "$(sys.statedir)/fwupd"; + fileexists("/usr/bin/fwupdmgr"), + "/usr/bin/fwupdmgr", + fileexists("/usr/local/bin/fwupdmgr"), + "/usr/local/bin/fwupdmgr", + "/usr/bin/fwupdmgr" + ); + + "_fwupd_dir" string => "$(sys.statedir)/fwupd"; "_devices_src" string => "/var/cache/fwupd/devices.json"; "_updates_src" string => "$(_fwupd_dir)/inventory_updates.json"; "_security_src" string => "$(_fwupd_dir)/inventory_security.json"; - "_cache" string => "$(_fwupd_dir)/inventory_cache"; - "_template" string => "$(this.promise_dirname)/fwupd-inventory.mustache"; + "_cache" string => "$(_fwupd_dir)/inventory_cache"; + "_template" string => "$(this.promise_dirname)/fwupd-inventory.mustache"; # TTLs in seconds for the fwupdmgr-derived JSON caches. - "_updates_ttl" string => "43200"; # 12 h - "_security_ttl" string => "86400"; # 24 h + "_updates_ttl" string => "43200"; + + # 12 h + "_security_ttl" string => "86400"; + # 24 h # Pre-compute file mtimes here rather than calling filestat() inline # inside class expressions: nested $(filestat($(path), mtime)) does not # expand reliably inside isgreaterthan(), so the staleness comparisons # silently never match. Defaulting to "0" when a file is missing lets # the class expressions below stay simple and total. "_cache_mtime" - string => ifelse(fileexists("$(_cache)"), - filestat("$(_cache)", "mtime"), "0"); + string => ifelse( + fileexists("$(_cache)"), filestat("$(_cache)", "mtime"), "0" + ); + "_devices_mtime" - string => ifelse(fileexists("$(_devices_src)"), - filestat("$(_devices_src)", "mtime"), "0"); + string => ifelse( + fileexists("$(_devices_src)"), + filestat("$(_devices_src)", "mtime"), + "0" + ); + "_updates_mtime" - string => ifelse(fileexists("$(_updates_src)"), - filestat("$(_updates_src)", "mtime"), "0"); + string => ifelse( + fileexists("$(_updates_src)"), + filestat("$(_updates_src)", "mtime"), + "0" + ); + "_security_mtime" - string => ifelse(fileexists("$(_security_src)"), - filestat("$(_security_src)", "mtime"), "0"); + string => ifelse( + fileexists("$(_security_src)"), + filestat("$(_security_src)", "mtime"), + "0" + ); - "_updates_age" string => eval("$(sys.systime) - $(_updates_mtime)", "math", "infix"); - "_security_age" string => eval("$(sys.systime) - $(_security_mtime)", "math", "infix"); + "_updates_age" + string => eval("$(sys.systime) - $(_updates_mtime)", "math", "infix"); + + "_security_age" + string => eval("$(sys.systime) - $(_security_mtime)", "math", "infix"); # /proc/1 mtime is when PID 1 (init) was exec'd, i.e., boot time on # Linux. Used below to force a refresh of the updates cache after a @@ -71,15 +92,17 @@ bundle agent inventory_fwupd_main # snapshot that still lists devices whose updates have already been # activated. "_boot_mtime" - string => ifelse(fileexists("/proc/1"), - filestat("/proc/1", "mtime"), "0"); + string => ifelse( + fileexists("/proc/1"), filestat("/proc/1", "mtime"), "0" + ); classes: linux:: "have_fwupdmgr" scope => "namespace", expression => isexecutable("$(fwupdmgr)"); - "_have_devices" expression => fileexists("$(_devices_src)"); + + "_have_devices" expression => fileexists("$(_devices_src)"); # CPU vendor classes for platform-specific compliance conditions. # /proc/cpuinfo vendor_id: "GenuineIntel" or "AuthenticAMD". @@ -87,6 +110,7 @@ bundle agent inventory_fwupd_main scope => "namespace", meta => { "report" }, expression => regline("vendor_id.*GenuineIntel", "/proc/cpuinfo"); + "fwupd_cpu_vendor_amd" scope => "namespace", meta => { "report" }, @@ -98,15 +122,17 @@ bundle agent inventory_fwupd_main "fwupd_oem_vendor_hp" scope => "namespace", meta => { "report" }, - expression => regline("(HP Inc\.|Hewlett-Packard)", - "/sys/class/dmi/id/sys_vendor"); + expression => regline( + "(HP Inc\.|Hewlett-Packard)", "/sys/class/dmi/id/sys_vendor" + ); linux.have_fwupdmgr:: - "_updates_stale" - not => fileexists("$(_updates_src)"); + "_updates_stale" not => fileexists("$(_updates_src)"); + "_updates_stale" expression => isgreaterthan("$(_updates_age)", "$(_updates_ttl)"), if => fileexists("$(_updates_src)"); + # Force refresh after a reboot: a pre-boot cache may still list # devices whose updates have been activated by the boot, and # consumers (manage-fwupd) would otherwise repeatedly re-apply @@ -115,8 +141,8 @@ bundle agent inventory_fwupd_main expression => islessthan("$(_updates_mtime)", "$(_boot_mtime)"), if => fileexists("$(_updates_src)"); - "_security_stale" - not => fileexists("$(_security_src)"); + "_security_stale" not => fileexists("$(_security_src)"); + "_security_stale" expression => isgreaterthan("$(_security_age)", "$(_security_ttl)"), if => fileexists("$(_security_src)"); @@ -125,53 +151,69 @@ bundle agent inventory_fwupd_main # Module-protocol cache must be rebuilt when any of the source JSON # files is newer than the cache itself, or when the cache is missing. "_cache_missing" not => fileexists("$(_cache)"); + "_cache_stale_devices" expression => isgreaterthan("$(_devices_mtime)", "$(_cache_mtime)"); + "_cache_stale_updates" expression => isgreaterthan("$(_updates_mtime)", "$(_cache_mtime)"), if => "have_fwupdmgr"; + "_cache_stale_security" expression => isgreaterthan("$(_security_mtime)", "$(_cache_mtime)"), if => "have_fwupdmgr"; + "_rebuild_cache" - or => { "_cache_missing", "_cache_stale_devices", - "_cache_stale_updates", "_cache_stale_security" }; + or => { + "_cache_missing", + "_cache_stale_devices", + "_cache_stale_updates", + "_cache_stale_security", + }; vars: linux.have_fwupdmgr._updates_stale:: "_updates_raw" string => execresult("$(fwupdmgr) get-updates --json", "noshell"); + "_updates_payload" - string => ifelse(regcmp("\s*\{.*", "$(_updates_raw)"), - "$(_updates_raw)", - '{"Devices":[]}'); + string => ifelse( + regcmp("\s*\{.*", "$(_updates_raw)"), + "$(_updates_raw)", + '{"Devices":[]}' + ); linux.have_fwupdmgr._security_stale:: "_security_raw" string => execresult("$(fwupdmgr) security --json", "noshell"); + "_security_payload" - string => ifelse(regcmp("\s*\{.*", "$(_security_raw)"), - "$(_security_raw)", - '{"SecurityAttributes":[]}'); + string => ifelse( + regcmp("\s*\{.*", "$(_security_raw)"), + "$(_security_raw)", + '{"SecurityAttributes":[]}' + ); files: linux:: - "$(_fwupd_dir)/." - create => "true"; + "$(_fwupd_dir)/." create => "true"; linux.have_fwupdmgr._updates_stale:: - "$(_updates_src)" - content => "$(_updates_payload)"; + "$(_updates_src)" content => "$(_updates_payload)"; linux.have_fwupdmgr._security_stale:: - "$(_security_src)" - content => "$(_security_payload)"; + "$(_security_src)" content => "$(_security_payload)"; methods: linux._have_devices._rebuild_cache:: - "render" usebundle => inventory_fwupd_render( - "$(_devices_src)", "$(_updates_src)", "$(_security_src)", - "$(_template)", "$(_cache)"); + "render" + usebundle => inventory_fwupd_render( + "$(_devices_src)", + "$(_updates_src)", + "$(_security_src)", + "$(_template)", + "$(_cache)" + ); linux.have_fwupdmgr:: # HSI rollup is small (typically <40 attributes) and computed inline. @@ -179,17 +221,20 @@ bundle agent inventory_fwupd_main # cache because filtering objects out of a data container and reshaping # them into a mustache-iterable list is more code than just iterating # and tagging the few results with inventory meta. - "hsi" usebundle => inventory_fwupd_hsi("$(_security_src)"), + "hsi" + usebundle => inventory_fwupd_hsi("$(_security_src)"), if => fileexists("$(_security_src)"); # Per-device pending firmware update inventory, emitted natively # from the updates JSON cache. Mirrors the HSI bundle idiom. - "updates" usebundle => inventory_fwupd_updates("$(_updates_src)"), + "updates" + usebundle => inventory_fwupd_updates("$(_updates_src)"), if => fileexists("$(_updates_src)"); # Per-device firmware inventory, emitted natively from the devices # JSON cache. Mirrors the HSI bundle idiom. - "devices" usebundle => inventory_fwupd_devices("$(_devices_src)"), + "devices" + usebundle => inventory_fwupd_devices("$(_devices_src)"), if => fileexists("$(_devices_src)"); classes: @@ -221,65 +266,70 @@ bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, temp # caller then loads every inventory variable in one shot. { vars: - "_template_body" string => readfile("$(template)", "inf"); - - "_devices_json" data => readjson("$(devices_src)"); - "_dev_count" int => length("_devices_json[Devices]"); - - "_updates_json" - data => readjson("$(updates_src)"), - if => fileexists("$(updates_src)"); - "_updates_count" - int => length("_updates_json[Devices]"), - if => isvariable("_updates_json[Devices]"); - - "_security_json" - data => readjson("$(security_src)"), - if => fileexists("$(security_src)"); - - # Default counters when fwupdmgr-derived data is not present. - "_updates_count" - int => "0", - if => not(isvariable("_updates_count")); - - # Status rollup - "_status" - string => ifelse( - isgreaterthan("$(_updates_count)", "0"), "UPDATES_AVAILABLE", - isgreaterthan("$(_dev_count)", "0"), "OK", - "NO_DEVICES"); - - # HSI inventory is emitted directly by inventory_fwupd_hsi rather than - # threaded through this cache, since reshaping filtered objects into a - # mustache-iterable list is more code than just tagging them in policy. - "_extra_json" string => format( - '{ "status": "%s" }', - "$(_status)"); - "_extra" data => parsejson("$(_extra_json)"); - - # Measurements file: one key=value per line, read by - # inventory_fwupd_monitor for time-series tracking. - "_measurements_path" string => "$(sys.statedir)/fwupd/inventory_measurements"; - "_measurements_body" string => format( + "_template_body" string => readfile("$(template)", "inf"); + "_devices_json" data => readjson("$(devices_src)"); + "_dev_count" int => length("_devices_json[Devices]"); + + "_updates_json" + data => readjson("$(updates_src)"), + if => fileexists("$(updates_src)"); + + "_updates_count" + int => length("_updates_json[Devices]"), + if => isvariable("_updates_json[Devices]"); + + "_security_json" + data => readjson("$(security_src)"), + if => fileexists("$(security_src)"); + + # Default counters when fwupdmgr-derived data is not present. + "_updates_count" + int => "0", + if => not(isvariable("_updates_count")); + + # Status rollup + "_status" + string => ifelse( + isgreaterthan("$(_updates_count)", "0"), + "UPDATES_AVAILABLE", + isgreaterthan("$(_dev_count)", "0"), + "OK", + "NO_DEVICES" + ); + + # HSI inventory is emitted directly by inventory_fwupd_hsi rather than + # threaded through this cache, since reshaping filtered objects into a + # mustache-iterable list is more code than just tagging them in policy. + "_extra_json" string => format('{ "status": "%s" }', "$(_status)"); + "_extra" data => parsejson("$(_extra_json)"); + + # Measurements file: one key=value per line, read by + # inventory_fwupd_monitor for time-series tracking. + "_measurements_path" + string => "$(sys.statedir)/fwupd/inventory_measurements"; + + "_measurements_body" + string => format( "firmware_devices_total=%d$(const.n)firmware_updates_available=%d", - "$(_dev_count)", "$(_updates_count)"); + "$(_dev_count)", + "$(_updates_count)" + ); - # Mergedata combines device data, status counters, and optional - # extra top-level keys into a single data structure for mustache rendering. - # Note: pending-update inventory is emitted natively via - # inventory_fwupd_updates() rather than threaded through this cache, - # because Releases[0].Version cannot be accessed from mustache templates. - "_merged" data => mergedata("_devices_json", "_extra"); + # Mergedata combines device data, status counters, and optional + # extra top-level keys into a single data structure for mustache rendering. + # Note: pending-update inventory is emitted natively via + # inventory_fwupd_updates() rather than threaded through this cache, + # because Releases[0].Version cannot be accessed from mustache templates. + "_merged" data => mergedata("_devices_json", "_extra"); files: - "$(cache)" - create => "true", - template_method => "inline_mustache", - edit_template_string => "$(_template_body)", - template_data => @(_merged); - - "$(_measurements_path)" - content => "$(_measurements_body)"; + "$(cache)" + create => "true", + template_method => "inline_mustache", + edit_template_string => "$(_template_body)", + template_data => @(_merged); + + "$(_measurements_path)" content => "$(_measurements_body)"; } bundle agent inventory_fwupd_hsi(security_src) @@ -303,90 +353,117 @@ bundle agent inventory_fwupd_hsi(security_src) # An attribute is failing when its HsiResult differs from HsiResultSuccess. { classes: - "inventory_fwupd_hsi_failing_$(_idxes)" - not => strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"); + "inventory_fwupd_hsi_failing_$(_idxes)" + not => strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"); + + # Per-level failure/success classes for the HSI rollup. + # If ANY attribute at level N fails, _hsi_LN_fail is defined. + # If ANY attribute at level N passes, _hsi_LN_pass is defined. + "_hsi_L$(_level[$(_idxes)])_fail" + expression => "inventory_fwupd_hsi_failing_$(_idxes)"; - # Per-level failure/success classes for the HSI rollup. - # If ANY attribute at level N fails, _hsi_LN_fail is defined. - # If ANY attribute at level N passes, _hsi_LN_pass is defined. - "_hsi_L$(_level[$(_idxes)])_fail" - expression => "inventory_fwupd_hsi_failing_$(_idxes)"; - "_hsi_L$(_level[$(_idxes)])_pass" - not => "inventory_fwupd_hsi_failing_$(_idxes)"; + "_hsi_L$(_level[$(_idxes)])_pass" + not => "inventory_fwupd_hsi_failing_$(_idxes)"; vars: - "_sec" data => readjson("$(security_src)"); - "_idxes" slist => getindices("_sec[SecurityAttributes]"); - - "_name[$(_idxes)]" - string => "$(_sec[SecurityAttributes][$(_idxes)][Name])"; - "_level[$(_idxes)]" - string => "$(_sec[SecurityAttributes][$(_idxes)][HsiLevel])"; - "_result[$(_idxes)]" - string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResult])"; - "_success[$(_idxes)]" - string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResultSuccess])"; - - "_status[$(_idxes)]" - string => ifelse(strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"), - "PASS", "FAIL"); - - # Normalized name for per-attribute inventory: the CSME version - # attribute has a dynamic Name containing the firmware version - # (e.g. "csme v0:16.1.40.2765") which would create a different - # inventory attribute on every host. Normalize it to a fixed name - # so compliance conditions can reference it statically. - "_inv_name[$(_idxes)]" - string => ifelse(regcmp("csme v.*", "$(_name[$(_idxes)])"), - "CSME version", - "$(_name[$(_idxes)])"); - - # Normalized level for per-attribute inventory: fwupd marks some - # attributes as HsiLevel 0 with a "runtime-issue" flag even though - # they contribute to scored HSI levels. Map them to their HSI spec - # level so inventory attribute names match compliance conditions. - # UEFI secure boot (org.fwupd.hsi.Uefi.SecureBoot) → HSI:1 - # CET OS Support (org.fwupd.hsi.IntelCet.Active) → HSI:3 - "_inv_level[$(_idxes)]" - string => ifelse(strcmp("$(_name[$(_idxes)])", "UEFI secure boot"), - "1", - strcmp("$(_name[$(_idxes)])", "CET OS Support"), - "3", - "$(_level[$(_idxes)])"); - - # Per-attribute inventory: one string variable per HSI check, - # named "Firmware HSI L: " with value PASS or FAIL. - # This enables per-check compliance report conditions. - "fwupd_hsi_check[$(_idxes)]" - string => "$(_status[$(_idxes)])", - meta => { "inventory", - "attribute_name=Firmware HSI L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])" }; - - "fwupd_hsi_attr[$(_idxes)]" - string => "$(_name[$(_idxes)]) (HSI L$(_level[$(_idxes)])): $(_result[$(_idxes)]) [$(_status[$(_idxes)])]", - meta => { "inventory", "attribute_name=Firmware HSI attributes" }; - - "fwupd_hsi_failing_count" - int => countclassesmatching("inventory_fwupd_hsi_failing_[0-9]+"); - - # Rolled-up HSI level: highest level with a pass where no level - # at or below it has a failure. Checks highest first so the - # first match wins. Mirrors fwupd's fu_security_attrs_calculate_hsi(). - "fwupd_hsi_level" - string => ifelse( - and("_hsi_L4_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail"), not("_hsi_L3_fail"), not("_hsi_L4_fail")), "HSI:4", - and("_hsi_L3_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail"), not("_hsi_L3_fail")), "HSI:3", - and("_hsi_L2_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail")), "HSI:2", - and("_hsi_L1_pass", not("_hsi_L1_fail")), "HSI:1", - "HSI:0"), - meta => { "inventory", "attribute_name=Firmware HSI level" }; + "_sec" data => readjson("$(security_src)"); + "_idxes" slist => getindices("_sec[SecurityAttributes]"); + "_name[$(_idxes)]" string => "$(_sec[SecurityAttributes][$(_idxes)][Name])"; + + "_level[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiLevel])"; + + "_result[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResult])"; + + "_success[$(_idxes)]" + string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResultSuccess])"; + + "_status[$(_idxes)]" + string => ifelse( + strcmp("$(_result[$(_idxes)])", "$(_success[$(_idxes)])"), + "PASS", + "FAIL" + ); + + # Normalized name for per-attribute inventory: the CSME version + # attribute has a dynamic Name containing the firmware version + # (e.g. "csme v0:16.1.40.2765") which would create a different + # inventory attribute on every host. Normalize it to a fixed name + # so compliance conditions can reference it statically. + "_inv_name[$(_idxes)]" + string => ifelse( + regcmp("csme v.*", "$(_name[$(_idxes)])"), + "CSME version", + "$(_name[$(_idxes)])" + ); + + # Normalized level for per-attribute inventory: fwupd marks some + # attributes as HsiLevel 0 with a "runtime-issue" flag even though + # they contribute to scored HSI levels. Map them to their HSI spec + # level so inventory attribute names match compliance conditions. + # UEFI secure boot (org.fwupd.hsi.Uefi.SecureBoot) → HSI:1 + # CET OS Support (org.fwupd.hsi.IntelCet.Active) → HSI:3 + "_inv_level[$(_idxes)]" + string => ifelse( + strcmp("$(_name[$(_idxes)])", "UEFI secure boot"), + "1", + strcmp("$(_name[$(_idxes)])", "CET OS Support"), + "3", + "$(_level[$(_idxes)])" + ); + + # Per-attribute inventory: one string variable per HSI check, + # named "Firmware HSI L: " with value PASS or FAIL. + # This enables per-check compliance report conditions. + "fwupd_hsi_check[$(_idxes)]" + string => "$(_status[$(_idxes)])", + meta => { + "inventory", + "attribute_name=Firmware HSI L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])", + }; + + "fwupd_hsi_attr[$(_idxes)]" + string => "$(_name[$(_idxes)]) (HSI L$(_level[$(_idxes)])): $(_result[$(_idxes)]) [$(_status[$(_idxes)])]", + meta => { "inventory", "attribute_name=Firmware HSI attributes" }; + + "fwupd_hsi_failing_count" + int => countclassesmatching("inventory_fwupd_hsi_failing_[0-9]+"); + + # Rolled-up HSI level: highest level with a pass where no level + # at or below it has a failure. Checks highest first so the + # first match wins. Mirrors fwupd's fu_security_attrs_calculate_hsi(). + "fwupd_hsi_level" + string => ifelse( + and( + "_hsi_L4_pass", + not("_hsi_L1_fail"), + not("_hsi_L2_fail"), + not("_hsi_L3_fail"), + not("_hsi_L4_fail") + ), + "HSI:4", + and( + "_hsi_L3_pass", + not("_hsi_L1_fail"), + not("_hsi_L2_fail"), + not("_hsi_L3_fail") + ), + "HSI:3", + and("_hsi_L2_pass", not("_hsi_L1_fail"), not("_hsi_L2_fail")), + "HSI:2", + and("_hsi_L1_pass", not("_hsi_L1_fail")), + "HSI:1", + "HSI:0" + ), + meta => { "inventory", "attribute_name=Firmware HSI level" }; files: - # Write HSI failing count to its own measurements file so - # cf-monitord can track it as a time-series metric. - "$(sys.statedir)/fwupd/measurements_hsi" - content => "firmware_hsi_failing_attributes=$(fwupd_hsi_failing_count)", - if => isvariable("fwupd_hsi_failing_count"); + # Write HSI failing count to its own measurements file so + # cf-monitord can track it as a time-series metric. + "$(sys.statedir)/fwupd/measurements_hsi" + content => "firmware_hsi_failing_attributes=$(fwupd_hsi_failing_count)", + if => isvariable("fwupd_hsi_failing_count"); } bundle agent inventory_fwupd_updates(updates_src) @@ -402,17 +479,18 @@ bundle agent inventory_fwupd_updates(updates_src) # into JSON arrays, so we flatten it natively in CFEngine). { vars: - "_upd" data => readjson("$(updates_src)"); - "_idxes" slist => getindices("_upd[Devices]"); - - "_did[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][DeviceId])"; - "_name[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Name])"; - "_oldv[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Version])"; - "_newv[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Releases][0][Version])"; - - "fwupd_dev_pending[$(_did[$(_idxes)])]" - string => "$(_name[$(_idxes)]): $(_oldv[$(_idxes)]) -> $(_newv[$(_idxes)])", - meta => { "inventory", "attribute_name=Firmware device pending update" }; + "_upd" data => readjson("$(updates_src)"); + "_idxes" slist => getindices("_upd[Devices]"); + "_did[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][DeviceId])"; + "_name[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Name])"; + "_oldv[$(_idxes)]" string => "$(_upd[Devices][$(_idxes)][Version])"; + + "_newv[$(_idxes)]" + string => "$(_upd[Devices][$(_idxes)][Releases][0][Version])"; + + "fwupd_dev_pending[$(_did[$(_idxes)])]" + string => "$(_name[$(_idxes)]): $(_oldv[$(_idxes)]) -> $(_newv[$(_idxes)])", + meta => { "inventory", "attribute_name=Firmware device pending update" }; } bundle agent inventory_fwupd_devices(devices_src) @@ -425,19 +503,18 @@ bundle agent inventory_fwupd_devices(devices_src) # Name silently skip via the if => not(strcmp(...)) guard. { vars: - "_dev" data => readjson("$(devices_src)"); - "_idxes" slist => getindices("_dev[Devices]"); - - "_did[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][DeviceId])"; - "_name[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Name])"; - "_vend[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Vendor])"; - "_ver[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Version])"; - "_plug[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Plugin])"; - - "fwupd_dev[$(_did[$(_idxes)])]" - string => "$(_name[$(_idxes)]) | $(_vend[$(_idxes)]) | v$(_ver[$(_idxes)]) | [$(_plug[$(_idxes)])]", - meta => { "inventory", "attribute_name=Firmware devices" }, - if => not(strcmp("$(_name[$(_idxes)])", "")); + "_dev" data => readjson("$(devices_src)"); + "_idxes" slist => getindices("_dev[Devices]"); + "_did[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][DeviceId])"; + "_name[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Name])"; + "_vend[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Vendor])"; + "_ver[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Version])"; + "_plug[$(_idxes)]" string => "$(_dev[Devices][$(_idxes)][Plugin])"; + + "fwupd_dev[$(_did[$(_idxes)])]" + string => "$(_name[$(_idxes)]) | $(_vend[$(_idxes)]) | v$(_ver[$(_idxes)]) | [$(_plug[$(_idxes)])]", + meta => { "inventory", "attribute_name=Firmware devices" }, + if => not(strcmp("$(_name[$(_idxes)])", "")); } bundle monitor inventory_fwupd_monitor @@ -481,12 +558,12 @@ bundle monitor inventory_fwupd_monitor body match_value inventory_fwupd_kv(key) { - select_line_matching => "$(key)=.*"; - extraction_regex => "$(key)=(\d+)"; + select_line_matching => "$(key)=.*"; + extraction_regex => "$(key)=(\d+)"; } bundle agent __main__ { methods: - "main" usebundle => inventory_fwupd_main; + "main" usebundle => inventory_fwupd_main; } diff --git a/inventory/inventory-smartctl/cfbs.json b/inventory/inventory-smartctl/cfbs.json index 6916e94d..2babe4e9 100644 --- a/inventory/inventory-smartctl/cfbs.json +++ b/inventory/inventory-smartctl/cfbs.json @@ -3,9 +3,7 @@ "description": "Inventory SMART drive health, temperature, and wear data", "tags": ["inventory", "monitoring", "hardware", "storage", "smartctl"], "version": "0.1.0", - "steps": [ - "copy ./policy.cf services/inventory/smartctl.cf" - ], + "steps": ["copy ./policy.cf services/inventory/smartctl.cf"], "dependencies": [], "subdirectory": "inventory/inventory-smartctl" } diff --git a/inventory/inventory-smartctl/policy.cf b/inventory/inventory-smartctl/policy.cf index bafd762c..7aae3302 100644 --- a/inventory/inventory-smartctl/policy.cf +++ b/inventory/inventory-smartctl/policy.cf @@ -1,6 +1,6 @@ body file control { - namespace => "inventory_smartctl"; + namespace => "inventory_smartctl"; } bundle agent main @@ -22,25 +22,35 @@ bundle agent main { vars: linux:: - "_smartctl" string => ifelse( - fileexists("/usr/sbin/smartctl"), "/usr/sbin/smartctl", - fileexists("/sbin/smartctl"), "/sbin/smartctl", - "/usr/sbin/smartctl" # default fallback - ); - "_sdir" string => "$(sys.statedir)"; - "_cache_ttl" string => "3600"; # 1 hour - + "_smartctl" + string => ifelse( + fileexists("/usr/sbin/smartctl"), + "/usr/sbin/smartctl", + fileexists("/sbin/smartctl"), + "/sbin/smartctl", + "/usr/sbin/smartctl" + # default fallback + ); + + "_sdir" string => "$(sys.statedir)"; + "_cache_ttl" string => "3600"; + + # 1 hour # Enumerate drives - extract first field from each line of smartctl --scan "_scan_lines" slist => splitstring( - execresult("$(_smartctl) --scan 2>/dev/null", "useshell"), - "\n", 32); + execresult("$(_smartctl) --scan 2>/dev/null", "useshell"), "\n", 32 + ); "_drives" - slist => maplist(regex_replace("$(this)", "^(\S+).*", "\1", ""), "_scan_lines"); + slist => maplist( + regex_replace("$(this)", "^(\S+).*", "\1", ""), "_scan_lines" + ); "_id[${_drives}]" string => canonify("${_drives}"); - "_cache[${_drives}]" string => "$(_sdir)/inventory_smartctl_${_id[${_drives}]}.json"; + + "_cache[${_drives}]" + string => "$(_sdir)/inventory_smartctl_${_id[${_drives}]}.json"; linux._have_smartctl:: # Rolled-up status: OK or DEGRADED (SMARTCTL_MISSING when smartctl absent) @@ -115,20 +125,23 @@ bundle agent main "_cache_stale_${_id[${_drives}]}" expression => isgreaterthan( eval("$(sys.systime) - $(filestat(${_cache[${_drives}]}, mtime))"), - "$(_cache_ttl)"), + "$(_cache_ttl)" + ), if => fileexists("${_cache[${_drives}]}"); # Refresh if missing or stale "_refresh_${_id[${_drives}]}" or => { "_cache_missing_${_id[${_drives}]}", - "_cache_stale_${_id[${_drives}]}" + "_cache_stale_${_id[${_drives}]}", }; files: linux._have_smartctl:: "${_cache[${_drives}]}" - content => execresult("$(_smartctl) -j -a ${_drives}", "noshell", "stdout"), + content => execresult( + "$(_smartctl) -j -a ${_drives}", "noshell", "stdout" + ), if => "_refresh_${_id[${_drives}]}"; methods: @@ -142,6 +155,7 @@ bundle agent main reports: linux._have_smartctl.verbose_mode:: "inventory_smartctl: monitoring ${_drives}"; + "inventory_smartctl: ${_drives} health=${_d_${_id[${_drives}]}[health]}" if => isvariable("_d_${_id[${_drives}]}[health]"); @@ -153,58 +167,82 @@ bundle agent parse(drive, cache_file) # @brief Parse smartctl JSON and return key metrics via bundle_return_value_index { vars: - "_json" data => readjson("$(cache_file)"); + "_json" data => readjson("$(cache_file)"); + + # Extract metrics directly from JSON + "_health" + string => ifelse( + strcmp("${_json[smart_status][passed]}", "true"), "PASSED", "FAILED" + ), + if => isvariable("_json[smart_status][passed]"); + + "_model" + string => "${_json[model_name]}", + if => isvariable("_json[model_name]"); + + "_temp" + string => "${_json[temperature][current]}", + if => isvariable("_json[temperature][current]"); + + "_hours" + string => "${_json[power_on_time][hours]}", + if => isvariable("_json[power_on_time][hours]"); + + "_nvme_spare" + string => "${_json[nvme_smart_health_information_log][available_spare]}", + if => isvariable( + "_json[nvme_smart_health_information_log][available_spare]" + ); - # Extract metrics directly from JSON - "_health" - string => ifelse(strcmp("${_json[smart_status][passed]}", "true"), "PASSED", "FAILED"), - if => isvariable("_json[smart_status][passed]"); + "_nvme_pct_used" + string => "${_json[nvme_smart_health_information_log][percentage_used]}", + if => isvariable( + "_json[nvme_smart_health_information_log][percentage_used]" + ); - "_model" - string => "${_json[model_name]}", - if => isvariable("_json[model_name]"); + "_nvme_media_errors" + string => "${_json[nvme_smart_health_information_log][media_errors]}", + if => isvariable( + "_json[nvme_smart_health_information_log][media_errors]" + ); - "_temp" - string => "${_json[temperature][current]}", - if => isvariable("_json[temperature][current]"); + reports: + "$(_health)" + bundle_return_value_index => "health", + if => isvariable("_health"); - "_hours" - string => "${_json[power_on_time][hours]}", - if => isvariable("_json[power_on_time][hours]"); + "$(_model)" + bundle_return_value_index => "model", + if => isvariable("_model"); - "_nvme_spare" - string => "${_json[nvme_smart_health_information_log][available_spare]}", - if => isvariable("_json[nvme_smart_health_information_log][available_spare]"); + "$(_temp)" + bundle_return_value_index => "temp", + if => isvariable("_temp"); - "_nvme_pct_used" - string => "${_json[nvme_smart_health_information_log][percentage_used]}", - if => isvariable("_json[nvme_smart_health_information_log][percentage_used]"); + "$(_hours)" + bundle_return_value_index => "hours", + if => isvariable("_hours"); - "_nvme_media_errors" - string => "${_json[nvme_smart_health_information_log][media_errors]}", - if => isvariable("_json[nvme_smart_health_information_log][media_errors]"); + "$(_nvme_spare)" + bundle_return_value_index => "nvme_spare", + if => isvariable("_nvme_spare"); - reports: - "$(_health)" bundle_return_value_index => "health", - if => isvariable("_health"); - "$(_model)" bundle_return_value_index => "model", - if => isvariable("_model"); - "$(_temp)" bundle_return_value_index => "temp", - if => isvariable("_temp"); - "$(_hours)" bundle_return_value_index => "hours", - if => isvariable("_hours"); - "$(_nvme_spare)" bundle_return_value_index => "nvme_spare", - if => isvariable("_nvme_spare"); - "$(_nvme_pct_used)" bundle_return_value_index => "nvme_pct_used", - if => isvariable("_nvme_pct_used"); - "$(_nvme_media_errors)" bundle_return_value_index => "nvme_media_errors", - if => isvariable("_nvme_media_errors"); + "$(_nvme_pct_used)" + bundle_return_value_index => "nvme_pct_used", + if => isvariable("_nvme_pct_used"); + + "$(_nvme_media_errors)" + bundle_return_value_index => "nvme_media_errors", + if => isvariable("_nvme_media_errors"); } -body file control { namespace => "default"; } +body file control +{ + namespace => "default"; +} bundle agent __main__ { methods: - "inventory_smartctl:main"; + "inventory_smartctl:main"; } diff --git a/inventory/inventory-windows-services/inventory-windows-services.cf b/inventory/inventory-windows-services/inventory-windows-services.cf index ca7b25f8..d37ff9e4 100644 --- a/inventory/inventory-windows-services/inventory-windows-services.cf +++ b/inventory/inventory-windows-services/inventory-windows-services.cf @@ -5,24 +5,21 @@ bundle agent inventory_windows_services_running vars: windows:: "_cache" string => "$(sys.statedir)/windows-services-running.json"; - "_data" data => readjson("$(_cache)"); "_indices" slist => getindices(_data); "i[$(_indices)]" string => "$(_data[$(_indices)][DisplayName])", - meta => { "inventory", "attribute_name=Windows services running"}; - + meta => { "inventory", "attribute_name=Windows services running" }; commands: windows:: "Get-Service | Where-Object {$_.Status -eq 'Running'} | ConvertTo-Json | Set-Content -Path '${_cache}'" contain => powershell; - } bundle agent __main__ { methods: - "inventory_windows_services_running"; + "inventory_windows_services_running"; } diff --git a/management/autorun-bundles/def.json b/management/autorun-bundles/def.json index 045031b7..385c3e7e 100644 --- a/management/autorun-bundles/def.json +++ b/management/autorun-bundles/def.json @@ -1,5 +1,3 @@ { - "classes": { - "services_autorun_bundles": ["any"] - } + "classes": { "services_autorun_bundles": ["any"] } } diff --git a/management/autorun-inputs/def.json b/management/autorun-inputs/def.json index 90faea3a..c44ff2a1 100644 --- a/management/autorun-inputs/def.json +++ b/management/autorun-inputs/def.json @@ -1,5 +1,3 @@ { - "classes": { - "services_autorun_inputs": ["any"] - } + "classes": { "services_autorun_inputs": ["any"] } } diff --git a/management/autorun/def.json b/management/autorun/def.json index d2090d26..193a0b30 100644 --- a/management/autorun/def.json +++ b/management/autorun/def.json @@ -1,5 +1,3 @@ { - "classes": { - "services_autorun": ["any"] - } + "classes": { "services_autorun": ["any"] } } diff --git a/management/command-dispatcher/main.cf b/management/command-dispatcher/main.cf index 3f968053..2a309960 100644 --- a/management/command-dispatcher/main.cf +++ b/management/command-dispatcher/main.cf @@ -2,18 +2,16 @@ body file control { namespace => "command_dispatcher"; } + bundle agent main { classes: - "enabled" - expression => isvariable("commands_to_run"); - "run_$(i)" - expression => "$(_condition[$(i)])"; + "enabled" expression => isvariable("commands_to_run"); + "run_$(i)" expression => "$(_condition[$(i)])"; vars: enabled:: - "i" - slist => getindices(commands_to_run); + "i" slist => getindices(commands_to_run); "_command[$(i)]" string => "$(commands_to_run[$(i)][command])", @@ -23,17 +21,20 @@ bundle agent main string => ifelse( not(strcmp("$(commands_to_run[$(i)][condition])", "")), "$(commands_to_run[$(i)][condition])", - "any"); + "any" + ); "_ifelapsed[$(i)]" string => ifelse( not(strcmp("$(commands_to_run[$(i)][ifelapsed])", "")), "$(commands_to_run[$(i)][ifelapsed])", - "5"); + "5" + ); reports: enabled:: "Command [$(i)]: $(_command[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + !enabled:: "Command-dispatcher: commands_to_run variable not found"; @@ -44,20 +45,24 @@ bundle agent main action => ifelapsed("$(_ifelapsed[$(i)])"), contain => in_shell; } + body contain in_shell { useshell => "true"; exec_owner => "root"; exec_timeout => "300"; } + body action ifelapsed(x) { ifelapsed => "$(x)"; } + body file control { namespace => "default"; } + bundle agent __main__ { methods: diff --git a/management/demo/def.json b/management/demo/def.json index ea609c9e..ed6de96a 100644 --- a/management/demo/def.json +++ b/management/demo/def.json @@ -1,14 +1,14 @@ { - "classes": { - "mpf_augments_control_enabled": ["any"], - "cfengine_mp_fr_dependencies_auto_install": ["any"] - }, - "vars": { - "acl": ["0.0.0.0/0", "::/0"], - "default_data_select_host_monitoring_include": [".*"], - "default_data_select_policy_hub_monitoring_include": [".*"], - "control_executor_splaytime": "1", - "control_executor_schedule": ["any"], - "control_hub_hub_schedule": ["any"] - } + "classes": { + "mpf_augments_control_enabled": ["any"], + "cfengine_mp_fr_dependencies_auto_install": ["any"] + }, + "vars": { + "acl": ["0.0.0.0/0", "::/0"], + "default_data_select_host_monitoring_include": [".*"], + "default_data_select_policy_hub_monitoring_include": [".*"], + "control_executor_splaytime": "1", + "control_executor_schedule": ["any"], + "control_hub_hub_schedule": ["any"] + } } diff --git a/management/disable-automatic-key-trust/def.json b/management/disable-automatic-key-trust/def.json index 6dde1534..3a57315e 100644 --- a/management/disable-automatic-key-trust/def.json +++ b/management/disable-automatic-key-trust/def.json @@ -1 +1,3 @@ -{ "variables": {"default:def.trustkeysfrom": []} } +{ + "variables": { "default:def.trustkeysfrom": [] } +} diff --git a/management/disable-recommendations/def.json b/management/disable-recommendations/def.json index 715b0b42..48f83646 100644 --- a/management/disable-recommendations/def.json +++ b/management/disable-recommendations/def.json @@ -1,8 +1,8 @@ { - "classes": { - "default:cfengine_recommendations_disabled": { - "class_expressions": [ "any::" ], - "comment": "We disabled all recommendations emitted by the MPF to quiet policy output." - } + "classes": { + "default:cfengine_recommendations_disabled": { + "class_expressions": ["any::"], + "comment": "We disabled all recommendations emitted by the MPF to quiet policy output." } + } } diff --git a/management/every-minute/def.json b/management/every-minute/def.json index 0b329788..9f4fbbb8 100644 --- a/management/every-minute/def.json +++ b/management/every-minute/def.json @@ -1,7 +1,7 @@ { - "vars": { - "control_executor_splaytime": "1", - "control_executor_schedule": ["any"], - "control_hub_hub_schedule": ["any"] - } + "vars": { + "control_executor_splaytime": "1", + "control_executor_schedule": ["any"], + "control_hub_hub_schedule": ["any"] + } } diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index c9d5e7ae..6c651c34 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -12,20 +12,20 @@ bundle agent allowed # via CMDB or augments to control firmware update behavior. { vars: - "apply_updates" - string => "!any", - if => not(isvariable("apply_updates")), - comment => "Default disabled; set to a class expression to enable firmware updates"; - - "device_name_reglist" - slist => {}, - if => not(isvariable("device_name_reglist")), - comment => "Default empty allow-list; override via CMDB or augments"; - - "reboot_after_update" - string => "!any", - if => not(isvariable("reboot_after_update")), - comment => "Default disabled; set to a class expression to reboot after firmware update"; + "apply_updates" + string => "!any", + if => not(isvariable("apply_updates")), + comment => "Default disabled; set to a class expression to enable firmware updates"; + + "device_name_reglist" + slist => {}, + if => not(isvariable("device_name_reglist")), + comment => "Default empty allow-list; override via CMDB or augments"; + + "reboot_after_update" + string => "!any", + if => not(isvariable("reboot_after_update")), + comment => "Default disabled; set to a class expression to reboot after firmware update"; } bundle agent main @@ -60,18 +60,20 @@ bundle agent main { vars: linux:: - "_update_marker" - string => "$(sys.statedir)/fwupd/update_applied"; + "_update_marker" string => "$(sys.statedir)/fwupd/update_applied"; "_systemd_run" - string => ifelse(isexecutable("/bin/systemd-run"), "/bin/systemd-run", - isexecutable("/usr/bin/systemd-run"), "/usr/bin/systemd-run", - ""), + string => ifelse( + isexecutable("/bin/systemd-run"), + "/bin/systemd-run", + isexecutable("/usr/bin/systemd-run"), + "/usr/bin/systemd-run", + "" + ), comment => "Path to systemd-run when available (empty string otherwise)"; linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: - "_updates_cache" - string => "$(sys.statedir)/fwupd/inventory_updates.json"; + "_updates_cache" string => "$(sys.statedir)/fwupd/inventory_updates.json"; "_updates" data => readjson("$(_updates_cache)"), @@ -114,7 +116,10 @@ bundle agent main linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: "_device_allowed_$(_dev_idx)" - expression => regcmp("$(manage_fwupd:allowed.device_name_reglist)", "$(_dev_name[$(_dev_idx)])"), + expression => regcmp( + "$(manage_fwupd:allowed.device_name_reglist)", + "$(_dev_name[$(_dev_idx)])" + ), comment => "Device $(_dev_name[$(_dev_idx)]) matches allow-list entry"; packages: @@ -128,7 +133,6 @@ bundle agent main "fwupd-refresh.timer" service_policy => "enabled", comment => "Ensure the fwupd metadata refresh timer is enabled so the LVFS firmware catalog stays current"; - # --- Stale marker cleanup ---------------------------------------- files: linux:: @@ -136,17 +140,21 @@ bundle agent main delete => default:tidy, if => not("_update_applied"), comment => "Remove stale marker from a previous boot cycle"; - # --- Apply firmware updates -------------------------------------- commands: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: "$(default:inventory_fwupd_main.fwupdmgr)" - arglist => { "update", "--no-reboot-check", "$(_dev_id[$(_dev_idx)])" }, - handle => canonify("manage_fwupd_apply_device_$(_dev_idx)_$(_dev_name[$(_dev_idx)])"), + arglist => { + "update", "--no-reboot-check", "$(_dev_id[$(_dev_idx)])" + }, + handle => canonify( + "manage_fwupd_apply_device_$(_dev_idx)_$(_dev_name[$(_dev_idx)])" + ), if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), - classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), + classes => default:results( + "bundle", "manage_fwupd_update_$(_dev_idx)" + ), comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; - # --- Post-update marker and reboot -------------------------------- files: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -166,18 +174,20 @@ bundle agent main arglist => { "--unit=cfengine-fwupd-reboot", "--description=Reboot after cf-agent exits for firmware update", - "/bin/bash", "-c", - "while kill -0 $(this.promiser_pid) 2>/dev/null; do sleep 1; done; /sbin/shutdown -r now 'Rebooting for firmware update applied by CFEngine'" + "/bin/bash", + "-c", + "while kill -0 $(this.promiser_pid) 2>/dev/null; do sleep 1; done; /sbin/shutdown -r now 'Rebooting for firmware update applied by CFEngine'", }, handle => "manage_fwupd_reboot_after_update", comment => "Deferred reboot: polls for cf-agent to exit, then reboots immediately"; manage_fwupd:_update_applied.manage_fwupd:reboot_after_update.!manage_fwupd:_have_systemd_run:: "/sbin/shutdown" - arglist => { "-r", "+1", "Rebooting for firmware update applied by CFEngine" }, + arglist => { + "-r", "+1", "Rebooting for firmware update applied by CFEngine" + }, handle => "manage_fwupd_reboot_after_update_fallback", comment => "Fallback reboot for non-systemd systems (shutdown -r +1)"; - # --- Inventory -------------------------------------------------- vars: linux.default:have_fwupdmgr:: diff --git a/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf b/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf index 17200366..0cb0ab2f 100644 --- a/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf +++ b/management/package-method-ansible-galaxy-collection/package-method-ansible-galaxy-collections.cf @@ -1,15 +1,12 @@ body package_method ansible_galaxy_collection { - package_changes => "bulk"; - package_name_convention => "$(name)"; - package_delete_convention => "$(name)"; - - package_installed_regex => ".*"; - package_list_name_regex => '^"([^"]*)",.*'; - package_list_version_regex => '.*,"([^"]*)".*'; - - - package_list_command => "ansible-galaxy collection list"; - package_delete_command => 'echo "Deleting an Ansible Galaxy Collection is not supported"'; - package_add_command => "ansible-galaxy collection install "; + package_changes => "bulk"; + package_name_convention => "$(name)"; + package_delete_convention => "$(name)"; + package_installed_regex => ".*"; + package_list_name_regex => '^"([^"]*)",.*'; + package_list_version_regex => '.*,"([^"]*)".*'; + package_list_command => "ansible-galaxy collection list"; + package_delete_command => 'echo "Deleting an Ansible Galaxy Collection is not supported"'; + package_add_command => "ansible-galaxy collection install "; } diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf index 8c3db90d..1da97b86 100644 --- a/management/package-method-winget/package-method-winget.cf +++ b/management/package-method-winget/package-method-winget.cf @@ -2,54 +2,57 @@ # 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:"; + 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}"), + "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}"), + + "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"; } - diff --git a/management/package-method-winget/winget-installed.cf b/management/package-method-winget/winget-installed.cf index a14363ed..2d558ad2 100644 --- a/management/package-method-winget/winget-installed.cf +++ b/management/package-method-winget/winget-installed.cf @@ -7,29 +7,45 @@ 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}"), + "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"), + "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."; } diff --git a/management/powershell-execution-policy/powershell-execution-policy.cf b/management/powershell-execution-policy/powershell-execution-policy.cf index 4f65904b..5544ec99 100644 --- a/management/powershell-execution-policy/powershell-execution-policy.cf +++ b/management/powershell-execution-policy/powershell-execution-policy.cf @@ -4,29 +4,37 @@ 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}'"; + "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]}", + + "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; + "${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"); + "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; + contain => powershell; } diff --git a/management/windows-capability/windows-capability.cf b/management/windows-capability/windows-capability.cf index 739bb231..c1d8ad22 100644 --- a/management/windows-capability/windows-capability.cf +++ b/management/windows-capability/windows-capability.cf @@ -1,12 +1,19 @@ bundle agent windows_capability_installed(capability_name) { methods: - "Installed" usebundle => windows_capability:_windows_capability_state("${capability_name}", "Installed"); + "Installed" + usebundle => windows_capability:_windows_capability_state( + "${capability_name}", "Installed" + ); } + bundle agent windows_optionial_capability_notpresent(capability_name) { methods: - "NotPresent" usebundle => windows_capability:_windows_capability_state("${capability_name}", "NotPresent"); + "NotPresent" + usebundle => windows_capability:_windows_capability_state( + "${capability_name}", "NotPresent" + ); } bundle agent windows_capability @@ -20,7 +27,6 @@ body file control { namespace => "windows_capability"; } - # https://learn.microsoft.com/en-us/powershell/module/dism/get-windowscapability?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/powershell/module/dism/remove-windowscapability?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/powershell/module/dism/add-windowscapability?view=windowsserver2022-ps @@ -28,12 +34,17 @@ bundle agent _inventory { vars: windows:: - "cache_file" string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; - "command" string => "Get-WindowsCapability -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; + "cache_file" + string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; + + "command" + string => "Get-WindowsCapability -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; "csv" data => readcsv("${cache_file}"); "i" slist => getindices("csv"); - "${this.namespace}[${i}]" string => "${csv[${i}][0]}:${csv[${i}][1]}", + + "${this.namespace}[${i}]" + string => "${csv[${i}][0]}:${csv[${i}][1]}", meta => { "inventory", "attribute_name=Windows Capability" }; files: @@ -42,20 +53,27 @@ bundle agent _inventory delete => default:tidy; commands: - windows:: - "${command}" - if => not(fileexists("${cache_file}")), - contain => default:powershell; + windows:: + "${command}" + if => not(fileexists("${cache_file}")), + contain => default:powershell; } - bundle agent _windows_capability_state(capability_name, desired_state) { vars: - "operation" string => ifelse(strcmp("${desired_state}", "Installed"), "Add", "Remove"); + "operation" + string => ifelse( + strcmp("${desired_state}", "Installed"), "Add", "Remove" + ); + classes: windows:: - "state_not_ok" expression => returnszero("if((Get-WindowsCapability -Online -Name ${capability_name} | select-object -expandproperty state) -ne '${desired_state}'){exit 0}else{exit 1}", "powershell"); + "state_not_ok" + expression => returnszero( + "if((Get-WindowsCapability -Online -Name ${capability_name} | select-object -expandproperty state) -ne '${desired_state}'){exit 0}else{exit 1}", + "powershell" + ); commands: windows.state_not_ok:: @@ -65,6 +83,5 @@ bundle agent _windows_capability_state(capability_name, desired_state) files: windows.state_repaired:: - "${_inventory.cache_file}" - delete => default:tidy; + "${_inventory.cache_file}" delete => default:tidy; } diff --git a/management/windows-optional-feature/windows-optional-feature.cf b/management/windows-optional-feature/windows-optional-feature.cf index 27aec9b5..878e363b 100644 --- a/management/windows-optional-feature/windows-optional-feature.cf +++ b/management/windows-optional-feature/windows-optional-feature.cf @@ -2,17 +2,22 @@ # https://learn.microsoft.com/en-us/powershell/module/dism/disable-windowsoptionalfeature?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/powershell/module/dism/enable-windowsoptionalfeature?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v#enable-hyper-v-using-powershell - -bundle agent windows_optional_feature_enabled( feature_name ) +bundle agent windows_optional_feature_enabled(feature_name) { methods: - "Enabled" usebundle => windows_optional_feature:_promise_state("${feature_name}", "Enabled"); + "Enabled" + usebundle => windows_optional_feature:_promise_state( + "${feature_name}", "Enabled" + ); } -bundle agent windows_optional_feature_disabled( feature_name ) +bundle agent windows_optional_feature_disabled(feature_name) { methods: - "Disabled" usebundle => windows_optional_feature:_promise_state("${feature_name}", "Disabled"); + "Disabled" + usebundle => windows_optional_feature:_promise_state( + "${feature_name}", "Disabled" + ); } bundle agent windows_optional_feature @@ -31,11 +36,17 @@ bundle agent _inventory { vars: windows:: - "cache_file" string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; - "command" string => "Get-WindowsOptionalFeature -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; + "cache_file" + string => "${sys.statedir}${const.dirsep}${this.namespace}_cache.csv"; + + "command" + string => "Get-WindowsOptionalFeature -Online | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${cache_file}'"; + "csv" data => readcsv("${cache_file}"); "i" slist => getindices("csv"); - "${this.namespace}[${i}]" string => "${csv[${i}][0]}:${csv[${i}][1]}", + + "${this.namespace}[${i}]" + string => "${csv[${i}][0]}:${csv[${i}][1]}", meta => { "inventory", "attribute_name=Windows Optional Features" }; files: @@ -45,29 +56,36 @@ bundle agent _inventory commands: windows:: - "${command}" - if => not(fileexists("${cache_file}")), - contain => default:powershell; + "${command}" + if => not(fileexists("${cache_file}")), + contain => default:powershell; } bundle agent _promise_state(feature_name, desired_state) { vars: - "operation" string => ifelse(strcmp("${desired_state}", "Enabled"), "Enable", "Disable"); + "operation" + string => ifelse( + strcmp("${desired_state}", "Enabled"), "Enable", "Disable" + ); + classes: windows:: - "state_not_ok" expression => returnszero( "if((Get-WindowsOptionalFeature -Online -FeatureName ${feature_name} | select-object -expandproperty state) -ne '${state_name}'){exit 0}else{exit 1}", "powershell"); + "state_not_ok" + expression => returnszero( + "if((Get-WindowsOptionalFeature -Online -FeatureName ${feature_name} | select-object -expandproperty state) -ne '${state_name}'){exit 0}else{exit 1}", + "powershell" + ); + commands: windows.state_not_ok:: -# -All enables parent features if need be + # -All enables parent features if need be "${operation}-WindowsOptionalFeature -Online -FeatureName ${feature_name} -NoRestart -All" contain => default:powershell, classes => default:results("bundle", "state"); -# -NoRestart so that we don't wait forever for a Yes reply from a user -# then if we ran this command due to needing to, restart below. - + # -NoRestart so that we don't wait forever for a Yes reply from a user + # then if we ran this command due to needing to, restart below. files: windows.state_repaired:: - "${_inventory.cache_file}" - delete => default:tidy; + "${_inventory.cache_file}" delete => default:tidy; } diff --git a/promise-types/ansible/example.cf b/promise-types/ansible/example.cf index 6d55e707..29f54287 100644 --- a/promise-types/ansible/example.cf +++ b/promise-types/ansible/example.cf @@ -9,7 +9,7 @@ bundle agent main { ansible: "my_playbook" - playbook => "/tmp/playbook.yaml", + playbook => "/tmp/playbook.yaml", inventory => "/tmp/inventory.yaml", - tags => {"helloworld"}; + tags => { "helloworld" }; } diff --git a/promise-types/appstreams/init.cf b/promise-types/appstreams/init.cf index 24252ab6..bad84430 100644 --- a/promise-types/appstreams/init.cf +++ b/promise-types/appstreams/init.cf @@ -1,6 +1,6 @@ promise agent appstreams # @brief Define appstreams promise type { - path => "$(sys.workdir)/modules/promises/appstreams.py"; - interpreter => "/usr/bin/python3"; + path => "$(sys.workdir)/modules/promises/appstreams.py"; + interpreter => "/usr/bin/python3"; } diff --git a/promise-types/git/example.cf b/promise-types/git/example.cf index ea8f9b64..7d710d1b 100644 --- a/promise-types/git/example.cf +++ b/promise-types/git/example.cf @@ -9,7 +9,7 @@ bundle agent main { git: "starter_pack_repo" - repository => "https://github.com/cfengine/starter_pack", + repository => "https://github.com/cfengine/starter_pack", destination => "$(this.promise_dirname)/starter-pack", - version => "master"; + version => "master"; } diff --git a/promise-types/git/test.cf b/promise-types/git/test.cf index 6fb482ac..ef3fc38b 100644 --- a/promise-types/git/test.cf +++ b/promise-types/git/test.cf @@ -2,9 +2,7 @@ body common control { version => "1.0"; } - ####################################################### - bundle agent init { files: @@ -13,8 +11,7 @@ bundle agent init file_select => all, delete => init_delete; - "$(this.promise_dirname)/starter-pack/." - delete => init_delete; + "$(this.promise_dirname)/starter-pack/." delete => init_delete; } body depth_search aggressive @@ -35,15 +32,13 @@ body file_select all body delete init_delete { dirlinks => "delete"; - rmdirs => "true"; + rmdirs => "true"; } - ####################################################### - promise agent git { - path => "$(this.promise_dirname)/git.py"; - interpreter => "/usr/bin/python3"; + path => "$(this.promise_dirname)/git.py"; + interpreter => "/usr/bin/python3"; } bundle agent test @@ -54,34 +49,32 @@ bundle agent test git: "starter_pack_repo" - repository => "https://github.com/cfengine/starter_pack", + repository => "https://github.com/cfengine/starter_pack", destination => "$(this.promise_dirname)/starter-pack", - version => "master"; + version => "master"; classes: - "promise_repaired" - expression => canonify("starter_pack_repo_updated"), - scope => "namespace"; + "promise_repaired" + expression => canonify("starter_pack_repo_updated"), + scope => "namespace"; } - ####################################################### - bundle agent check { classes: - "file_ok" - if => fileexists("$(this.promise_dirname)/starter-pack/.git"); - - "ok" expression => "file_ok.promise_repaired"; + "file_ok" if => fileexists("$(this.promise_dirname)/starter-pack/.git"); + "ok" expression => "file_ok.promise_repaired"; reports: DEBUG.file_ok:: "file_ok"; + DEBUG.promise_repaired:: "promise_repaired"; ok:: "$(this.promise_filename) Pass"; + !ok:: "$(this.promise_filename) FAIL"; } diff --git a/promise-types/groups/groups.cf b/promise-types/groups/groups.cf index f8c60ccd..5ab2f389 100644 --- a/promise-types/groups/groups.cf +++ b/promise-types/groups/groups.cf @@ -3,8 +3,6 @@ # Note: As of 3.20.0, custom promise modules support the use of custom bodies. # Versions before 3.20.0 uses JSON strings or data containers to define # members instead. - - promise agent groups # @brief Define groups promise type. { @@ -21,17 +19,16 @@ body members foo exclude => { "malcom" }; } @else + bundle common foo # @breif Define members using JSON string. { vars: - "members" - string => '{ "include": ["alice", "bob"], + "members" + string => '{ "include": ["alice", "bob"], "exclude": ["malcom"] }'; } @endif - - @if minimum_version(3.20) body members bar # @breif Define members for group bar using attribute `only`. @@ -39,58 +36,51 @@ body members bar only => { "alice" }; } @else + bundle common bar # @brief Define members using data container. { vars: - "members" - data => '{ "only": ["alice"] }'; + "members" data => '{ "only": ["alice"] }'; } @endif - bundle agent example_groups # @breif Example groups promise statements. { groups: - "foo" - policy => "present", + "foo" + policy => "present", @if minimum_version(3.20) - members => foo; + members => foo; @else - members => "$(foo.members)"; + members => "$(foo.members)"; @endif - "bar" + "bar" # policy defaults to present @if minimum_version(3.20) - members => bar, + members => bar, @else - members => "@(bar.members)", + members => "@(bar.members)", @endif - gid => "1234"; + gid => "1234"; - "baz" - policy => "absent"; + "baz" policy => "absent"; } - bundle agent example_users # @breif Create users for the groups example. { users: - "alice" - policy => "present"; - "bob" - policy => "present"; - "malcom" - policy => "present"; + "alice" policy => "present"; + "bob" policy => "present"; + "malcom" policy => "present"; } - bundle agent __main__ { methods: - "example_users"; - "example_groups"; + "example_users"; + "example_groups"; } diff --git a/promise-types/http/example.cf b/promise-types/http/example.cf index 1e76909d..3fd2050c 100644 --- a/promise-types/http/example.cf +++ b/promise-types/http/example.cf @@ -5,20 +5,23 @@ promise agent http interpreter => "/usr/bin/python3"; } -bundle agent backup_file(file) { +bundle agent backup_file(file) +{ http: - "http://localhost:8080/backups/$file" - method => "POST", - payload => "@$(file)", - if => not(canonify(concat("$(this.promiser)", "_POST_request_done"))); + "http://localhost:8080/backups/$file" + method => "POST", + payload => "@$(file)", + if => not(canonify(concat("$(this.promiser)", "_POST_request_done"))); } -bundle agent __main__ { +bundle agent __main__ +{ http: - "https://cfengine.com/images/cfengine-logo.svg" - file => "/var/cfengine/cfengine-logo.svg", - if => not(fileexists("/var/cfengine/cfengine-logo.svg")); + "https://cfengine.com/images/cfengine-logo.svg" + file => "/var/cfengine/cfengine-logo.svg", + if => not(fileexists("/var/cfengine/cfengine-logo.svg")); methods: - "Backup cf-lock.lmdb" usebundle => backup_file("/var/cfengine/state/cf_lock.lmdb"); + "Backup cf-lock.lmdb" + usebundle => backup_file("/var/cfengine/state/cf_lock.lmdb"); } diff --git a/promise-types/iptables/test.cf b/promise-types/iptables/test.cf index 3a7104f7..78414101 100755 --- a/promise-types/iptables/test.cf +++ b/promise-types/iptables/test.cf @@ -1,49 +1,44 @@ promise agent iptables { - path => "$(sys.workdir)/modules/promises/iptables.py"; - interpreter => "/usr/bin/python3"; + path => "$(sys.workdir)/modules/promises/iptables.py"; + interpreter => "/usr/bin/python3"; } body common control { - bundlesequence => { - aggressive_policy, - input_flushed, - all_flushed, - }; + bundlesequence => { aggressive_policy, input_flushed, all_flushed }; } bundle agent aggressive_policy { iptables: - "aggressive_policy" - command => "policy", - chain => "INPUT", - target => "DROP"; + "aggressive_policy" + command => "policy", + chain => "INPUT", + target => "DROP"; reports: - "--- ${this.bundle} ---"; + "--- ${this.bundle} ---"; } bundle agent input_flushed { iptables: - "input_flushed" - command => "flush", - chain => "INPUT"; + "input_flushed" + command => "flush", + chain => "INPUT"; reports: - "--- ${this.bundle} ---"; + "--- ${this.bundle} ---"; } bundle agent all_flushed { iptables: - "all_flushed" - command => "flush", - chain => "ALL"; + "all_flushed" + command => "flush", + chain => "ALL"; reports: - "--- ${this.bundle} ---"; + "--- ${this.bundle} ---"; } - diff --git a/promise-types/json/example.cf b/promise-types/json/example.cf index 91d3a84f..12a52302 100644 --- a/promise-types/json/example.cf +++ b/promise-types/json/example.cf @@ -8,6 +8,5 @@ promise agent json bundle agent main { json: - "/tmp/myusers.json:name" - string => "John" + "/tmp/myusers.json:name" string => "John"; } diff --git a/promise-types/json/test.cf b/promise-types/json/test.cf index ecb5fcac..c5c9c346 100644 --- a/promise-types/json/test.cf +++ b/promise-types/json/test.cf @@ -2,32 +2,34 @@ body common control { inputs => { "$(sys.libdir)/stdlib.cf" }; version => "1.0"; - bundlesequence => { "init", "test", "check", "cleanup"}; + bundlesequence => { "init", "test", "check", "cleanup" }; } - ####################################################### - bundle agent init { vars: "to_overwrite" - data => readjson("$(this.promise_dirname)/tests/to_overwrite.start.json", 100k); - "to_modify" - data => readjson("$(this.promise_dirname)/tests/to_modify.start.json", 100k); + data => readjson( + "$(this.promise_dirname)/tests/to_overwrite.start.json", 100k + ); + + "to_modify" + data => readjson( + "$(this.promise_dirname)/tests/to_modify.start.json", 100k + ); files: - "$(this.promise_dirname)/tests/to_overwrite.json" - create => "true", - content => "$(to_overwrite)"; - "$(this.promise_dirname)/tests/to_modify.json" - create => "true", - content => "$(to_modify)"; - "$(this.promise_dirname)/tests/to_append_1.json" - create => "true"; -} + "$(this.promise_dirname)/tests/to_overwrite.json" + create => "true", + content => "$(to_overwrite)"; -####################################################### + "$(this.promise_dirname)/tests/to_modify.json" + create => "true", + content => "$(to_modify)"; + "$(this.promise_dirname)/tests/to_append_1.json" create => "true"; +} +####################################################### promise agent json { path => "$(this.promise_dirname)/json_promise_type.py"; @@ -37,125 +39,121 @@ promise agent json bundle agent test { vars: - "objects" - data => '{ "bar": [1, 2, 3] }'; - "int_arrays" - data => '[1,2,3]'; - "arrays" - ilist => { "1", "2" }; # slist == rlist == ilist -> lists of string - "numbers" - int => "1"; - "strings" - string => "hello"; - "bools" - string => "true"; - "nulls" - string => "null"; + "objects" data => '{ "bar": [1, 2, 3] }'; + "int_arrays" data => '[1,2,3]'; + "arrays" ilist => { "1", "2" }; + # slist == rlist == ilist -> lists of string + "numbers" int => "1"; + "strings" string => "hello"; + "bools" string => "true"; + "nulls" string => "null"; json: - "$(this.promise_dirname)/tests/to_overwrite.json" - object => "@(objects)"; - - "$(this.promise_dirname)/tests/to_modify.json:Hello" - string => "$(strings)"; + "$(this.promise_dirname)/tests/to_overwrite.json" object => "@(objects)"; + "$(this.promise_dirname)/tests/to_modify.json:Hello" string => "$(strings)"; + "$(this.promise_dirname)/tests/to_append_1.json:a" object => "@(objects)"; - "$(this.promise_dirname)/tests/to_append_1.json:a" - object => "@(objects)"; "$(this.promise_dirname)/tests/to_append_1.json:b" object => "@(int_arrays)"; - "$(this.promise_dirname)/tests/to_append_1.json:c" - array => "@(int_arrays)"; - "$(this.promise_dirname)/tests/to_append_1.json:d" - string => "$(strings)"; - "$(this.promise_dirname)/tests/to_append_1.json:e" - number => "$(numbers)"; - "$(this.promise_dirname)/tests/to_append_1.json:f" - primitive => "$(bools)"; - "$(this.promise_dirname)/tests/to_append_1.json:g" - primitive => "$(nulls)"; + + "$(this.promise_dirname)/tests/to_append_1.json:c" array => "@(int_arrays)"; + "$(this.promise_dirname)/tests/to_append_1.json:d" string => "$(strings)"; + "$(this.promise_dirname)/tests/to_append_1.json:e" number => "$(numbers)"; + "$(this.promise_dirname)/tests/to_append_1.json:f" primitive => "$(bools)"; + "$(this.promise_dirname)/tests/to_append_1.json:g" primitive => "$(nulls)"; "$(this.promise_dirname)/tests/to_append_2.json:a" object => '{ "bar": [1, 2, 3] }'; - "$(this.promise_dirname)/tests/to_append_2.json:b" - object => '[1,2,3]'; - "$(this.promise_dirname)/tests/to_append_2.json:c" - array => '[1,2,3]'; - "$(this.promise_dirname)/tests/to_append_2.json:d" - string => "hello"; - "$(this.promise_dirname)/tests/to_append_2.json:e" - number => "1"; - "$(this.promise_dirname)/tests/to_append_2.json:f" - primitive => "true"; - "$(this.promise_dirname)/tests/to_append_2.json:g" - primitive => "null"; + "$(this.promise_dirname)/tests/to_append_2.json:b" object => '[1,2,3]'; + "$(this.promise_dirname)/tests/to_append_2.json:c" array => '[1,2,3]'; + "$(this.promise_dirname)/tests/to_append_2.json:d" string => "hello"; + "$(this.promise_dirname)/tests/to_append_2.json:e" number => "1"; + "$(this.promise_dirname)/tests/to_append_2.json:f" primitive => "true"; + "$(this.promise_dirname)/tests/to_append_2.json:g" primitive => "null"; } - ####################################################### - bundle agent check { vars: "to_overwrite_content" - data => readjson("$(this.promise_dirname)/tests/to_overwrite.json", 100k); + data => readjson( + "$(this.promise_dirname)/tests/to_overwrite.json", 100k + ); + "to_modify_content" data => readjson("$(this.promise_dirname)/tests/to_modify.json", 100k); + "to_append_1_content" data => readjson("$(this.promise_dirname)/tests/to_append_1.json", 100k); + "to_append_2_content" data => readjson("$(this.promise_dirname)/tests/to_append_2.json", 100k); "to_overwrite_content_true" - data => readjson("$(this.promise_dirname)/tests/to_overwrite.expected.json", 100k); + data => readjson( + "$(this.promise_dirname)/tests/to_overwrite.expected.json", 100k + ); + "to_modify_content_true" - data => readjson("$(this.promise_dirname)/tests/to_modify.expected.json", 100k); + data => readjson( + "$(this.promise_dirname)/tests/to_modify.expected.json", 100k + ); + "to_append_content_true" - data => readjson("$(this.promise_dirname)/tests/to_append.expected.json", 100k); + data => readjson( + "$(this.promise_dirname)/tests/to_append.expected.json", 100k + ); - "to_overwrite_content_indices" - slist => getindices("to_overwrite_content"); - "to_modify_content_indices" - slist => getindices("to_modify_content"); - "to_append_1_content_indices" - slist => getindices("to_append_1_content"); - "to_append_2_content_indices" - slist => getindices("to_append_2_content"); + "to_overwrite_content_indices" slist => getindices("to_overwrite_content"); + "to_modify_content_indices" slist => getindices("to_modify_content"); + "to_append_1_content_indices" slist => getindices("to_append_1_content"); + "to_append_2_content_indices" slist => getindices("to_append_2_content"); "to_overwrite_content_true_indices" slist => getindices("to_overwrite_content_true"); + "to_modify_content_true_indices" slist => getindices("to_modify_content_true"); + "to_append_content_true_indices" slist => getindices("to_append_content_true"); classes: "ok" - expression => and ( - strcmp("$(to_overwrite_content[$(to_overwrite_content_indices)])", "$(to_overwrite_content_true[$(to_overwrite_content_true_indices)])"), - strcmp("$(to_modify_content[$(to_modify_content_indices)])", "$(to_modify_content_true[$(to_modify_content_true_indices)])"), - strcmp("$(to_append_1_content[$(to_append_1_content_indices)])", "$(to_append_2_content[$(to_append_2_content_indices)])"), - strcmp("$(to_append_1_content[$(to_append_1_content_indices)])", "$(to_append_content_true[$(to_append_content_true_indices)])") + expression => and( + strcmp( + "$(to_overwrite_content[$(to_overwrite_content_indices)])", + "$(to_overwrite_content_true[$(to_overwrite_content_true_indices)])" + ), + strcmp( + "$(to_modify_content[$(to_modify_content_indices)])", + "$(to_modify_content_true[$(to_modify_content_true_indices)])" + ), + strcmp( + "$(to_append_1_content[$(to_append_1_content_indices)])", + "$(to_append_2_content[$(to_append_2_content_indices)])" + ), + strcmp( + "$(to_append_1_content[$(to_append_1_content_indices)])", + "$(to_append_content_true[$(to_append_content_true_indices)])" + ) ); reports: ok:: "$(this.promise_filename) Pass"; + !ok:: "$(this.promise_filename) FAIL"; } - # ####################################################### - bundle agent cleanup { files: - "$(this.promise_dirname)/tests/to_overwrite.json" - delete => tidy; - "$(this.promise_dirname)/tests/to_modify.json" - delete => tidy; - "$(this.promise_dirname)/tests/to_append_1.json" - delete => tidy; - "$(this.promise_dirname)/tests/to_append_2.json" - delete => tidy; + "$(this.promise_dirname)/tests/to_overwrite.json" delete => tidy; + "$(this.promise_dirname)/tests/to_modify.json" delete => tidy; + "$(this.promise_dirname)/tests/to_append_1.json" delete => tidy; + "$(this.promise_dirname)/tests/to_append_2.json" delete => tidy; } diff --git a/promise-types/json/tests/to_modify.start.json b/promise-types/json/tests/to_modify.start.json index 02bb47e1..c7acc90c 100644 --- a/promise-types/json/tests/to_modify.start.json +++ b/promise-types/json/tests/to_modify.start.json @@ -1,3 +1,3 @@ { - "Hello": "World" + "Hello": "World" } diff --git a/promise-types/json/tests/to_overwrite.start.json b/promise-types/json/tests/to_overwrite.start.json index 02bb47e1..c7acc90c 100644 --- a/promise-types/json/tests/to_overwrite.start.json +++ b/promise-types/json/tests/to_overwrite.start.json @@ -1,3 +1,3 @@ { - "Hello": "World" + "Hello": "World" } diff --git a/promise-types/symlinks/example.cf b/promise-types/symlinks/example.cf index 0fafc2be..c2dc4505 100644 --- a/promise-types/symlinks/example.cf +++ b/promise-types/symlinks/example.cf @@ -8,8 +8,6 @@ promise agent symlinks bundle agent main { symlinks: - "/tmp/myfilelink" - file => "tmp/myfile"; - "/tmp/mydirlink" - directory => "tmp/mydirectory"; + "/tmp/myfilelink" file => "tmp/myfile"; + "/tmp/mydirlink" directory => "tmp/mydirectory"; } diff --git a/promise-types/symlinks/test.cf b/promise-types/symlinks/test.cf index 468ac0d9..828ab69a 100644 --- a/promise-types/symlinks/test.cf +++ b/promise-types/symlinks/test.cf @@ -2,28 +2,19 @@ body common control { inputs => { "$(sys.libdir)/stdlib.cf" }; version => "1.0"; - bundlesequence => { "init", "test", "check", "cleanup"}; + bundlesequence => { "init", "test", "check", "cleanup" }; } - ####################################################### - bundle agent init { files: - "/tmp/my-file" - create => "true"; - "/tmp/my-dir/." - create => "true"; - "/tmp/other-dir/." - create => "true"; - "/tmp/replaced-link" - link_from => ln_s("/tmp/other-dir"); - "/tmp/already-existing-link" - link_from => ln_s("/tmp/other-dir"); + "/tmp/my-file" create => "true"; + "/tmp/my-dir/." create => "true"; + "/tmp/other-dir/." create => "true"; + "/tmp/replaced-link" link_from => ln_s("/tmp/other-dir"); + "/tmp/already-existing-link" link_from => ln_s("/tmp/other-dir"); } - ####################################################### - promise agent symlinks { path => "$(this.promise_dirname)/symlinks.py"; @@ -46,36 +37,33 @@ bundle agent test "/tmp/file-link" file => "/tmp/my-file", classes => outcome("created_file"); + "/tmp/dir-link" directory => "/tmp/my-dir", classes => outcome("created_dir"); + "/tmp/replaced-link" directory => "/tmp/my-dir", classes => outcome("corrected"); + "/tmp/already-existing-link" directory => "/tmp/other-dir", classes => outcome("didnothing"); - } - ####################################################### - bundle agent check { - vars: - "my_file_stat" - string => filestat("/tmp/file-link", "linktarget"); - "my_dir_stat" - string => filestat("/tmp/dir-link", "linktarget"); - "replaced_link_stat" - string => filestat("/tmp/replaced-link", "linktarget"); + "my_file_stat" string => filestat("/tmp/file-link", "linktarget"); + "my_dir_stat" string => filestat("/tmp/dir-link", "linktarget"); + "replaced_link_stat" string => filestat("/tmp/replaced-link", "linktarget"); + "already_existing_link_stat" string => filestat("/tmp/already-existing-link", "linktarget"); classes: "ok" - expression => and ( + expression => and( strcmp("$(my_file_stat)", "/tmp/my-file"), strcmp("$(my_dir_stat)", "/tmp/my-dir"), strcmp("$(replaced_link_stat)", "/tmp/my-dir"), @@ -89,28 +77,19 @@ bundle agent check reports: ok:: "$(this.promise_filename) Pass"; + !ok:: "$(this.promise_filename) FAIL"; - } - # ####################################################### - bundle agent cleanup { files: - "/tmp/file-link" - delete => tidy; - "/tmp/dir-link" - delete => tidy; - "/tmp/my-file" - delete => tidy; - "/tmp/my-dir/." - delete => tidy; - "/tmp/other-dir/." - delete => tidy; - "/tmp/replaced-link" - delete => tidy; - "/tmp/already-existing-link" - delete => tidy; + "/tmp/file-link" delete => tidy; + "/tmp/dir-link" delete => tidy; + "/tmp/my-file" delete => tidy; + "/tmp/my-dir/." delete => tidy; + "/tmp/other-dir/." delete => tidy; + "/tmp/replaced-link" delete => tidy; + "/tmp/already-existing-link" delete => tidy; } diff --git a/promise-types/systemd/example.cf b/promise-types/systemd/example.cf index da3c3da6..1a0595bc 100644 --- a/promise-types/systemd/example.cf +++ b/promise-types/systemd/example.cf @@ -14,7 +14,7 @@ bundle agent main enabled => "true", state => "restarted", unit_description => "my sample service", - service_exec_start => {"/usr/bin/sleep 86400"}, + service_exec_start => { "/usr/bin/sleep 86400" }, service_type => "simple", - install_wanted_by => {"multi-user.target"}; + install_wanted_by => { "multi-user.target" }; } diff --git a/promise-types/systemd/test.cf b/promise-types/systemd/test.cf index a5b21fc1..5a05975c 100644 --- a/promise-types/systemd/test.cf +++ b/promise-types/systemd/test.cf @@ -2,20 +2,15 @@ body common control { version => "1.0"; } - ####################################################### - bundle agent init { - } - ####################################################### - promise agent systemd { - path => "$(this.promise_dirname)/systemd.py"; - interpreter => "/usr/bin/python3"; + path => "$(this.promise_dirname)/systemd.py"; + interpreter => "/usr/bin/python3"; } bundle agent test @@ -31,40 +26,36 @@ bundle agent test enabled => "true", state => "restarted", unit_description => "my sample service", - service_exec_start => {"/usr/bin/sleep 86400"}, + service_exec_start => { "/usr/bin/sleep 86400" }, service_type => "simple", - install_wanted_by => {"multi-user.target"}; + install_wanted_by => { "multi-user.target" }; classes: - "promise_repaired" - expression => canonify("sample_restarted"), - scope => "namespace"; + "promise_repaired" + expression => canonify("sample_restarted"), + scope => "namespace"; } - ####################################################### - bundle agent check { classes: - "file_ok" - if => fileexists("/lib/systemd/system/sample.service"); - - "ok" expression => "file_ok.promise_repaired"; + "file_ok" if => fileexists("/lib/systemd/system/sample.service"); + "ok" expression => "file_ok.promise_repaired"; reports: DEBUG.file_ok:: "file_ok"; + DEBUG.promise_repaired:: "promise_repaired"; ok:: "$(this.promise_filename) Pass"; + !ok:: "$(this.promise_filename) FAIL"; } - ####################################################### - bundle agent cleanup { systemd: diff --git a/pyrightconfig.json b/pyrightconfig.json index 3ab68711..c9566077 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,4 +1,4 @@ { - "reportMissingImports": "none", - "extraPaths": ["libraries/python"] + "reportMissingImports": "none", + "extraPaths": ["libraries/python"] } diff --git a/security/conditional-installer/main.cf b/security/conditional-installer/main.cf index 8493954a..996f7e1b 100644 --- a/security/conditional-installer/main.cf +++ b/security/conditional-installer/main.cf @@ -7,7 +7,9 @@ bundle agent main { classes: "enabled" - if => and(isvariable(packages_to_install), isvariable(packages_to_uninstall)); + if => and( + isvariable(packages_to_install), isvariable(packages_to_uninstall) + ); vars: # Mock data, should be based on input: @@ -21,14 +23,16 @@ bundle agent main # ]'; enabled:: # Determine packages to install: - "i" - slist => getindices(packages_to_install); + "i" slist => getindices(packages_to_install); + "_packages_to_install_lists[$(i)]" - slist => string_split("$(packages_to_install[$(i)][packages])", ",", 100), + slist => string_split( + "$(packages_to_install[$(i)][packages])", ",", 100 + ), if => "$(packages_to_install[$(i)][condition])", comment => "$(packages_to_install[$(i)][why])"; - "_packages_to_install" - slist => getvalues(_packages_to_install_lists); + + "_packages_to_install" slist => getvalues(_packages_to_install_lists); # Determine packages to uninstall: "_packages_to_uninstall_unfiltered" @@ -36,16 +40,13 @@ bundle agent main "_packages_to_uninstall" slist => difference( - _packages_to_uninstall_unfiltered, - _packages_to_install + _packages_to_uninstall_unfiltered, _packages_to_install ); packages: enabled:: - "$(_packages_to_uninstall)" - policy => "absent"; - "$(_packages_to_install)" - policy => "present"; + "$(_packages_to_uninstall)" policy => "absent"; + "$(_packages_to_install)" policy => "present"; } body file control diff --git a/security/cron-access/cron-access.cf b/security/cron-access/cron-access.cf index 4e8e3219..fc2a2b63 100644 --- a/security/cron-access/cron-access.cf +++ b/security/cron-access/cron-access.cf @@ -21,9 +21,11 @@ bundle agent cron_access "$(directories)" perms => mog("700", "root", "root"), if => fileexists("$(this.promiser)"); + "/etc/cron.allow" perms => mog("640", "root", "root"), if => fileexists("$(this.promiser)"); + "/etc/crontab" perms => mog("600", "root", "root"), if => fileexists("$(this.promiser)"); diff --git a/security/delete-home-dotrhosts/policy/main.cf b/security/delete-home-dotrhosts/policy/main.cf index d0500a50..654c9118 100644 --- a/security/delete-home-dotrhosts/policy/main.cf +++ b/security/delete-home-dotrhosts/policy/main.cf @@ -1,6 +1,6 @@ body file control { - namespace => "delete_home_dotrhosts"; + namespace => "delete_home_dotrhosts"; } bundle agent main @@ -9,7 +9,6 @@ bundle agent main # @inventory ~/.rhosts list of ~.rhosts files found. # @inventory ~/.rhosts Exception The string defined that records the details about why a host has an exception for this policy. { - classes: "dotrhosts_management_disabled" or => { @@ -21,30 +20,29 @@ bundle agent main vars: dotrhosts_management_disabled:: - "exception" # Fill in exception reason if missing (class was used) + "exception" + # Fill in exception reason if missing (class was used) string => "Unknown reason", if => not(isvariable("delete_home_dotrhosts:main.exception")); !default:windows:: - # /home and /root are common default home dir roots, but it's not uncommon # to have other home dir roots - "home_dir_roots" slist => { "/home" }, meta => { "noreport" }, - if => not( isvariable( "home_dir_roots" )); + if => not(isvariable("home_dir_roots")); - # Find .rhosts files in the root of peoples home directories. + # Find .rhosts files in the root of peoples home directories. "_found[$(home_dir_roots)]" - slist => findfiles( "$(home_dir_roots)/*/.rhosts" ), - if => isdir( $(home_dir_roots) ); + slist => findfiles("$(home_dir_roots)/*/.rhosts"), + if => isdir($(home_dir_roots)); + "_found[/root]" slist => { "/root/.rhosts" }, - if => fileexists( "/root/.rhosts" ); + if => fileexists("/root/.rhosts"); - "_found" - slist => getvalues( _found ); + "_found" slist => getvalues(_found); # We inventory the found files so that we easily know where they are. # This way, even hosts with exceptions will report the inventory, but not @@ -60,16 +58,15 @@ bundle agent main "inv_exception" string => "$(exception)", - if => isvariable( "exception" ), + if => isvariable("exception"), meta => { "inventory", "attribute_name=~/.rhosts exception reason" }; files: !dotrhosts_management_disabled.!default:windows:: - "$(_found)" - delete => default:tidy; + "$(_found)" delete => default:tidy; reports: dotrhosts_management_disabled.(default:inform_mode|default:verbose_mode):: "Found $(with), but not removing because of exception: $(exception)" - with => join( ", ", _found ); + with => join(", ", _found); } diff --git a/security/delete-home-dotshosts/policy/main.cf b/security/delete-home-dotshosts/policy/main.cf index 1a836304..489caf56 100644 --- a/security/delete-home-dotshosts/policy/main.cf +++ b/security/delete-home-dotshosts/policy/main.cf @@ -1,6 +1,6 @@ body file control { - namespace => "delete_home_dotshosts"; + namespace => "delete_home_dotshosts"; } bundle agent main @@ -9,7 +9,6 @@ bundle agent main # @inventory ~/.shosts list of ~.shosts files found. # @inventory ~/.shosts Exception The string defined that records the details about why a host has an exception for this policy. { - classes: "dotshosts_management_disabled" or => { @@ -21,30 +20,29 @@ bundle agent main vars: dotshosts_management_disabled:: - "exception" # Fill in exception reason if missing (class was used) + "exception" + # Fill in exception reason if missing (class was used) string => "Unknown reason", if => not(isvariable("delete_home_dotshosts:main.exception")); !default:windows:: - # /home and /root are common default home dir roots, but it's not uncommon # to have other home dir roots - "home_dir_roots" slist => { "/home" }, meta => { "noreport" }, - if => not( isvariable( "home_dir_roots" )); + if => not(isvariable("home_dir_roots")); - # Find .shosts files in the root of peoples home directories. + # Find .shosts files in the root of peoples home directories. "_found[$(home_dir_roots)]" - slist => findfiles( "$(home_dir_roots)/*/.shosts" ), - if => isdir( $(home_dir_roots) ); + slist => findfiles("$(home_dir_roots)/*/.shosts"), + if => isdir($(home_dir_roots)); + "_found[/root]" slist => { "/root/.shosts" }, - if => fileexists( "/root/.shosts" ); + if => fileexists("/root/.shosts"); - "_found" - slist => getvalues( _found ); + "_found" slist => getvalues(_found); # We inventory the found files so that we easily know where they are. # This way, even hosts with exceptions will report the inventory, but not @@ -60,16 +58,15 @@ bundle agent main "inv_exception" string => "$(exception)", - if => isvariable( "exception" ), + if => isvariable("exception"), meta => { "inventory", "attribute_name=~/.shosts exception reason" }; files: !dotshosts_management_disabled.!default:windows:: - "$(_found)" - delete => default:tidy; + "$(_found)" delete => default:tidy; reports: dotshosts_management_disabled.(default:inform_mode|default:verbose_mode):: "Found $(with), but not removing because of exception: $(exception)" - with => join( ", ", _found ); + with => join(", ", _found); } diff --git a/security/dirtyfrag/dirtyfrag.cf b/security/dirtyfrag/dirtyfrag.cf index 80bbdf14..2fc6b108 100644 --- a/security/dirtyfrag/dirtyfrag.cf +++ b/security/dirtyfrag/dirtyfrag.cf @@ -21,8 +21,7 @@ bundle agent main # Minimum patched kernel package versions per distro live in # patched-kernels.json next to this policy. The data file is # walked here; comparison happens in the classes:: block below. - "_data_file" - string => "$(this.promise_dirname)/patched-kernels.json"; + "_data_file" string => "$(this.promise_dirname)/patched-kernels.json"; "_data" data => readjson("${_data_file}"), @@ -47,7 +46,9 @@ bundle agent main string => "${_entries_idx}", if => and( regcmp("${_data[entries][${_entries_idx}][id_match]}", "${_os_id}"), - regcmp("${_data[entries][${_entries_idx}][version_match]}", "${_os_ver}") + regcmp( + "${_data[entries][${_entries_idx}][version_match]}", "${_os_ver}" + ) ); "_esp_patched_ver" @@ -80,8 +81,8 @@ bundle agent main if => not(regcmp("debian|ubuntu", "$(default:sys.os_release[ID])")); # --- Mitigation-conf paths and contents ----------------------- - "_esp_conf_path" string => "/etc/modprobe.d/dirtyfrag-esp.conf"; - "_rxrpc_conf_path" string => "/etc/modprobe.d/dirtyfrag-rxrpc.conf"; + "_esp_conf_path" string => "/etc/modprobe.d/dirtyfrag-esp.conf"; + "_rxrpc_conf_path" string => "/etc/modprobe.d/dirtyfrag-rxrpc.conf"; "_userns_conf_path" string => "/etc/sysctl.d/dirtyfrag-userns.conf"; "_esp_conf_content" @@ -135,15 +136,16 @@ bundle agent main # filter(".+", ...) drops the empties; join glues with ", ". "_esp_loaded_list" slist => { - ifelse("_esp4_loaded", "esp4", ""), - ifelse("_esp6_loaded", "esp6", ""), - ifelse("_ipcomp_loaded", "ipcomp", ""), + ifelse("_esp4_loaded", "esp4", ""), + ifelse("_esp6_loaded", "esp6", ""), + ifelse("_ipcomp_loaded", "ipcomp", ""), ifelse("_ipcomp6_loaded", "ipcomp6", ""), }; "_esp_loaded_names" - string => join(", ", - filter(".+", "_esp_loaded_list", "false", "false", "10")); + string => join( + ", ", filter(".+", "_esp_loaded_list", "false", "false", "10") + ); # --- Status strings ------------------------------------------- "_esp_status" @@ -180,20 +182,29 @@ bundle agent main # the long _esp_status string. ifelse takes first-match order. "_esp_mitigation_method" string => ifelse( - "dirtyfrag_esp_needs_mitigation", "none", - "_esp_kernel_patched", "kernel-patch", - "_esp_admin_patched", "admin-override", - "_esp_conf_exists", "modprobe", - "_userns_conf_exists", "userns", + "dirtyfrag_esp_needs_mitigation", + "none", + "_esp_kernel_patched", + "kernel-patch", + "_esp_admin_patched", + "admin-override", + "_esp_conf_exists", + "modprobe", + "_userns_conf_exists", + "userns", "not-applicable" ); "_rxrpc_mitigation_method" string => ifelse( - "dirtyfrag_rxrpc_needs_mitigation", "none", - "_rxrpc_kernel_patched", "kernel-patch", - "_rxrpc_admin_patched", "admin-override", - "_rxrpc_conf_exists", "modprobe", + "dirtyfrag_rxrpc_needs_mitigation", + "none", + "_rxrpc_kernel_patched", + "kernel-patch", + "_rxrpc_admin_patched", + "admin-override", + "_rxrpc_conf_exists", + "modprobe", "not-applicable" ); @@ -234,12 +245,12 @@ bundle agent main classes: # --- CMDB toggles --------------------------------------------- - "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); - "_mitigate_rxrpc" expression => strcmp("true", "$(mitigate_rxrpc)"); + "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); + "_mitigate_rxrpc" expression => strcmp("true", "$(mitigate_rxrpc)"); "_mitigate_userns" expression => strcmp("true", "$(mitigate_userns)"); # --- Admin override: manually declare host as patched --------- - "_esp_admin_patched" expression => strcmp("true", "$(esp_patched)"); + "_esp_admin_patched" expression => strcmp("true", "$(esp_patched)"); "_rxrpc_admin_patched" expression => strcmp("true", "$(rxrpc_patched)"); # --- Kernel-patch comparison ---------------------------------- @@ -251,18 +262,14 @@ bundle agent main # (tempfile + commands:, or a bundled wrapper script) are heavier # for no real safety gain on a static, internally-built string. # See lessons-learned/version-compare-shell-exception.md. - "_patch_data_matched" - expression => isvariable("_matched_idx"); + "_patch_data_matched" expression => isvariable("_matched_idx"); "_esp_kernel_patched" expression => returnszero( "printf '%s\\n%s\\n' '$(_esp_patched_ver)' '$(_kernel_pkg_ver)' | sort -V -C", "useshell" ), - if => and( - isvariable("_esp_patched_ver"), - isvariable("_kernel_pkg_ver") - ); + if => and(isvariable("_esp_patched_ver"), isvariable("_kernel_pkg_ver")); "_rxrpc_kernel_patched" expression => returnszero( @@ -270,22 +277,20 @@ bundle agent main "useshell" ), if => and( - isvariable("_rxrpc_patched_ver"), - isvariable("_kernel_pkg_ver") + isvariable("_rxrpc_patched_ver"), isvariable("_kernel_pkg_ver") ); # --- Unprivileged user namespace enabled? --------------------- "_userns_disabled" expression => strcmp("0", "${_max_userns}"); - "_userns_enabled" not => "_userns_disabled"; + "_userns_enabled" not => "_userns_disabled"; # --- xfrm-ESP/IPComp modules ---------------------------------- # On-disk presence collapses to: did findfiles return anything? # some(".+", slist) is true when the list has any non-empty element. "_esp_files_present" expression => some(".+", "_esp_module_files"); - - "_esp4_loaded" expression => isdir("/sys/module/esp4"); - "_esp6_loaded" expression => isdir("/sys/module/esp6"); - "_ipcomp_loaded" expression => isdir("/sys/module/ipcomp"); + "_esp4_loaded" expression => isdir("/sys/module/esp4"); + "_esp6_loaded" expression => isdir("/sys/module/esp6"); + "_ipcomp_loaded" expression => isdir("/sys/module/ipcomp"); "_ipcomp6_loaded" expression => isdir("/sys/module/ipcomp6"); "_esp_any_loaded" @@ -293,19 +298,16 @@ bundle agent main "_esp4_loaded", "_esp6_loaded", "_ipcomp_loaded", "_ipcomp6_loaded" }; - "dirtyfrag_esp_present" - or => { "_esp_files_present", "_esp_any_loaded" }; + "dirtyfrag_esp_present" or => { "_esp_files_present", "_esp_any_loaded" }; # --- RxRPC module --------------------------------------------- "_rxrpc_files_present" expression => some(".+", "_rxrpc_module_files"); - "_rxrpc_loaded" expression => isdir("/sys/module/rxrpc"); - - "dirtyfrag_rxrpc_present" - or => { "_rxrpc_files_present", "_rxrpc_loaded" }; + "_rxrpc_loaded" expression => isdir("/sys/module/rxrpc"); + "dirtyfrag_rxrpc_present" or => { "_rxrpc_files_present", "_rxrpc_loaded" }; # --- Mitigation conf files in place? -------------------------- - "_esp_conf_exists" expression => fileexists("${_esp_conf_path}"); - "_rxrpc_conf_exists" expression => fileexists("${_rxrpc_conf_path}"); + "_esp_conf_exists" expression => fileexists("${_esp_conf_path}"); + "_rxrpc_conf_exists" expression => fileexists("${_rxrpc_conf_path}"); "_userns_conf_exists" expression => fileexists("${_userns_conf_path}"); # ESP is mitigated by modprobe blacklist, patched kernel, or @@ -313,18 +315,14 @@ bundle agent main # vulnerability condition (see dirtyfrag_esp_needs_mitigation). "_esp_mitigated" or => { - "_esp_conf_exists", - "_esp_kernel_patched", - "_esp_admin_patched", + "_esp_conf_exists", "_esp_kernel_patched", "_esp_admin_patched", }; # RxRPC is mitigated by the modprobe blacklist, patched kernel, # or admin override. "_rxrpc_mitigated" or => { - "_rxrpc_conf_exists", - "_rxrpc_kernel_patched", - "_rxrpc_admin_patched", + "_rxrpc_conf_exists", "_rxrpc_kernel_patched", "_rxrpc_admin_patched", }; # --- Per-CVE vulnerability checks ----------------------------- @@ -334,13 +332,15 @@ bundle agent main # confs); it's tracked here so disabling userns blocks the # exploit vector directly. "dirtyfrag_esp_needs_mitigation" - and => { "dirtyfrag_esp_present", "!_esp_mitigated", "!_userns_conf_exists" }, - meta => { "report" }, + and => { + "dirtyfrag_esp_present", "!_esp_mitigated", "!_userns_conf_exists" + }, + meta => { "report" }, scope => "namespace"; "dirtyfrag_rxrpc_needs_mitigation" - and => { "dirtyfrag_rxrpc_present", "!_rxrpc_mitigated" }, - meta => { "report" }, + and => { "dirtyfrag_rxrpc_present", "!_rxrpc_mitigated" }, + meta => { "report" }, scope => "namespace"; # --- Composite roll-ups for targeting and querying ------------ @@ -348,18 +348,20 @@ bundle agent main # don't clutter the default Mission Portal inventory columns. Use # for class filters in reports, alerts, and CMDB-style targeting. "dirtyfrag_vulnerable" - or => { "dirtyfrag_esp_needs_mitigation", "dirtyfrag_rxrpc_needs_mitigation" }, - meta => { "report" }, + or => { + "dirtyfrag_esp_needs_mitigation", "dirtyfrag_rxrpc_needs_mitigation" + }, + meta => { "report" }, scope => "namespace"; "dirtyfrag_esp_mitigated" - or => { "_esp_mitigated", "_userns_conf_exists" }, - meta => { "report" }, + or => { "_esp_mitigated", "_userns_conf_exists" }, + meta => { "report" }, scope => "namespace"; "dirtyfrag_rxrpc_mitigated" - or => { "_rxrpc_mitigated" }, - meta => { "report" }, + or => { "_rxrpc_mitigated" }, + meta => { "report" }, scope => "namespace"; files: @@ -368,25 +370,24 @@ bundle agent main # promise detail page so operators can search "CVE-2026-43284" # and find every policy artifact addressing it. # handle gives each promise a stable, queryable name in reports. - _mitigate_esp:: "${_esp_conf_path}" -> { "CVE-2026-43284" } - handle => "dirtyfrag_esp_modprobe_blacklist", - create => "true", + handle => "dirtyfrag_esp_modprobe_blacklist", + create => "true", content => "${_esp_conf_content}", comment => "Blacklist xfrm-ESP/IPComp modules to mitigate Dirty Frag CVE-2026-43284"; _mitigate_rxrpc:: "${_rxrpc_conf_path}" -> { "CVE-2026-43500" } - handle => "dirtyfrag_rxrpc_modprobe_blacklist", - create => "true", + handle => "dirtyfrag_rxrpc_modprobe_blacklist", + create => "true", content => "${_rxrpc_conf_content}", comment => "Blacklist RxRPC module to mitigate Dirty Frag CVE-2026-43500"; _mitigate_userns:: "${_userns_conf_path}" -> { "CVE-2026-43284" } - handle => "dirtyfrag_userns_sysctl_conf", - create => "true", + handle => "dirtyfrag_userns_sysctl_conf", + create => "true", content => "${_userns_conf_content}", comment => "Disable unprivileged user namespaces to block CVE-2026-43284 ESP exploit path"; @@ -398,16 +399,15 @@ bundle agent main # modprobe -r (vs rmmod) tolerates "some of the named modules # aren't loaded", which simplifies the per-module gating to a # single class. - _mitigate_esp._esp_any_loaded:: "/sbin/modprobe" -> { "CVE-2026-43284" } - handle => "dirtyfrag_esp_modprobe_unload", + handle => "dirtyfrag_esp_modprobe_unload", arglist => { "-r", "esp4", "esp6", "ipcomp", "ipcomp6" }, comment => "Unload ESP/IPComp modules while ESP mitigation is active"; _mitigate_rxrpc._rxrpc_loaded:: "/sbin/modprobe" -> { "CVE-2026-43500" } - handle => "dirtyfrag_rxrpc_modprobe_unload", + handle => "dirtyfrag_rxrpc_modprobe_unload", arglist => { "-r", "rxrpc" }, comment => "Unload RxRPC module while RxRPC mitigation is active"; @@ -417,7 +417,7 @@ bundle agent main # boot). _mitigate_userns._userns_enabled:: "/sbin/sysctl" -> { "CVE-2026-43284" } - handle => "dirtyfrag_userns_sysctl_reapply", + handle => "dirtyfrag_userns_sysctl_reapply", arglist => { "--system" }, comment => "Re-apply user.max_user_namespaces=0 while userns is enabled"; @@ -432,7 +432,6 @@ bundle agent main "Dirty Frag CVE-2026-43500: kernel $(_kernel_pkg_ver) >= $(_rxrpc_patched_ver) (PATCHED)"; inform_mode:: - "Dirty Frag CVE-2026-43284 (xfrm-ESP/IPComp): $(_esp_status)"; "Dirty Frag CVE-2026-43500 (RxRPC): $(_rxrpc_status)"; } diff --git a/security/enable-aslr/enable-aslr.cf b/security/enable-aslr/enable-aslr.cf index ef7a84a8..7acdbb3f 100644 --- a/security/enable-aslr/enable-aslr.cf +++ b/security/enable-aslr/enable-aslr.cf @@ -13,21 +13,23 @@ bundle agent enable_aslr "aslr_mode[0]" string => "Disabled"; "aslr_mode[1]" string => "Conservative Randomization"; "aslr_mode[2]" string => "Full Randomization"; - "aslr_proc_path" string => "/proc/sys/kernel/randomize_va_space"; "aslr_conf_path" string => "/etc/sysctl.d/60-enable-aslr.conf"; "aslr_value" string => readfile("${aslr_proc_path}"); - "randomize_va_space_inventory" string => "${aslr_mode[${aslr_value}]} (${aslr_value})", - meta => { "inventory", "attribute_name=Address space layout randomization (ASLR)" }, + + "randomize_va_space_inventory" + string => "${aslr_mode[${aslr_value}]} (${aslr_value})", + meta => { + "inventory", + "attribute_name=Address space layout randomization (ASLR)", + }, comment => "Report on Address space layout randomization (ASLR) mode"; classes: "aslr_enabled" expression => strcmp("${aslr_value}", "2"); + "aslr_management_disabled" - or => { - "exception_enable_aslr", - "data:exception_enable_aslr", - }; + or => { "exception_enable_aslr", "data:exception_enable_aslr" }; files: !aslr_management_disabled.(linux.!aslr_enabled):: @@ -37,6 +39,5 @@ bundle agent enable_aslr commands: !aslr_management_disabled.(linux.!aslr_enabled):: - "sysctl --load ${aslr_conf_path}" - contain => in_shell; + "sysctl --load ${aslr_conf_path}" contain => in_shell; } diff --git a/security/etc-issue-access/etc-issue-access.cf b/security/etc-issue-access/etc-issue-access.cf index 83c463f0..670c445c 100644 --- a/security/etc-issue-access/etc-issue-access.cf +++ b/security/etc-issue-access/etc-issue-access.cf @@ -8,7 +8,7 @@ bundle agent etc_issue_access # https://static.open-scap.org/ssg-guides/ssg-rhel7-guide-anssi_nt28_minimal.html { files: - "/etc/issue" -> { "CCE-83717-9", "CCE-83707-0", "CCE-83347-5"} + "/etc/issue" -> { "CCE-83717-9", "CCE-83707-0", "CCE-83347-5" } perms => mog("644", "root", "root"), if => fileexists("$(this.promiser)"); } diff --git a/security/etc-motd-access/etc-motd-access.cf b/security/etc-motd-access/etc-motd-access.cf index 36627b4c..3f0f7ec0 100644 --- a/security/etc-motd-access/etc-motd-access.cf +++ b/security/etc-motd-access/etc-motd-access.cf @@ -8,7 +8,7 @@ bundle agent etc_motd_access # https://static.open-scap.org/ssg-guides/ssg-rhel7-guide-anssi_nt28_minimal.html { files: - "/etc/motd" -> {"CCE-83737-7", "CCE-83727-8", "CCE-83337-6"} + "/etc/motd" -> { "CCE-83737-7", "CCE-83727-8", "CCE-83337-6" } perms => mog("644", "root", "root"), if => fileexists("$(this.promiser)"); } diff --git a/security/install-aide/install-aide.cf b/security/install-aide/install-aide.cf index 2c3ec472..bf958634 100644 --- a/security/install-aide/install-aide.cf +++ b/security/install-aide/install-aide.cf @@ -7,16 +7,18 @@ bundle agent install_aide { vars: redhat|suse:: - "pkg_name" slist => { "aide" }; # Name in yum - debian:: - "pkg_name" slist => { "aide" }; # name in apt + "pkg_name" slist => { "aide" }; + # Name in yum + debian:: + "pkg_name" slist => { "aide" }; + # name in apt packages: - "${pkg_name}" -> { "CCE-27096-7" } - policy => "present", - if => isvariable("pkg_name"); + "${pkg_name}" -> { "CCE-27096-7" } + policy => "present", + if => isvariable("pkg_name"); reports: - "warning: aide package name not known for this platform" - unless => isvariable("pkg_name"); + "warning: aide package name not known for this platform" + unless => isvariable("pkg_name"); } diff --git a/security/inventory-selinux-modules/main.cf b/security/inventory-selinux-modules/main.cf index 84bea638..fa4757b9 100644 --- a/security/inventory-selinux-modules/main.cf +++ b/security/inventory-selinux-modules/main.cf @@ -1,58 +1,57 @@ body file control { - namespace => "inventory_selinux"; + namespace => "inventory_selinux"; } bundle agent semodule_list_modules { classes: - "_$(this.namespace)_$(this.bundle)_supported_platform" - if => isexecutable( "$(path[semodule])"); + "_$(this.namespace)_$(this.bundle)_supported_platform" + if => isexecutable("$(path[semodule])"); vars: - "path[semodule]" - string => ifelse( isexecutable("/sbin/semodule"), "/sbin/semodule", - isexecutable("/usr/sbin/semodule"), "/usr/sbin/semodule", - "" ), - meta => { "paths.cf" }; + "path[semodule]" + string => ifelse( + isexecutable("/sbin/semodule"), + "/sbin/semodule", + isexecutable("/usr/sbin/semodule"), + "/usr/sbin/semodule", + "" + ), + meta => { "paths.cf" }; _inventory_selinux_semodule_list_modules_supported_platform:: "hours_cached" string => "1", - if => not( isvariable( hours_cached )); + if => not(isvariable(hours_cached)); "cache_file" string => "$(sys.statedir)/$(this.namespace)_$(this.bundle).txt"; "cache_file_age" - string => filestat( $(cache_file), mtime ), - if => fileexists( "$(cache_file)" ); + string => filestat($(cache_file), mtime), + if => fileexists("$(cache_file)"); # Example $(cache_file) content # 400 cfengine-enterprise pp # 200 container pp # 100 wine pp disabled # 100 wireshark pp + "d" + data => data_readstringarrayidx("$(cache_file)", "", "\s+", inf, inf), + if => fileexists($(cache_file)); - - "d" data => data_readstringarrayidx( "$(cache_file)", - "", - "\s+", - inf, - inf), - if => fileexists( $(cache_file) ); - - "i" slist => getindices( "d" ); + "i" slist => getindices("d"); # Inventory enabled and disabled selinux modules "enabled[$(i)]" string => "$(d[$(i)][1])", - if => strcmp( "$(d[$(i)][3])", "" ), + if => strcmp("$(d[$(i)][3])", ""), meta => { "inventory", "attribute_name=SELinux Modules Enabled" }; "disabled[$(i)]" string => "$(d[$(i)][1])", - if => strcmp( "$(d[$(i)][3])", "disabled" ), + if => strcmp("$(d[$(i)][3])", "disabled"), meta => { "inventory", "attribute_name=SELinux Modules Disabled" }; files: @@ -61,14 +60,16 @@ bundle agent semodule_list_modules "$(cache_file)" delete => default:tidy, if => isgreaterthan( - format( "%d", eval( "$(sys.systime)-$(cache_file_age)" )), # now minus mtime of cache file - format( "%d", eval( "$(hours_cached)*60*60" ))); # cache minutes + format("%d", eval("$(sys.systime)-$(cache_file_age)")), + # now minus mtime of cache file + format("%d", eval("$(hours_cached)*60*60")) + ); + # cache minutes commands: _inventory_selinux_semodule_list_modules_supported_platform:: "/sbin/semodule" - arglist => { "--list-modules=full", - ">", "$(cache_file)" }, - contain => default:in_shell, # Required for redirection - if => not( fileexists( $(cache_file) )); - + arglist => { "--list-modules=full", ">", "$(cache_file)" }, + contain => default:in_shell, + # Required for redirection + if => not(fileexists($(cache_file))); } diff --git a/security/inventory-unshadowed-users/policy/main.cf b/security/inventory-unshadowed-users/policy/main.cf index ddbfe2ad..c1949e29 100644 --- a/security/inventory-unshadowed-users/policy/main.cf +++ b/security/inventory-unshadowed-users/policy/main.cf @@ -1,6 +1,6 @@ body file control { - namespace => "inventory_unshadowed_users"; + namespace => "inventory_unshadowed_users"; } bundle agent main @@ -11,7 +11,15 @@ bundle agent main linux:: "inventory[$(default:parsed_etc_passwd_shadow.all_local_users)]" string => "$(default:parsed_etc_passwd_shadow.all_local_users)", - if => not( strcmp("x", "$(default:parsed_etc_passwd_shadow._passwd_passwd[$(default:parsed_etc_passwd_shadow.all_local_users)])")), - meta => { "inventory", "attribute_name=Local users not using shadowed password" }, + if => not( + strcmp( + "x", + "$(default:parsed_etc_passwd_shadow._passwd_passwd[$(default:parsed_etc_passwd_shadow.all_local_users)])" + ) + ), + meta => { + "inventory", + "attribute_name=Local users not using shadowed password", + }, comment => "Inventory of local user who is not using a shadowed password (lacks 'x' in second field of '/etc/passwd')."; } diff --git a/security/maintainers-in-motd/maintainers-in-motd.cf b/security/maintainers-in-motd/maintainers-in-motd.cf index eae8a07c..a43815bd 100644 --- a/security/maintainers-in-motd/maintainers-in-motd.cf +++ b/security/maintainers-in-motd/maintainers-in-motd.cf @@ -2,17 +2,37 @@ bundle agent maintainers_in_motd { classes: linux:: - "has_maintainer" or => { - isvariable("data:variables.maintainer"), - isvariable("data:variables.maintainer_email") - }; + "has_maintainer" + or => { + isvariable("data:variables.maintainer"), + isvariable("data:variables.maintainer_email"), + }; vars: has_maintainer:: - "maintainer" string => ifelse(isvariable("data:variables.maintainer"),"${data:variables.maintainer}",""); - "maintainer_email" string => ifelse(isvariable("data:variables.maintainer_email"),"(${data:variables.maintainer_email})", "(email missing)"); - "purpose" string => ifelse(isvariable("data:variables.purpose"),"${data:variables.purpose}", "unspecified"); - "message" string => "::: use this machine for ${purpose}, contact ${maintainer}${maintainer_email} with any questions/issues. :::"; + "maintainer" + string => ifelse( + isvariable("data:variables.maintainer"), + "${data:variables.maintainer}", + "" + ); + + "maintainer_email" + string => ifelse( + isvariable("data:variables.maintainer_email"), + "(${data:variables.maintainer_email})", + "(email missing)" + ); + + "purpose" + string => ifelse( + isvariable("data:variables.purpose"), + "${data:variables.purpose}", + "unspecified" + ); + + "message" + string => "::: use this machine for ${purpose}, contact ${maintainer}${maintainer_email} with any questions/issues. :::"; files: has_maintainer:: diff --git a/security/uninstall-apache/uninstall-apache.cf b/security/uninstall-apache/uninstall-apache.cf index 5a9a1f37..f4e1e4a9 100644 --- a/security/uninstall-apache/uninstall-apache.cf +++ b/security/uninstall-apache/uninstall-apache.cf @@ -7,18 +7,20 @@ bundle agent uninstall_apache { vars: redhat|suse:: - "pkg_name" slist => { "httpd" }; # Name in yum - debian:: - "pkg_name" slist => { "apache2" }; # name in apt + "pkg_name" slist => { "httpd" }; + # Name in yum + debian:: + "pkg_name" slist => { "apache2" }; + # name in apt classes: - "apache_allowed" - or => { - "hardening_apache_allowed", - "data:hardening_apache_allowed", - "exception_uninstall_apache", - "data:exception_uninstall_apache", - }; + "apache_allowed" + or => { + "hardening_apache_allowed", + "data:hardening_apache_allowed", + "exception_uninstall_apache", + "data:exception_uninstall_apache", + }; packages: !apache_allowed:: diff --git a/security/uninstall-bind/uninstall-bind.cf b/security/uninstall-bind/uninstall-bind.cf index 427f0ee5..b9b397d9 100644 --- a/security/uninstall-bind/uninstall-bind.cf +++ b/security/uninstall-bind/uninstall-bind.cf @@ -7,18 +7,20 @@ bundle agent uninstall_bind { vars: redhat|suse:: - "pkg_name" slist => { "bind" }; # Name in yum - debian:: - "pkg_name" slist => { "bind", "bind9" }; # name in apt + "pkg_name" slist => { "bind" }; + # Name in yum + debian:: + "pkg_name" slist => { "bind", "bind9" }; + # name in apt classes: - "bind_allowed" - or => { - "hardening_bind_allowed", - "data:hardening_bind_allowed", - "exception_uninstall_bind", - "data:exception_uninstall_bind", - }; + "bind_allowed" + or => { + "hardening_bind_allowed", + "data:hardening_bind_allowed", + "exception_uninstall_bind", + "data:exception_uninstall_bind", + }; packages: !bind_allowed:: diff --git a/security/uninstall-dhcp/uninstall-dhcp.cf b/security/uninstall-dhcp/uninstall-dhcp.cf index de50b0a5..dbc50e0e 100644 --- a/security/uninstall-dhcp/uninstall-dhcp.cf +++ b/security/uninstall-dhcp/uninstall-dhcp.cf @@ -7,18 +7,20 @@ bundle agent uninstall_dhcp { vars: redhat|suse:: - "pkg_name" slist => { "dhcp" }; # Name in yum - debian:: - "pkg_name" slist => { "isc-dhcp-server" }; # name in apt + "pkg_name" slist => { "dhcp" }; + # Name in yum + debian:: + "pkg_name" slist => { "isc-dhcp-server" }; + # name in apt classes: - "dhcp_allowed" - or => { - "hardening_dhcp_allowed", - "data:hardening_dhcp_allowed", - "exception_uninstall_dhcp", - "data:exception_uninstall_dhcp", - }; + "dhcp_allowed" + or => { + "hardening_dhcp_allowed", + "data:hardening_dhcp_allowed", + "exception_uninstall_dhcp", + "data:exception_uninstall_dhcp", + }; packages: !dhcp_allowed:: diff --git a/security/uninstall-dovecot/uninstall-dovecot.cf b/security/uninstall-dovecot/uninstall-dovecot.cf index 34e3129d..1a9a8801 100644 --- a/security/uninstall-dovecot/uninstall-dovecot.cf +++ b/security/uninstall-dovecot/uninstall-dovecot.cf @@ -7,18 +7,20 @@ bundle agent uninstall_dovecot { vars: redhat|suse:: - "pkg_name" slist => { "dovecot" }; # Name in yum - debian:: - "pkg_name" slist => { "dovecot-core" }; # name in apt + "pkg_name" slist => { "dovecot" }; + # Name in yum + debian:: + "pkg_name" slist => { "dovecot-core" }; + # name in apt classes: - "dovecot_allowed" - or => { - "hardening_dovecot_allowed", - "data:hardening_dovecot_allowed", - "exception_uninstall_dovecot", - "data:exception_uninstall_dovecot", - }; + "dovecot_allowed" + or => { + "hardening_dovecot_allowed", + "data:hardening_dovecot_allowed", + "exception_uninstall_dovecot", + "data:exception_uninstall_dovecot", + }; packages: !dovecot_allowed:: diff --git a/security/uninstall-packages/main.cf b/security/uninstall-packages/main.cf index cced8d52..83d8d7d1 100644 --- a/security/uninstall-packages/main.cf +++ b/security/uninstall-packages/main.cf @@ -6,8 +6,7 @@ body file control bundle agent uninstall_packages { vars: - "i" - slist => getindices(package_names); + "i" slist => getindices(package_names); packages: "$(package_names[$(i)][name])" diff --git a/security/uninstall-rsh-server/uninstall-rsh-server.cf b/security/uninstall-rsh-server/uninstall-rsh-server.cf index b43c04c4..eb294a6b 100644 --- a/security/uninstall-rsh-server/uninstall-rsh-server.cf +++ b/security/uninstall-rsh-server/uninstall-rsh-server.cf @@ -7,18 +7,20 @@ bundle agent uninstall_rsh_server { vars: redhat|suse:: - "pkg_name" slist => { "rsh-server" }; # Name in yum - debian:: - "pkg_name" slist => { "rsh-server" }; # name in apt + "pkg_name" slist => { "rsh-server" }; + # Name in yum + debian:: + "pkg_name" slist => { "rsh-server" }; + # name in apt classes: - "rsh_server_allowed" - or => { - "hardening_rsh_server_allowed", - "data:hardening_rsh_server_allowed", - "exception_uninstall_rsh_server", - "data:exception_uninstall_rsh_server", - }; + "rsh_server_allowed" + or => { + "hardening_rsh_server_allowed", + "data:hardening_rsh_server_allowed", + "exception_uninstall_rsh_server", + "data:exception_uninstall_rsh_server", + }; packages: !rsh_server_allowed:: diff --git a/security/uninstall-samba/uninstall-samba.cf b/security/uninstall-samba/uninstall-samba.cf index c8e3aa40..c8aa2ef8 100644 --- a/security/uninstall-samba/uninstall-samba.cf +++ b/security/uninstall-samba/uninstall-samba.cf @@ -7,18 +7,20 @@ bundle agent uninstall_samba { vars: redhat|suse:: - "pkg_name" slist => { "samba" }; # Name in yum - debian:: - "pkg_name" slist => { "samba" }; # name in apt + "pkg_name" slist => { "samba" }; + # Name in yum + debian:: + "pkg_name" slist => { "samba" }; + # name in apt classes: - "samba_allowed" - or => { - "hardening_samba_allowed", - "data:hardening_samba_allowed", - "exception_uninstall_samba", - "data:exception_uninstall_samba", - }; + "samba_allowed" + or => { + "hardening_samba_allowed", + "data:hardening_samba_allowed", + "exception_uninstall_samba", + "data:exception_uninstall_samba", + }; packages: !samba_allowed:: diff --git a/security/uninstall-squid/uninstall-squid.cf b/security/uninstall-squid/uninstall-squid.cf index 7a5b2f80..d68b2cfe 100644 --- a/security/uninstall-squid/uninstall-squid.cf +++ b/security/uninstall-squid/uninstall-squid.cf @@ -7,18 +7,20 @@ bundle agent uninstall_squid { vars: redhat|suse:: - "pkg_name" slist => { "squid" }; # Name in yum - debian:: - "pkg_name" slist => { "squid" }; # name in apt + "pkg_name" slist => { "squid" }; + # Name in yum + debian:: + "pkg_name" slist => { "squid" }; + # name in apt classes: - "squid_allowed" - or => { - "hardening_squid_allowed", - "data:hardening_squid_allowed", - "exception_uninstall_squid", - "data:exception_uninstall_squid", - }; + "squid_allowed" + or => { + "hardening_squid_allowed", + "data:hardening_squid_allowed", + "exception_uninstall_squid", + "data:exception_uninstall_squid", + }; packages: !squid_allowed:: diff --git a/security/uninstall-talk/uninstall-talk.cf b/security/uninstall-talk/uninstall-talk.cf index 71c9b379..bb01ec09 100644 --- a/security/uninstall-talk/uninstall-talk.cf +++ b/security/uninstall-talk/uninstall-talk.cf @@ -7,18 +7,20 @@ bundle agent uninstall_talk { vars: redhat|suse:: - "pkg_name" slist => { "talk", "talk-server" }; # Name in yum - debian:: - "pkg_name" slist => { "talk", "talkd" }; # name in apt + "pkg_name" slist => { "talk", "talk-server" }; + # Name in yum + debian:: + "pkg_name" slist => { "talk", "talkd" }; + # name in apt classes: - "talk_allowed" - or => { - "hardening_talk_allowed", - "data:hardening_talk_allowed", - "exception_uninstall_talk", - "data:exception_uninstall_talk", - }; + "talk_allowed" + or => { + "hardening_talk_allowed", + "data:hardening_talk_allowed", + "exception_uninstall_talk", + "data:exception_uninstall_talk", + }; packages: !talk_allowed:: diff --git a/security/uninstall-xinetd/uninstall-xinetd.cf b/security/uninstall-xinetd/uninstall-xinetd.cf index 285a9ebe..b3784ba0 100644 --- a/security/uninstall-xinetd/uninstall-xinetd.cf +++ b/security/uninstall-xinetd/uninstall-xinetd.cf @@ -7,18 +7,20 @@ bundle agent uninstall_xinetd { vars: redhat|suse:: - "pkg_name" slist => { "xinetd" }; # Name in yum - debian:: - "pkg_name" slist => { "xinetd" }; # name in apt + "pkg_name" slist => { "xinetd" }; + # Name in yum + debian:: + "pkg_name" slist => { "xinetd" }; + # name in apt classes: - "xinetd_allowed" - or => { - "hardening_xinetd_allowed", - "data:hardening_xinetd_allowed", - "exception_uninstall_xinetd", - "data:exception_uninstall_xinetd", - }; + "xinetd_allowed" + or => { + "hardening_xinetd_allowed", + "data:hardening_xinetd_allowed", + "exception_uninstall_xinetd", + "data:exception_uninstall_xinetd", + }; packages: !xinetd_allowed:: diff --git a/software/windows/windows-openssh-server.cf b/software/windows/windows-openssh-server.cf index 25f8f875..6a15fe15 100644 --- a/software/windows/windows-openssh-server.cf +++ b/software/windows/windows-openssh-server.cf @@ -10,10 +10,10 @@ bundle agent windows_openssh_server_installed methods: windows:: "Add OpenSSH.Server Capability" - # NOTE: this version "0.0.1.0" is not the actual version but rather a static number that is mysterious to me. - # the actual installed version will be the "latest" - usebundle => windows_capability_installed("OpenSSH.Server~~~~0.0.1.0"), - classes => classes_generic("openssh"); + # NOTE: this version "0.0.1.0" is not the actual version but rather a static number that is mysterious to me. + # the actual installed version will be the "latest" + usebundle => windows_capability_installed("OpenSSH.Server~~~~0.0.1.0"), + classes => classes_generic("openssh"); services: windows.openssh_ok:: From fa464c0ee19f04d7f7098e29f5859f52ab17de36 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 1 Jun 2026 13:36:24 +0200 Subject: [PATCH 113/146] CI: Added cfengine format --check to GH Action Ticket: ENT-14151 Signed-off-by: Ole Herman Schumacher Elgesem --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c770028d..7572f7cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,5 @@ jobs: run: pytest promise-types/ -v - name: Lint policy with CFEngine CLI run: cfengine lint --strict no ./ + - name: Check formatting with CFEngine CLI + run: cfengine format --check From 6d4d5d1da1866eef5fdd38601f519acbfb531d69 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 1 Jun 2026 14:15:24 +0200 Subject: [PATCH 114/146] Manual edits to make comments more consistent Signed-off-by: Ole Herman Schumacher Elgesem --- examples/rss/rss.cf | 6 +++--- inventory/inventory-fwupd/policy.cf | 4 ++-- .../windows-optional-feature/windows-optional-feature.cf | 5 +++-- security/delete-home-dotrhosts/policy/main.cf | 2 +- security/delete-home-dotshosts/policy/main.cf | 2 +- security/install-aide/install-aide.cf | 5 +++-- security/inventory-selinux-modules/main.cf | 7 ++++--- security/uninstall-apache/uninstall-apache.cf | 5 +++-- security/uninstall-bind/uninstall-bind.cf | 5 +++-- security/uninstall-dhcp/uninstall-dhcp.cf | 5 +++-- security/uninstall-dovecot/uninstall-dovecot.cf | 5 +++-- security/uninstall-rsh-server/uninstall-rsh-server.cf | 5 +++-- security/uninstall-samba/uninstall-samba.cf | 5 +++-- security/uninstall-squid/uninstall-squid.cf | 5 +++-- security/uninstall-talk/uninstall-talk.cf | 5 +++-- security/uninstall-xinetd/uninstall-xinetd.cf | 5 +++-- 16 files changed, 44 insertions(+), 32 deletions(-) diff --git a/examples/rss/rss.cf b/examples/rss/rss.cf index 1cf6f4c6..2cc66f2d 100644 --- a/examples/rss/rss.cf +++ b/examples/rss/rss.cf @@ -19,27 +19,27 @@ bundle agent example_rss_promise # REPAIRED if content on disk changes # KEPT if content on disk does not need changed # NOTKEPT if unable to fetch rss, unable to re-write content + # Default to newest feed => "https://blogs.nasa.gov/stationreport/feed/", select => "oldest"; - # Default to newest "/tmp/ISS-random.txt" # Contains Item description # REPAIRED if content on disk changes # KEPT if content on disk does not need changed # NOTKEPT if unable to fetch rss, unable to re-write content + # Default to newest feed => "https://blogs.nasa.gov/stationreport/feed/", select => "random"; - # Default to newest "/tmp/ISS-newest-from-local-file-source.txt" # Contains Item description # REPAIRED if content on disk changes # KEPT if content on disk does not need changed # NOTKEPT if unable to fetch rss, unable to re-write content + # Default to newest feed => "/tmp/iss.xml", select => "newest"; - # Default to newest } bundle agent __main__ diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index 7b68cf6f..3662dc74 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -43,12 +43,12 @@ bundle agent inventory_fwupd_main "_template" string => "$(this.promise_dirname)/fwupd-inventory.mustache"; # TTLs in seconds for the fwupdmgr-derived JSON caches. + # 12 h: "_updates_ttl" string => "43200"; - # 12 h + # 24 h: "_security_ttl" string => "86400"; - # 24 h # Pre-compute file mtimes here rather than calling filestat() inline # inside class expressions: nested $(filestat($(path), mtime)) does not # expand reliably inside isgreaterthan(), so the staleness comparisons diff --git a/management/windows-optional-feature/windows-optional-feature.cf b/management/windows-optional-feature/windows-optional-feature.cf index 878e363b..09087885 100644 --- a/management/windows-optional-feature/windows-optional-feature.cf +++ b/management/windows-optional-feature/windows-optional-feature.cf @@ -80,11 +80,12 @@ bundle agent _promise_state(feature_name, desired_state) commands: windows.state_not_ok:: # -All enables parent features if need be + # -NoRestart so that we don't wait forever for a Yes reply from a user + # then if we ran this command due to needing to, restart below. "${operation}-WindowsOptionalFeature -Online -FeatureName ${feature_name} -NoRestart -All" contain => default:powershell, classes => default:results("bundle", "state"); - # -NoRestart so that we don't wait forever for a Yes reply from a user - # then if we ran this command due to needing to, restart below. + files: windows.state_repaired:: "${_inventory.cache_file}" delete => default:tidy; diff --git a/security/delete-home-dotrhosts/policy/main.cf b/security/delete-home-dotrhosts/policy/main.cf index 654c9118..915a75d4 100644 --- a/security/delete-home-dotrhosts/policy/main.cf +++ b/security/delete-home-dotrhosts/policy/main.cf @@ -20,8 +20,8 @@ bundle agent main vars: dotrhosts_management_disabled:: + # Fill in exception reason if missing (class was used) "exception" - # Fill in exception reason if missing (class was used) string => "Unknown reason", if => not(isvariable("delete_home_dotrhosts:main.exception")); diff --git a/security/delete-home-dotshosts/policy/main.cf b/security/delete-home-dotshosts/policy/main.cf index 489caf56..f4ce6393 100644 --- a/security/delete-home-dotshosts/policy/main.cf +++ b/security/delete-home-dotshosts/policy/main.cf @@ -20,8 +20,8 @@ bundle agent main vars: dotshosts_management_disabled:: + # Fill in exception reason if missing (class was used) "exception" - # Fill in exception reason if missing (class was used) string => "Unknown reason", if => not(isvariable("delete_home_dotshosts:main.exception")); diff --git a/security/install-aide/install-aide.cf b/security/install-aide/install-aide.cf index bf958634..8871c6e4 100644 --- a/security/install-aide/install-aide.cf +++ b/security/install-aide/install-aide.cf @@ -7,12 +7,13 @@ bundle agent install_aide { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "aide" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "aide" }; - # name in apt + packages: "${pkg_name}" -> { "CCE-27096-7" } policy => "present", diff --git a/security/inventory-selinux-modules/main.cf b/security/inventory-selinux-modules/main.cf index fa4757b9..caad3fbc 100644 --- a/security/inventory-selinux-modules/main.cf +++ b/security/inventory-selinux-modules/main.cf @@ -60,16 +60,17 @@ bundle agent semodule_list_modules "$(cache_file)" delete => default:tidy, if => isgreaterthan( + # now minus mtime of cache file: format("%d", eval("$(sys.systime)-$(cache_file_age)")), - # now minus mtime of cache file + # cache minutes: format("%d", eval("$(hours_cached)*60*60")) ); - # cache minutes + commands: _inventory_selinux_semodule_list_modules_supported_platform:: "/sbin/semodule" arglist => { "--list-modules=full", ">", "$(cache_file)" }, + # Required for redirection: contain => default:in_shell, - # Required for redirection if => not(fileexists($(cache_file))); } diff --git a/security/uninstall-apache/uninstall-apache.cf b/security/uninstall-apache/uninstall-apache.cf index f4e1e4a9..ed55213a 100644 --- a/security/uninstall-apache/uninstall-apache.cf +++ b/security/uninstall-apache/uninstall-apache.cf @@ -7,12 +7,13 @@ bundle agent uninstall_apache { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "httpd" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "apache2" }; - # name in apt + classes: "apache_allowed" or => { diff --git a/security/uninstall-bind/uninstall-bind.cf b/security/uninstall-bind/uninstall-bind.cf index b9b397d9..7ee38e4a 100644 --- a/security/uninstall-bind/uninstall-bind.cf +++ b/security/uninstall-bind/uninstall-bind.cf @@ -7,12 +7,13 @@ bundle agent uninstall_bind { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "bind" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "bind", "bind9" }; - # name in apt + classes: "bind_allowed" or => { diff --git a/security/uninstall-dhcp/uninstall-dhcp.cf b/security/uninstall-dhcp/uninstall-dhcp.cf index dbc50e0e..76a13c15 100644 --- a/security/uninstall-dhcp/uninstall-dhcp.cf +++ b/security/uninstall-dhcp/uninstall-dhcp.cf @@ -7,12 +7,13 @@ bundle agent uninstall_dhcp { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "dhcp" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "isc-dhcp-server" }; - # name in apt + classes: "dhcp_allowed" or => { diff --git a/security/uninstall-dovecot/uninstall-dovecot.cf b/security/uninstall-dovecot/uninstall-dovecot.cf index 1a9a8801..5e5d88aa 100644 --- a/security/uninstall-dovecot/uninstall-dovecot.cf +++ b/security/uninstall-dovecot/uninstall-dovecot.cf @@ -7,12 +7,13 @@ bundle agent uninstall_dovecot { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "dovecot" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "dovecot-core" }; - # name in apt + classes: "dovecot_allowed" or => { diff --git a/security/uninstall-rsh-server/uninstall-rsh-server.cf b/security/uninstall-rsh-server/uninstall-rsh-server.cf index eb294a6b..d4d65987 100644 --- a/security/uninstall-rsh-server/uninstall-rsh-server.cf +++ b/security/uninstall-rsh-server/uninstall-rsh-server.cf @@ -7,12 +7,13 @@ bundle agent uninstall_rsh_server { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "rsh-server" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "rsh-server" }; - # name in apt + classes: "rsh_server_allowed" or => { diff --git a/security/uninstall-samba/uninstall-samba.cf b/security/uninstall-samba/uninstall-samba.cf index c8aa2ef8..62579d34 100644 --- a/security/uninstall-samba/uninstall-samba.cf +++ b/security/uninstall-samba/uninstall-samba.cf @@ -7,12 +7,13 @@ bundle agent uninstall_samba { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "samba" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "samba" }; - # name in apt + classes: "samba_allowed" or => { diff --git a/security/uninstall-squid/uninstall-squid.cf b/security/uninstall-squid/uninstall-squid.cf index d68b2cfe..124877d1 100644 --- a/security/uninstall-squid/uninstall-squid.cf +++ b/security/uninstall-squid/uninstall-squid.cf @@ -7,12 +7,13 @@ bundle agent uninstall_squid { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "squid" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "squid" }; - # name in apt + classes: "squid_allowed" or => { diff --git a/security/uninstall-talk/uninstall-talk.cf b/security/uninstall-talk/uninstall-talk.cf index bb01ec09..1d6f94ce 100644 --- a/security/uninstall-talk/uninstall-talk.cf +++ b/security/uninstall-talk/uninstall-talk.cf @@ -7,12 +7,13 @@ bundle agent uninstall_talk { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "talk", "talk-server" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "talk", "talkd" }; - # name in apt + classes: "talk_allowed" or => { diff --git a/security/uninstall-xinetd/uninstall-xinetd.cf b/security/uninstall-xinetd/uninstall-xinetd.cf index b3784ba0..5e8d653a 100644 --- a/security/uninstall-xinetd/uninstall-xinetd.cf +++ b/security/uninstall-xinetd/uninstall-xinetd.cf @@ -7,12 +7,13 @@ bundle agent uninstall_xinetd { vars: redhat|suse:: + # Name in yum: "pkg_name" slist => { "xinetd" }; - # Name in yum debian:: + # Name in apt: "pkg_name" slist => { "xinetd" }; - # name in apt + classes: "xinetd_allowed" or => { From c58c5d651d1beeba6e0a86373872cfe38c9f653b Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 1 Jun 2026 17:58:28 +0200 Subject: [PATCH 115/146] GH Actions: Copied stale.yml from core Signed-off-by: Ole Herman Schumacher Elgesem --- .github/workflows/stale.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..326b2e5f --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,24 @@ +# For more information, see: +# https://github.com/actions/stale +name: Label and close stale pull requests + +on: + schedule: + - cron: "31 19 * * *" + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-pr-message: "Marking this PR as stale due to inactivity; it will be closed in 7 days." + stale-pr-label: "stale" + days-before-stale: 30 + days-before-close: 7 + close-pr-message: "Closing PR due to inactivity. Feel free to open a new PR if necessary." From deb9176bd5f03f8f405ab0b4a15dae42cce6f77e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Mon, 15 Jun 2026 22:53:11 +0200 Subject: [PATCH 116/146] cfengine format with new version Signed-off-by: Ole Herman Schumacher Elgesem --- inventory/inventory-fde/inventory-fde.cf | 4 ++-- management/manage-fwupd/main.cf | 8 ++------ management/package-method-winget/package-method-winget.cf | 4 ++++ .../powershell-execution-policy.cf | 1 + management/windows-capability/windows-capability.cf | 1 + promise-types/git/test.cf | 3 +++ promise-types/groups/groups.cf | 5 ----- promise-types/json/test.cf | 8 +++++--- promise-types/symlinks/test.cf | 4 ++++ promise-types/systemd/test.cf | 4 ++++ security/dirtyfrag/dirtyfrag.cf | 7 ++----- 11 files changed, 28 insertions(+), 21 deletions(-) diff --git a/inventory/inventory-fde/inventory-fde.cf b/inventory/inventory-fde/inventory-fde.cf index 063af89d..9231f26f 100644 --- a/inventory/inventory-fde/inventory-fde.cf +++ b/inventory/inventory-fde/inventory-fde.cf @@ -2,6 +2,7 @@ body file control { namespace => "inventory_fde"; } + # Duplicated from the CFEngine standard library so this module can be parsed # and tested standalone without loading the full masterfiles. # _tidy: lib/files.cf body delete tidy @@ -200,8 +201,7 @@ bundle agent main "_luks1_dump[${_dm_devices}]" string => execresult( - "${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", - "noshell" + "${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", "noshell" ), if => canonify("_dm_is_luks1_${_dm_devices}"); diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index 6c651c34..f62cc434 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -144,16 +144,12 @@ bundle agent main commands: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: "$(default:inventory_fwupd_main.fwupdmgr)" - arglist => { - "update", "--no-reboot-check", "$(_dev_id[$(_dev_idx)])" - }, + arglist => { "update", "--no-reboot-check", "$(_dev_id[$(_dev_idx)])" }, handle => canonify( "manage_fwupd_apply_device_$(_dev_idx)_$(_dev_name[$(_dev_idx)])" ), if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), - classes => default:results( - "bundle", "manage_fwupd_update_$(_dev_idx)" - ), + classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; # --- Post-update marker and reboot -------------------------------- files: diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf index 1da97b86..c393c315 100644 --- a/management/package-method-winget/package-method-winget.cf +++ b/management/package-method-winget/package-method-winget.cf @@ -17,18 +17,22 @@ body package_method winget # 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 diff --git a/management/powershell-execution-policy/powershell-execution-policy.cf b/management/powershell-execution-policy/powershell-execution-policy.cf index 5544ec99..912d7c3d 100644 --- a/management/powershell-execution-policy/powershell-execution-policy.cf +++ b/management/powershell-execution-policy/powershell-execution-policy.cf @@ -21,6 +21,7 @@ bundle agent powershell_execution_policy_inventory 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 diff --git a/management/windows-capability/windows-capability.cf b/management/windows-capability/windows-capability.cf index c1d8ad22..d1a1bc70 100644 --- a/management/windows-capability/windows-capability.cf +++ b/management/windows-capability/windows-capability.cf @@ -27,6 +27,7 @@ body file control { namespace => "windows_capability"; } + # https://learn.microsoft.com/en-us/powershell/module/dism/get-windowscapability?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/powershell/module/dism/remove-windowscapability?view=windowsserver2022-ps # https://learn.microsoft.com/en-us/powershell/module/dism/add-windowscapability?view=windowsserver2022-ps diff --git a/promise-types/git/test.cf b/promise-types/git/test.cf index ef3fc38b..513165fd 100644 --- a/promise-types/git/test.cf +++ b/promise-types/git/test.cf @@ -2,6 +2,7 @@ body common control { version => "1.0"; } + ####################################################### bundle agent init { @@ -34,6 +35,7 @@ body delete init_delete dirlinks => "delete"; rmdirs => "true"; } + ####################################################### promise agent git { @@ -58,6 +60,7 @@ bundle agent test expression => canonify("starter_pack_repo_updated"), scope => "namespace"; } + ####################################################### bundle agent check { diff --git a/promise-types/groups/groups.cf b/promise-types/groups/groups.cf index 5ab2f389..e0ad4421 100644 --- a/promise-types/groups/groups.cf +++ b/promise-types/groups/groups.cf @@ -9,7 +9,6 @@ promise agent groups path => "$(sys.workdir)/modules/promises/groups.py"; interpreter => "/usr/bin/python3"; } - @if minimum_version(3.20) body members foo # @breif Define members for group foo using attributes `include` and @@ -19,7 +18,6 @@ body members foo exclude => { "malcom" }; } @else - bundle common foo # @breif Define members using JSON string. { @@ -36,7 +34,6 @@ body members bar only => { "alice" }; } @else - bundle common bar # @brief Define members using data container. { @@ -44,7 +41,6 @@ bundle common bar "members" data => '{ "only": ["alice"] }'; } @endif - bundle agent example_groups # @breif Example groups promise statements. { @@ -56,7 +52,6 @@ bundle agent example_groups @else members => "$(foo.members)"; @endif - "bar" # policy defaults to present @if minimum_version(3.20) diff --git a/promise-types/json/test.cf b/promise-types/json/test.cf index c5c9c346..c7b00901 100644 --- a/promise-types/json/test.cf +++ b/promise-types/json/test.cf @@ -4,6 +4,7 @@ body common control version => "1.0"; bundlesequence => { "init", "test", "check", "cleanup" }; } + ####################################################### bundle agent init { @@ -29,6 +30,7 @@ bundle agent init "$(this.promise_dirname)/tests/to_append_1.json" create => "true"; } + ####################################################### promise agent json { @@ -73,14 +75,13 @@ bundle agent test "$(this.promise_dirname)/tests/to_append_2.json:f" primitive => "true"; "$(this.promise_dirname)/tests/to_append_2.json:g" primitive => "null"; } + ####################################################### bundle agent check { vars: "to_overwrite_content" - data => readjson( - "$(this.promise_dirname)/tests/to_overwrite.json", 100k - ); + data => readjson("$(this.promise_dirname)/tests/to_overwrite.json", 100k); "to_modify_content" data => readjson("$(this.promise_dirname)/tests/to_modify.json", 100k); @@ -148,6 +149,7 @@ bundle agent check !ok:: "$(this.promise_filename) FAIL"; } + # ####################################################### bundle agent cleanup { diff --git a/promise-types/symlinks/test.cf b/promise-types/symlinks/test.cf index 828ab69a..90b23564 100644 --- a/promise-types/symlinks/test.cf +++ b/promise-types/symlinks/test.cf @@ -4,6 +4,7 @@ body common control version => "1.0"; bundlesequence => { "init", "test", "check", "cleanup" }; } + ####################################################### bundle agent init { @@ -14,6 +15,7 @@ bundle agent init "/tmp/replaced-link" link_from => ln_s("/tmp/other-dir"); "/tmp/already-existing-link" link_from => ln_s("/tmp/other-dir"); } + ####################################################### promise agent symlinks { @@ -50,6 +52,7 @@ bundle agent test directory => "/tmp/other-dir", classes => outcome("didnothing"); } + ####################################################### bundle agent check { @@ -81,6 +84,7 @@ bundle agent check !ok:: "$(this.promise_filename) FAIL"; } + # ####################################################### bundle agent cleanup { diff --git a/promise-types/systemd/test.cf b/promise-types/systemd/test.cf index 5a05975c..426d9d79 100644 --- a/promise-types/systemd/test.cf +++ b/promise-types/systemd/test.cf @@ -2,10 +2,12 @@ body common control { version => "1.0"; } + ####################################################### bundle agent init { } + ####################################################### promise agent systemd { @@ -35,6 +37,7 @@ bundle agent test expression => canonify("sample_restarted"), scope => "namespace"; } + ####################################################### bundle agent check { @@ -55,6 +58,7 @@ bundle agent check !ok:: "$(this.promise_filename) FAIL"; } + ####################################################### bundle agent cleanup { diff --git a/security/dirtyfrag/dirtyfrag.cf b/security/dirtyfrag/dirtyfrag.cf index 2fc6b108..80a4f29e 100644 --- a/security/dirtyfrag/dirtyfrag.cf +++ b/security/dirtyfrag/dirtyfrag.cf @@ -222,8 +222,7 @@ bundle agent main "inventory_dirtyfrag_rxrpc" string => "$(_rxrpc_status)", meta => { - "inventory", - "attribute_name=Dirty Frag CVE-2026-43500 (RxRPC) status", + "inventory", "attribute_name=Dirty Frag CVE-2026-43500 (RxRPC) status", }, comment => "CVE-2026-43500 RxRPC mitigation status"; @@ -314,9 +313,7 @@ bundle agent main # admin override. Userns mitigation is handled in the # vulnerability condition (see dirtyfrag_esp_needs_mitigation). "_esp_mitigated" - or => { - "_esp_conf_exists", "_esp_kernel_patched", "_esp_admin_patched", - }; + or => { "_esp_conf_exists", "_esp_kernel_patched", "_esp_admin_patched" }; # RxRPC is mitigated by the modprobe blacklist, patched kernel, # or admin override. From b89a5a812f016d5152633a1ad172eb9ff905d13a Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 17:44:49 +0200 Subject: [PATCH 117/146] Added policy-dispatcher experimental module Co-authored-by: Claude Fable 5 Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 42 ++++++++++++++ management/policy-dispatcher/README.md | 40 +++++++++++++ management/policy-dispatcher/def.json | 19 +++++++ management/policy-dispatcher/main.cf | 79 ++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 management/policy-dispatcher/README.md create mode 100644 management/policy-dispatcher/def.json create mode 100644 management/policy-dispatcher/main.cf diff --git a/cfbs.json b/cfbs.json index 28923372..9864d8b5 100644 --- a/cfbs.json +++ b/cfbs.json @@ -397,6 +397,48 @@ } ] }, + "policy-dispatcher": { + "description": "Policy dispatcher for saving policy snippets to files and running them with cf-agent", + "subdirectory": "management/policy-dispatcher", + "steps": [ + "copy main.cf services/cfbs/modules/policy-dispatcher/main.cf", + "policy_files services/cfbs/modules/policy-dispatcher/main.cf", + "bundles policy_dispatcher:main", + "input ./input.json def.json" + ], + "input": [ + { + "type": "list", + "variable": "policy_files_to_run", + "namespace": "policy_dispatcher", + "bundle": "main", + "label": "Policy", + "subtype": [ + { + "key": "policy", + "type": "string", + "label": "Policy", + "question": "Policy snippet to save and run (must contain a 'main' bundle)" + }, + { + "key": "condition", + "type": "string", + "label": "Condition", + "question": "Condition for when to run", + "default": "any" + }, + { + "key": "ifelapsed", + "type": "string", + "label": "ifelapsed", + "question": "Number of minutes between promise assessments", + "default": "5" + } + ], + "while": "Do you want to specify more policy snippets to be run?" + } + ] + }, "promise-type-ansible": { "description": "Promise type to manage systemd services.", "subdirectory": "promise-types/ansible", diff --git a/management/policy-dispatcher/README.md b/management/policy-dispatcher/README.md new file mode 100644 index 00000000..6d03d234 --- /dev/null +++ b/management/policy-dispatcher/README.md @@ -0,0 +1,40 @@ +This module enables the running of CFEngine policy snippets based on input from the Mission-Portal's build page or `cfbs input`. + +Each policy snippet is saved to a file in `$(sys.statedir)` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. + +**Note:** +The policy snippets are run with root-privilege (uid=0). +Each snippet must be a complete, standalone policy file, containing a `main` bundle (this is what `cf-agent` runs by default). + +--- + +**Usage:** + +* `policy` - The policy snippet to save and run. Must contain a `main` bundle. + +* `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` + +* `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. + +E.g. +```json +... +{ +"policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", +"condition": "linux", +"ifelapsed": "5" +}, +... +``` +Would report "Hello World" on every linux device with 5 minute intervals. + +--- + +## Contribute + +Feel free to open pull requests to expand this documentation, add features or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://tracker.mender.io/issues/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. diff --git a/management/policy-dispatcher/def.json b/management/policy-dispatcher/def.json new file mode 100644 index 00000000..81a59cf2 --- /dev/null +++ b/management/policy-dispatcher/def.json @@ -0,0 +1,19 @@ +{ + "variables": { + "policy_dispatcher:main.policy_files_to_run": { + "value": [ + { + "policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", + "condition": "any", + "ifelapsed": "5" + }, + { + "policy": "bundle agent main\n{\n files:\n \"/tmp/policy-dispatcher-example.txt\"\n create => \"true\",\n content => \"This file is managed by the policy-dispatcher module\n\";\n}\n", + "condition": "linux", + "ifelapsed": "60" + } + ], + "comment": "Example data generated by 'cfbs input' / Mission Portal build page" + } + } +} diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf new file mode 100644 index 00000000..b4ec9c84 --- /dev/null +++ b/management/policy-dispatcher/main.cf @@ -0,0 +1,79 @@ +body file control +{ + namespace => "policy_dispatcher"; +} + +bundle agent main +{ + classes: + "enabled" expression => isvariable("policy_files_to_run"); + "run_$(i)" expression => "$(_condition[$(i)])"; + + vars: + enabled:: + "i" slist => getindices(policy_files_to_run); + + "_policy[$(i)]" + string => "$(policy_files_to_run[$(i)][policy])", + if => isvariable("policy_files_to_run[$(i)][policy]"); + + "_condition[$(i)]" + string => ifelse( + not(strcmp("$(policy_files_to_run[$(i)][condition])", "")), + "$(policy_files_to_run[$(i)][condition])", + "any" + ); + + "_ifelapsed[$(i)]" + string => ifelse( + not(strcmp("$(policy_files_to_run[$(i)][ifelapsed])", "")), + "$(policy_files_to_run[$(i)][ifelapsed])", + "5" + ); + + "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher-$(i).cf"; + + files: + enabled:: + "$(_file[$(i)])" + create => "true", + content => "$(_policy[$(i)])", + perms => mo("600", "root"), + if => "run_$(i)"; + + reports: + enabled:: + "Policy [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + + !enabled:: + "Policy-dispatcher: policy_files_to_run variable not found"; + + commands: + enabled:: + "$(sys.cf_agent) --no-lock --file $(_file[$(i)])" + if => "run_$(i)", + action => ifelapsed("$(_ifelapsed[$(i)])"); +} + +body perms mo(mode, owner) +{ + mode => "$(mode)"; + owners => { "$(owner)" }; + rxdirs => "false"; +} + +body action ifelapsed(x) +{ + ifelapsed => "$(x)"; +} + +body file control +{ + namespace => "default"; +} + +bundle agent __main__ +{ + methods: + "policy_dispatcher:main"; +} From bcecc2a9076f3962a51554dbf9f508a43f51f4ff Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 17:46:10 +0200 Subject: [PATCH 118/146] cfengine format with the newest version Signed-off-by: Ole Herman Schumacher Elgesem --- management/manage-fwupd/main.cf | 4 ++++ management/package-method-winget/package-method-winget.cf | 2 ++ 2 files changed, 6 insertions(+) diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index f62cc434..5304c720 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -133,6 +133,7 @@ bundle agent main "fwupd-refresh.timer" service_policy => "enabled", comment => "Ensure the fwupd metadata refresh timer is enabled so the LVFS firmware catalog stays current"; + # --- Stale marker cleanup ---------------------------------------- files: linux:: @@ -140,6 +141,7 @@ bundle agent main delete => default:tidy, if => not("_update_applied"), comment => "Remove stale marker from a previous boot cycle"; + # --- Apply firmware updates -------------------------------------- commands: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -151,6 +153,7 @@ bundle agent main if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; + # --- Post-update marker and reboot -------------------------------- files: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -184,6 +187,7 @@ bundle agent main }, handle => "manage_fwupd_reboot_after_update_fallback", comment => "Fallback reboot for non-systemd systems (shutdown -r +1)"; + # --- Inventory -------------------------------------------------- vars: linux.default:have_fwupdmgr:: diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf index c393c315..6fccf5b3 100644 --- a/management/package-method-winget/package-method-winget.cf +++ b/management/package-method-winget/package-method-winget.cf @@ -6,6 +6,7 @@ body package_method winget 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 # @@ -14,6 +15,7 @@ body package_method winget # 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 "; From fa920014040a2a6db0117abe9b03fc9a4b99d32e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:24:01 +0200 Subject: [PATCH 119/146] policy-dispatcher: Changed to use folder in statedir Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index b4ec9c84..4756ddba 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -31,7 +31,7 @@ bundle agent main "5" ); - "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher-$(i).cf"; + "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher/$(i).cf"; files: enabled:: From 754da517dde5d1da16c6295337bb721808767ea2 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:31:18 +0200 Subject: [PATCH 120/146] policy-dispatcher: Improved README Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/README.md | 31 ++++++++++---------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/management/policy-dispatcher/README.md b/management/policy-dispatcher/README.md index 6d03d234..b3b7285d 100644 --- a/management/policy-dispatcher/README.md +++ b/management/policy-dispatcher/README.md @@ -1,34 +1,27 @@ -This module enables the running of CFEngine policy snippets based on input from the Mission-Portal's build page or `cfbs input`. +This module enables the running of CFEngine policy snippets based on input from Build in Mission Portal or `cfbs input`. -Each policy snippet is saved to a file in `$(sys.statedir)` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. +Each policy snippet is saved to a file in `/var/cfengine/state/policy-dispatcher/.cf` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. -**Note:** -The policy snippets are run with root-privilege (uid=0). +**Note:** The policy snippets are run with root-privilege (uid=0). Each snippet must be a complete, standalone policy file, containing a `main` bundle (this is what `cf-agent` runs by default). ---- - **Usage:** -* `policy` - The policy snippet to save and run. Must contain a `main` bundle. - -* `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` - -* `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. +- `policy` - The policy snippet to save and run. Must contain a `main` bundle. +- `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` +- `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. E.g. + ```json -... { -"policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", -"condition": "linux", -"ifelapsed": "5" -}, -... + "policy": "bundle agent main { reports: \"Hello World\"; } ", + "condition": "linux", + "ifelapsed": "5" +} ``` -Would report "Hello World" on every linux device with 5 minute intervals. ---- +Would report "Hello World" on every linux device with 5 minute intervals. ## Contribute From cd99668537fded29b2e7b1917fb4eea0a5e6277f Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:34:28 +0200 Subject: [PATCH 121/146] policy-dispatcher: Small cleanups and improvements Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index 4756ddba..cccf9e49 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -7,13 +7,12 @@ bundle agent main { classes: "enabled" expression => isvariable("policy_files_to_run"); - "run_$(i)" expression => "$(_condition[$(i)])"; vars: enabled:: "i" slist => getindices(policy_files_to_run); - "_policy[$(i)]" + "_policy_file[$(i)]" string => "$(policy_files_to_run[$(i)][policy])", if => isvariable("policy_files_to_run[$(i)][policy]"); @@ -37,21 +36,21 @@ bundle agent main enabled:: "$(_file[$(i)])" create => "true", - content => "$(_policy[$(i)])", + content => "$(_policy_file[$(i)])", perms => mo("600", "root"), - if => "run_$(i)"; + if => "$(_condition[$(i)])"; reports: - enabled:: - "Policy [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + enabled.DEBUG:: + "Policy file [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; - !enabled:: - "Policy-dispatcher: policy_files_to_run variable not found"; + !enabled.DEBUG:: + "policy-dispatcher: policy_files_to_run variable not found"; commands: enabled:: "$(sys.cf_agent) --no-lock --file $(_file[$(i)])" - if => "run_$(i)", + if => "$(_condition[$(i)])", action => ifelapsed("$(_ifelapsed[$(i)])"); } From 80d8e71e98fea79379023cfb49d5dbc3ab8668e6 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:55:24 +0200 Subject: [PATCH 122/146] policy-dispatcher: Clean up leftover files Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index cccf9e49..c606ca44 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -32,6 +32,14 @@ bundle agent main "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher/$(i).cf"; + "_expected_files" + slist => maplist("$(sys.statedir)/policy-dispatcher/$(this).cf", @(i)); + + "_actual_files" + slist => findfiles("$(sys.statedir)/policy-dispatcher/*.cf"); + + "_unexpected_files" slist => difference(_actual_files, _expected_files); + files: enabled:: "$(_file[$(i)])" @@ -40,6 +48,8 @@ bundle agent main perms => mo("600", "root"), if => "$(_condition[$(i)])"; + "$(_unexpected_files)" delete => tidy; + reports: enabled.DEBUG:: "Policy file [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; @@ -54,6 +64,14 @@ bundle agent main action => ifelapsed("$(_ifelapsed[$(i)])"); } +body delete tidy +# @brief Delete the file and remove empty directories +# and links to directories +{ + dirlinks => "delete"; + rmdirs => "true"; +} + body perms mo(mode, owner) { mode => "$(mode)"; From ef2937095c0e7798f3015ac27ebe63da292e48d2 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 16 Jul 2026 16:00:07 -0500 Subject: [PATCH 123/146] Removed noisy reports from command-dispatcher Ticket: ENT-14312 --- management/command-dispatcher/main.cf | 7 ------- 1 file changed, 7 deletions(-) diff --git a/management/command-dispatcher/main.cf b/management/command-dispatcher/main.cf index 2a309960..75598ba0 100644 --- a/management/command-dispatcher/main.cf +++ b/management/command-dispatcher/main.cf @@ -31,13 +31,6 @@ bundle agent main "5" ); - reports: - enabled:: - "Command [$(i)]: $(_command[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; - - !enabled:: - "Command-dispatcher: commands_to_run variable not found"; - commands: enabled:: "$(_command[$(i)])" From 9e4c1b27f09e06ac0e854c4d8e84492761ce3d4d Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Thu, 16 Jul 2026 16:08:55 -0500 Subject: [PATCH 124/146] Reformatted policy files for CI --- management/manage-fwupd/main.cf | 4 ++++ management/package-method-winget/package-method-winget.cf | 2 ++ 2 files changed, 6 insertions(+) diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index f62cc434..5304c720 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -133,6 +133,7 @@ bundle agent main "fwupd-refresh.timer" service_policy => "enabled", comment => "Ensure the fwupd metadata refresh timer is enabled so the LVFS firmware catalog stays current"; + # --- Stale marker cleanup ---------------------------------------- files: linux:: @@ -140,6 +141,7 @@ bundle agent main delete => default:tidy, if => not("_update_applied"), comment => "Remove stale marker from a previous boot cycle"; + # --- Apply firmware updates -------------------------------------- commands: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -151,6 +153,7 @@ bundle agent main if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; + # --- Post-update marker and reboot -------------------------------- files: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -184,6 +187,7 @@ bundle agent main }, handle => "manage_fwupd_reboot_after_update_fallback", comment => "Fallback reboot for non-systemd systems (shutdown -r +1)"; + # --- Inventory -------------------------------------------------- vars: linux.default:have_fwupdmgr:: diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf index c393c315..6fccf5b3 100644 --- a/management/package-method-winget/package-method-winget.cf +++ b/management/package-method-winget/package-method-winget.cf @@ -6,6 +6,7 @@ body package_method winget 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 # @@ -14,6 +15,7 @@ body package_method winget # 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 "; From 3589b607c89487d0e8d7bf60854728e396159277 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 11:07:17 +0000 Subject: [PATCH 125/146] Make command-dispatcher reports only fire in debug mode The dispatch/status reports promises are now guarded with .DEBUG so they only appear when running in debug mode, matching the equivalent reports in the policy-dispatcher module. Ticket: ENT-14312 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ge2NA5hG9Mr5adSeA78vrD --- management/command-dispatcher/main.cf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/management/command-dispatcher/main.cf b/management/command-dispatcher/main.cf index 75598ba0..62a45974 100644 --- a/management/command-dispatcher/main.cf +++ b/management/command-dispatcher/main.cf @@ -31,6 +31,13 @@ bundle agent main "5" ); + reports: + enabled.DEBUG:: + "Command [$(i)]: $(_command[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + + !enabled.DEBUG:: + "Command-dispatcher: commands_to_run variable not found"; + commands: enabled:: "$(_command[$(i)])" From 8258f78228e5b3651986389c94e726c005d108ec Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 29 Jul 2026 13:39:02 +0200 Subject: [PATCH 126/146] Added syntax highlighting to README.md for groups promise type Signed-off-by: Lars Erik Wik --- promise-types/groups/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/promise-types/groups/README.md b/promise-types/groups/README.md index b2637c77..c299c25c 100644 --- a/promise-types/groups/README.md +++ b/promise-types/groups/README.md @@ -18,7 +18,7 @@ The `groups` promise type helps managing local groups, letting you ensure some u Present group `foo` including user `alice` and `bob`, but excluding user `malcom`: -``` +```cf3 @if minimum_version(3.20) body members foo { @@ -43,7 +43,7 @@ bundle agent main Present group `bar` with GID `123` including only user `alice`: -``` +```cf3 @if minimum_version(3.20) body members bar { @@ -66,7 +66,7 @@ bundle agent main Absent group `baz`: -``` +```cf3 bundle agent main { groups: From 725a21488f1d234f9bfd7637cdf70b4213fe9e05 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 29 Jul 2026 13:43:29 +0200 Subject: [PATCH 127/146] Formatted README.md for groups promise type Signed-off-by: Lars Erik Wik --- promise-types/groups/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/promise-types/groups/README.md b/promise-types/groups/README.md index c299c25c..08754164 100644 --- a/promise-types/groups/README.md +++ b/promise-types/groups/README.md @@ -4,15 +4,15 @@ The `groups` promise type helps managing local groups, letting you ensure some u ## Requirements -* Unix-like system. +- Unix-like system. ## Attributes -| Name | Type | Description | Mandatory | Default | -| --------- | --------------------------- | ----------------------------------------------------------------------------------------------- | --------- | ------- | -| `policy` | `string` | Whether group should be present or absent on the local host | no | present | -| `members` | `string` / `data` / `body` | JSON string / data container / custom body containing attributes "include", "exclude" & "only" | no | - | -| `gid` | `integer` | The GID of the group | no | - | +| Name | Type | Description | Mandatory | Default | +| --------- | -------------------------- | ---------------------------------------------------------------------------------------------- | --------- | ------- | +| `policy` | `string` | Whether group should be present or absent on the local host | no | present | +| `members` | `string` / `data` / `body` | JSON string / data container / custom body containing attributes "include", "exclude" & "only" | no | - | +| `gid` | `integer` | The GID of the group | no | - | ## Examples From 639653ce5d328479cb37289d222ce87b150d9900 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 30 Jul 2026 12:03:09 +0200 Subject: [PATCH 128/146] Narrow ansible ModuleNotFoundError guard to the imports Ticket: ENT-14326 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 292 ++++++++++++----------- 1 file changed, 147 insertions(+), 145 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 610d5ecd..6ff1c520 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -1,4 +1,5 @@ import os +import sys from typing import Dict, Tuple, List from cfengine_module_library import PromiseModule, ValidationError, Result @@ -13,164 +14,165 @@ from ansible.plugins.callback import CallbackBase from ansible.vars.manager import VariableManager from ansible.plugins.loader import init_plugin_loader +except ModuleNotFoundError: - class CallbackModule(CallbackBase): - CALLBACK_VERSION = 1.0 - CALLBACK_TYPE = "stdout" - CALLBACK_NAME = "cfengine" - - def __init__(self, *args, promise, **kw): - self.promise = promise - self.hosts = set() - self.changed = False - super(CallbackModule, self).__init__(*args, **kw) - - def v2_runner_on_start(self, host, task): - self.hosts.add(str(host)) - self.promise.log_verbose( - "Task '" + task.name + "' started on '" + str(host) + "'" - ) - - def v2_runner_on_ok(self, result): - is_changed = result.is_changed() - if is_changed: - self.changed = True - self.promise.log_info( - "Task '" + result.task_name + "' successfully changed" - ) - else: - self.promise.log_verbose( - "Task '" + result.task_name + "' didn't change" - ) - - def v2_runner_on_failed(self, result, ignore_errors=False): - self.promise.log_error("Task '" + result.task_name + "' failed") - - def v2_runner_on_skipped(self, result): - self.promise.log_error("Task '" + result.task_name + "' was skipped") + class UnavailableAnsiblePromiseTypeModule(PromiseModule): - def v2_playbook_on_stats(self, stats): - for host in self.hosts: - summary_dict = stats.summarize(host) - summary = " ".join( - "%s=%s" % (k, v) for k, v in summary_dict.items() if v > 0 - ) - if summary_dict.get("unreachable"): - self.promise.log_error("Host '" + host + "' is unreachable") - elif summary: - self.promise.log_verbose( - "Summary of the tasks for '" + host + "' is: " + summary - ) - - class AnsiblePromiseTypeModule(PromiseModule): def __init__(self, **kwargs): - super(AnsiblePromiseTypeModule, self).__init__( + super(UnavailableAnsiblePromiseTypeModule, self).__init__( "ansible_promise_module", "0.0.0", **kwargs ) - def must_be_absolute(v): - if not os.path.isabs(v): - raise ValidationError( - "Must be an absolute path, not '{v}'".format(v=v) - ) - - self.add_attribute( - "playbook", str, default_to_promiser=True, validator=must_be_absolute - ) - self.add_attribute("inventory", str, validator=must_be_absolute) - self.add_attribute("limit", list, default=["localhost"]) - self.add_attribute("tags", list, default=[]) - self.add_attribute("become", bool, default=False) - self.add_attribute("become_method", str, default="sudo") - self.add_attribute("become_user", str, default="root") - self.add_attribute("connection", str, default="local") - self.add_attribute("forks", int, default=1) - self.add_attribute("private_key_file", str, validator=must_be_absolute) - self.add_attribute("remote_user", str, default="root") - - def prepare_promiser_and_attributes(self, promiser, attributes): - safe_promiser = promiser.replace(",", "_") - return (safe_promiser, attributes) - def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): - return - - def evaluate_promise( - self, promiser: str, attributes: Dict, metadata: Dict - ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(promiser, attributes) - - classes = [] - result = Result.KEPT - - context.CLIARGS = ImmutableDict( - tags=model.tags, - listtags=False, - listtasks=False, - listhosts=False, - syntax=False, - connection=model.connection, - module_path=None, - remote_user=model.remote_user, - private_key_file=model.private_key_file, - ssh_common_args=None, - ssh_extra_args=None, - sftp_extra_args=None, - scp_extra_args=None, - become=model.become, - become_method=model.become_method, - become_user=model.become_user, - forks=model.forks, - verbosity=0, - check=False, - start_at_task=None, - ) + raise ValidationError("Ansible Python module not available") - loader = DataLoader() - inventory = InventoryManager( - loader=loader, - sources=(model.inventory,) if model.inventory else (), - ) + if __name__ == "__main__": + UnavailableAnsiblePromiseTypeModule().start() - variable_manager = VariableManager( - loader=loader, - inventory=inventory, - version_info=CLI.version_info(gitinfo=False), - ) - pbex = PlaybookExecutor( - playbooks=[attributes["playbook"]], - inventory=inventory, - variable_manager=variable_manager, - loader=loader, - passwords={}, + # Nothing below this point can be defined without ansible. + sys.exit(0) + + +class CallbackModule(CallbackBase): + CALLBACK_VERSION = 1.0 + CALLBACK_TYPE = "stdout" + CALLBACK_NAME = "cfengine" + + def __init__(self, *args, promise, **kw): + self.promise = promise + self.hosts = set() + self.changed = False + super(CallbackModule, self).__init__(*args, **kw) + + def v2_runner_on_start(self, host, task): + self.hosts.add(str(host)) + self.promise.log_verbose( + "Task '" + task.name + "' started on '" + str(host) + "'" + ) + + def v2_runner_on_ok(self, result): + is_changed = result.is_changed() + if is_changed: + self.changed = True + self.promise.log_info( + "Task '" + result.task_name + "' successfully changed" ) - callback = CallbackModule(promise=self) - pbex._tqm._stdout_callback = callback # type: ignore + else: + self.promise.log_verbose("Task '" + result.task_name + "' didn't change") - exit_code = pbex.run() - if exit_code != 0: - classes.append("{safe_promiser}_failed".format(safe_promiser=promiser)) - result = Result.NOT_KEPT - elif callback.changed: - result = Result.REPAIRED + def v2_runner_on_failed(self, result, ignore_errors=False): + self.promise.log_error("Task '" + result.task_name + "' failed") - return (result, classes) + def v2_runner_on_skipped(self, result): + self.promise.log_error("Task '" + result.task_name + "' was skipped") - if __name__ == "__main__": - init_plugin_loader() - AnsiblePromiseTypeModule().start() - -except ModuleNotFoundError: - - class UnavailableAnsiblePromiseTypeModule(PromiseModule): - - def __init__(self, **kwargs): - super(UnavailableAnsiblePromiseTypeModule, self).__init__( - "ansible_promise_module", "0.0.0", **kwargs + def v2_playbook_on_stats(self, stats): + for host in self.hosts: + summary_dict = stats.summarize(host) + summary = " ".join( + "%s=%s" % (k, v) for k, v in summary_dict.items() if v > 0 ) + if summary_dict.get("unreachable"): + self.promise.log_error("Host '" + host + "' is unreachable") + elif summary: + self.promise.log_verbose( + "Summary of the tasks for '" + host + "' is: " + summary + ) - def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): - raise ValidationError("Ansible Python module not available") - if __name__ == "__main__": - UnavailableAnsiblePromiseTypeModule().start() +class AnsiblePromiseTypeModule(PromiseModule): + def __init__(self, **kwargs): + super(AnsiblePromiseTypeModule, self).__init__( + "ansible_promise_module", "0.0.0", **kwargs + ) + + def must_be_absolute(v): + if not os.path.isabs(v): + raise ValidationError("Must be an absolute path, not '{v}'".format(v=v)) + + self.add_attribute( + "playbook", str, default_to_promiser=True, validator=must_be_absolute + ) + self.add_attribute("inventory", str, validator=must_be_absolute) + self.add_attribute("limit", list, default=["localhost"]) + self.add_attribute("tags", list, default=[]) + self.add_attribute("become", bool, default=False) + self.add_attribute("become_method", str, default="sudo") + self.add_attribute("become_user", str, default="root") + self.add_attribute("connection", str, default="local") + self.add_attribute("forks", int, default=1) + self.add_attribute("private_key_file", str, validator=must_be_absolute) + self.add_attribute("remote_user", str, default="root") + + def prepare_promiser_and_attributes(self, promiser, attributes): + safe_promiser = promiser.replace(",", "_") + return (safe_promiser, attributes) + + def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): + return + + def evaluate_promise( + self, promiser: str, attributes: Dict, metadata: Dict + ) -> Tuple[str, List[str]]: + model = self.create_attribute_object(promiser, attributes) + + classes = [] + result = Result.KEPT + + context.CLIARGS = ImmutableDict( + tags=model.tags, + listtags=False, + listtasks=False, + listhosts=False, + syntax=False, + connection=model.connection, + module_path=None, + remote_user=model.remote_user, + private_key_file=model.private_key_file, + ssh_common_args=None, + ssh_extra_args=None, + sftp_extra_args=None, + scp_extra_args=None, + become=model.become, + become_method=model.become_method, + become_user=model.become_user, + forks=model.forks, + verbosity=0, + check=False, + start_at_task=None, + ) + + loader = DataLoader() + inventory = InventoryManager( + loader=loader, + sources=(model.inventory,) if model.inventory else (), + ) + + variable_manager = VariableManager( + loader=loader, + inventory=inventory, + version_info=CLI.version_info(gitinfo=False), + ) + pbex = PlaybookExecutor( + playbooks=[attributes["playbook"]], + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + passwords={}, + ) + callback = CallbackModule(promise=self) + pbex._tqm._stdout_callback = callback # type: ignore + + exit_code = pbex.run() + if exit_code != 0: + classes.append("{safe_promiser}_failed".format(safe_promiser=promiser)) + result = Result.NOT_KEPT + elif callback.changed: + result = Result.REPAIRED + + return (result, classes) + + +if __name__ == "__main__": + init_plugin_loader() + AnsiblePromiseTypeModule().start() From 434cc94e591e476b5b0003ea348d464fff78f8f3 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 30 Jul 2026 12:52:00 +0200 Subject: [PATCH 129/146] Install ansible callback so it survives ansible-core 2.19 TaskQueueManager stopped accepting the stdout callback as an object, so ansible loaded its default one instead and printed the play output onto stdout, corrupting the promise module protocol. Ticket: ENT-14326 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 6ff1c520..2107b9ac 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -161,7 +161,11 @@ def evaluate_promise( passwords={}, ) callback = CallbackModule(promise=self) - pbex._tqm._stdout_callback = callback # type: ignore + if hasattr(callback, "_init_callback_methods"): + # Required on ansible-core >= 2.19 after https://github.com/ansible/ansible/pull/85344 + callback._init_callback_methods() + pbex._tqm._callback_plugins = [callback] + pbex._tqm._callbacks_loaded = True exit_code = pbex.run() if exit_code != 0: From bb63b14bde22ed734a96de152eb0fdd3786ebe14 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 30 Jul 2026 13:27:47 +0200 Subject: [PATCH 130/146] Read the ansible playbook attribute from the attribute model It was read from the raw request dict, so the documented "fallback to the promiser" never applied and omitting the attribute raised a KeyError. Ticket: ENT-14326 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 2107b9ac..6f84210a 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -154,7 +154,7 @@ def evaluate_promise( version_info=CLI.version_info(gitinfo=False), ) pbex = PlaybookExecutor( - playbooks=[attributes["playbook"]], + playbooks=[model.playbook], inventory=inventory, variable_manager=variable_manager, loader=loader, From b2ebd29d29bb755098751fe221c90d99da6e6703 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 30 Jul 2026 13:43:00 +0200 Subject: [PATCH 131/146] Apply the ansible limit attribute It was declared and documented, but never read, so setting it did nothing. For backwards compatibility, default to no limit rather than the documented localhost, which would have restricted every existing promise to the local host. Ticket: ENT-14326 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lars Erik Wik --- promise-types/ansible/README.md | 2 +- promise-types/ansible/ansible_promise.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/promise-types/ansible/README.md b/promise-types/ansible/README.md index c6e3cbaf..c3a375ea 100644 --- a/promise-types/ansible/README.md +++ b/promise-types/ansible/README.md @@ -23,7 +23,7 @@ bundle agent main | ------------------ | --------- | ------------------------------------------------------------ | --------- | --------------- | | `playbook` | `string` | Absolute path of the Ansible playbook | No | Promiser | | `inventory` | `string` | Absolute path of the inventory file | No | - | -| `limit` | `slist` | List of host names to target | No | `{"localhost"}` | +| `limit` | `slist` | List of hosts and groups to target | No | `{}` (no limit) | | `tags` | `slist` | List of tags to play | No | `{}` | | `become` | `boolean` | Set the `become` option | No | `False` | | `become_method` | `string` | Set the `become_method` option | No | `"sudo"` | diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 6f84210a..15b4f7aa 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -94,7 +94,7 @@ def must_be_absolute(v): "playbook", str, default_to_promiser=True, validator=must_be_absolute ) self.add_attribute("inventory", str, validator=must_be_absolute) - self.add_attribute("limit", list, default=["localhost"]) + self.add_attribute("limit", list, default=[]) self.add_attribute("tags", list, default=[]) self.add_attribute("become", bool, default=False) self.add_attribute("become_method", str, default="sudo") @@ -147,6 +147,10 @@ def evaluate_promise( loader=loader, sources=(model.inventory,) if model.inventory else (), ) + # An empty subset means no host matches, so leave it alone to target + # everything, the way ansible-playbook does without --limit. + if model.limit: + inventory.subset(model.limit) variable_manager = VariableManager( loader=loader, From 8dbc7e4e8df93f35a96562ac982e0eabd24584e3 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Thu, 30 Jul 2026 13:45:20 +0200 Subject: [PATCH 132/146] Stop reporting skipped ansible tasks as errors A task is skipped when its condition does not match, which means nothing failed. Log it at the same level as a task that ran without changing anything. Ticket: ENT-14326 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 15b4f7aa..5fcd619c 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -64,7 +64,7 @@ def v2_runner_on_failed(self, result, ignore_errors=False): self.promise.log_error("Task '" + result.task_name + "' failed") def v2_runner_on_skipped(self, result): - self.promise.log_error("Task '" + result.task_name + "' was skipped") + self.promise.log_verbose("Task '" + result.task_name + "' was skipped") def v2_playbook_on_stats(self, stats): for host in self.hosts: From 1d150994c1b9a5b881a19724c63f1f2bc7e408e4 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Tue, 11 Aug 2026 11:37:11 -0500 Subject: [PATCH 133/146] ENT-14409: Collect device inventory with fwupdmgr, not fwupd's cache file fwupd rewrites /var/cache/fwupd/devices.json on every engine change signal, including during daemon startup before device enumeration has finished, so it is often zero-length on hosts with dozens of devices. readjson() logged 22 errors per agent run on such a host and the module produced no firmware inventory at all. Also touch the JSON caches when refreshing them. content only writes when the file differs, so an unchanged cache kept its old mtime and re-ran every fwupdmgr call on every agent run once past TTL. Ticket: ENT-14409 Changelog: title Signed-off-by: Nick Anderson --- inventory/inventory-fwupd/policy.cf | 76 +++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index 3662dc74..042ea949 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -4,12 +4,11 @@ # Host Security Identifier (HSI) attribute scores reported by fwupd. # # Implementation notes: -# - Device list is read directly from /var/cache/fwupd/devices.json, which -# fwupd's own systemd timer (fwupd-refresh.timer) keeps up to date. No -# fwupdmgr call is needed for the common steady-state inventory. -# - Pending updates and HSI attributes are not cached as files by fwupd, so -# they are sourced via fwupdmgr and re-fetched only when their respective -# JSON caches age past their TTL. +# - Device list, pending updates and HSI attributes come from fwupdmgr, into +# JSON caches refreshed past their TTL. validjson() checks each payload +# before caching, so readjson() never sees a bad file. +# - fwupd's own /var/cache/fwupd/devices.json is often zero-length, so it is +# not used (ENT-14409). # - All inventory variables are emitted via a single module-protocol cache # rendered from JSON with an inline_mustache template. The cache is # consumed in one call to read_module_protocol(), which loads dozens of @@ -36,7 +35,7 @@ bundle agent inventory_fwupd_main ); "_fwupd_dir" string => "$(sys.statedir)/fwupd"; - "_devices_src" string => "/var/cache/fwupd/devices.json"; + "_devices_src" string => "$(_fwupd_dir)/inventory_devices.json"; "_updates_src" string => "$(_fwupd_dir)/inventory_updates.json"; "_security_src" string => "$(_fwupd_dir)/inventory_security.json"; "_cache" string => "$(_fwupd_dir)/inventory_cache"; @@ -44,6 +43,7 @@ bundle agent inventory_fwupd_main # TTLs in seconds for the fwupdmgr-derived JSON caches. # 12 h: + "_devices_ttl" string => "43200"; "_updates_ttl" string => "43200"; # 24 h: @@ -80,6 +80,9 @@ bundle agent inventory_fwupd_main "0" ); + "_devices_age" + string => eval("$(sys.systime) - $(_devices_mtime)", "math", "infix"); + "_updates_age" string => eval("$(sys.systime) - $(_updates_mtime)", "math", "infix"); @@ -127,6 +130,17 @@ bundle agent inventory_fwupd_main ); linux.have_fwupdmgr:: + "_devices_stale" not => fileexists("$(_devices_src)"); + + "_devices_stale" + expression => isgreaterthan("$(_devices_age)", "$(_devices_ttl)"), + if => fileexists("$(_devices_src)"); + + # Firmware updates activate on boot, so device versions change then. + "_devices_stale" + expression => islessthan("$(_devices_mtime)", "$(_boot_mtime)"), + if => fileexists("$(_devices_src)"); + "_updates_stale" not => fileexists("$(_updates_src)"); "_updates_stale" @@ -172,13 +186,24 @@ bundle agent inventory_fwupd_main }; vars: + linux.have_fwupdmgr._devices_stale:: + "_devices_raw" + string => execresult("$(fwupdmgr) get-devices --json", "noshell"); + + "_devices_payload" + string => ifelse( + validjson("$(_devices_raw)", "true"), + "$(_devices_raw)", + '{"Devices":[]}' + ); + linux.have_fwupdmgr._updates_stale:: "_updates_raw" string => execresult("$(fwupdmgr) get-updates --json", "noshell"); "_updates_payload" string => ifelse( - regcmp("\s*\{.*", "$(_updates_raw)"), + validjson("$(_updates_raw)", "true"), "$(_updates_raw)", '{"Devices":[]}' ); @@ -189,7 +214,7 @@ bundle agent inventory_fwupd_main "_security_payload" string => ifelse( - regcmp("\s*\{.*", "$(_security_raw)"), + validjson("$(_security_raw)", "true"), "$(_security_raw)", '{"SecurityAttributes":[]}' ); @@ -198,11 +223,23 @@ bundle agent inventory_fwupd_main linux:: "$(_fwupd_dir)/." create => "true"; + # The mtime is the TTL clock. content only writes when the file differs, + # so without touch an unchanged cache stays stale and fwupdmgr runs + # again on every agent run. + linux.have_fwupdmgr._devices_stale:: + "$(_devices_src)" + content => "$(_devices_payload)", + touch => "true"; + linux.have_fwupdmgr._updates_stale:: - "$(_updates_src)" content => "$(_updates_payload)"; + "$(_updates_src)" + content => "$(_updates_payload)", + touch => "true"; linux.have_fwupdmgr._security_stale:: - "$(_security_src)" content => "$(_security_payload)"; + "$(_security_src)" + content => "$(_security_payload)", + touch => "true"; methods: linux._have_devices._rebuild_cache:: @@ -241,12 +278,13 @@ bundle agent inventory_fwupd_main linux._have_devices:: "_cache_was_read" if => read_module_protocol("$(_cache)"); - linux.!_have_devices:: - # No source file from fwupd - emit a single status attribute. + linux.!have_fwupdmgr:: + # vars run before classes, so guarding the variable on !have_fwupdmgr + # directly would set it on pass 1, before that class is evaluated. "_emit_missing_status"; vars: - linux.!_have_devices._emit_missing_status:: + linux._emit_missing_status:: "fwupd_status" string => "FWUPD_MISSING", meta => { "inventory", "attribute_name=Firmware update status" }; @@ -268,7 +306,11 @@ bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, temp vars: "_template_body" string => readfile("$(template)", "inf"); "_devices_json" data => readjson("$(devices_src)"); - "_dev_count" int => length("_devices_json[Devices]"); + + # fwupdmgr emits {} when it tracks no devices. + "_dev_count" + int => length("_devices_json[Devices]"), + if => isvariable("_devices_json[Devices]"); "_updates_json" data => readjson("$(updates_src)"), @@ -283,6 +325,10 @@ bundle agent inventory_fwupd_render(devices_src, updates_src, security_src, temp if => fileexists("$(security_src)"); # Default counters when fwupdmgr-derived data is not present. + "_dev_count" + int => "0", + if => not(isvariable("_dev_count")); + "_updates_count" int => "0", if => not(isvariable("_updates_count")); From 5c5339a19485e0c629b3d388327136ec133555db Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 12 Aug 2026 13:42:55 -0500 Subject: [PATCH 134/146] Fixed module screenshots not rendering on build.cfengine.com The site publishes only the README, so relative file: links resolved to 404s under /modules//. Use absolute raw.githubusercontent.com URLs, as the other modules do. --- inventory/inventory-fwupd/README.org | 4 ++-- management/manage-fwupd/README.org | 2 +- reporting/compliance-report-fwupd/README.org | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inventory/inventory-fwupd/README.org b/inventory/inventory-fwupd/README.org index 41c4142e..ac5c80d2 100644 --- a/inventory/inventory-fwupd/README.org +++ b/inventory/inventory-fwupd/README.org @@ -19,7 +19,7 @@ devices matching an allow-list. The inventory attributes appear in Mission Portal's column selector under "fwupd": -[[file:mp-inventory-fwupd-columns.png]] +[[https://raw.githubusercontent.com/cfengine/modules/master/inventory/inventory-fwupd/mp-inventory-fwupd-columns.png]] * Inventory Attributes @@ -85,7 +85,7 @@ These appear in Mission Portal monitoring graphs as =firmware_devices_total=, =firmware_updates_available=, and =firmware_hsi_failing=. -[[file:mp-monitoring-firmware-devices.png]] +[[https://raw.githubusercontent.com/cfengine/modules/master/inventory/inventory-fwupd/mp-monitoring-firmware-devices.png]] * Classes diff --git a/management/manage-fwupd/README.org b/management/manage-fwupd/README.org index 9d731270..009fd20c 100644 --- a/management/manage-fwupd/README.org +++ b/management/manage-fwupd/README.org @@ -95,7 +95,7 @@ first. The module reports =fwupd management status= as an inventory attribute visible in Mission Portal's inventory reports: -[[file:mp-inventory-fwupd-status.png]] +[[https://raw.githubusercontent.com/cfengine/modules/master/management/manage-fwupd/mp-inventory-fwupd-status.png]] * Behavior diff --git a/reporting/compliance-report-fwupd/README.org b/reporting/compliance-report-fwupd/README.org index 6a2e6122..3b1eb1f6 100644 --- a/reporting/compliance-report-fwupd/README.org +++ b/reporting/compliance-report-fwupd/README.org @@ -10,7 +10,7 @@ checks covering every individual HSI firmware security test. * Mission Portal -[[file:mp-compliance-report-fwupd.png]] +[[https://raw.githubusercontent.com/cfengine/modules/master/reporting/compliance-report-fwupd/mp-compliance-report-fwupd.png]] * Requirements From 050d2cc2314e69d36251942120b09153cc219bb0 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 12 Aug 2026 15:14:26 -0500 Subject: [PATCH 135/146] Replaced per-check HSI inventory attributes with pass/fail lists fwupd's security check set is hardware dependent, so one inventory attribute per check grew the fleet-wide attribute list with every distinct chipset while each host filled in only its own subset. Two slists keep the module at two stable attribute names regardless of the hardware, and carry the same information: a check in neither list is not applicable to that host. compliance-report-fwupd's per-check conditions are rewritten to match an item against "Firmware HSI failing", so the two modules must be released together. Changelog: title --- inventory/inventory-fwupd/README.org | 25 +-- inventory/inventory-fwupd/policy.cf | 55 +++--- reporting/compliance-report-fwupd/README.org | 157 ++++++++-------- .../fwupd-compliance-report.json | 168 +++++++++--------- 4 files changed, 210 insertions(+), 195 deletions(-) diff --git a/inventory/inventory-fwupd/README.org b/inventory/inventory-fwupd/README.org index ac5c80d2..03199461 100644 --- a/inventory/inventory-fwupd/README.org +++ b/inventory/inventory-fwupd/README.org @@ -46,30 +46,31 @@ For every device fwupd reports (keyed by DeviceId): | Attribute | Format | |---------------------------------+------------------------------------------------------------| | *Firmware HSI level* | =HSI:0= through =HSI:4= | -| *Firmware HSI L: * | =PASS= or =FAIL= (one per security check) | -| *Firmware HSI attributes* | =Name (HSI L): [PASS|FAIL]= (slist) | +| *Firmware HSI failing* | =L: = (slist) | +| *Firmware HSI passing* | =L: = (slist) | *Firmware HSI level* is the rolled-up Host Security Identifier level. fwupd walks levels 1--4 sequentially; the result is the highest level where all attributes pass, stopping at the first level with any failure. -*Firmware HSI L: * variables (e.g. =Firmware HSI L1: TPM v2.0=) -are individual string attributes with value =PASS= or =FAIL=. These are -consumed by *compliance-report-fwupd* for per-check compliance conditions. -Two normalizations are applied to keep inventory attribute names stable -and aligned with the HSI specification: +*Firmware HSI failing* and *Firmware HSI passing* are slists of the +security checks fwupd reported, split by outcome (e.g. =L1: TPM v2.0=). +The check set is hardware dependent, so a check in neither list is not +applicable to this host. *compliance-report-fwupd* matches an exact item +against *Firmware HSI failing* for its per-check conditions. + +Items keep the =L:= prefix because the name alone is not unique: +fwupd reports two distinct checks named =Platform debugging=, at L1 +(Intel DCI) and L2 (debug locked). Two normalizations keep items stable: - *Name normalization:* The CSME version attribute is emitted as - =Firmware HSI L1: CSME version= regardless of the firmware version - string fwupd reports (which varies per host). + =L1: CSME version= regardless of the firmware version string fwupd + reports (which varies per host). - *Level normalization:* fwupd marks some runtime checks at HsiLevel 0 even though they contribute to scored HSI levels. The module maps these to their specification levels: =UEFI secure boot= is emitted at L1 (not L0) and =CET OS Support= at L3 (not L0). -*Firmware HSI attributes* is an slist with one detailed entry per -security check, useful for drill-down in Mission Portal inventory views. - * Measurements The following values are emitted as =cf-monitord= measurements for diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index 042ea949..beb2339f 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -383,10 +383,8 @@ bundle agent inventory_fwupd_hsi(security_src) # compute the rolled-up HSI level. # # Emits: -# - Per-attribute PASS/FAIL strings named "Firmware HSI L: " -# for use in compliance report conditions. -# - An slist of detailed results formatted "Name (HSI L): -# [PASS|FAIL]" for inventory drill-down. +# - "Firmware HSI failing" and "Firmware HSI passing", slists of +# "L: " check names split by outcome. # - A count of failing attributes. # - The rolled-up HSI level (e.g. "HSI:3"). # @@ -444,10 +442,9 @@ bundle agent inventory_fwupd_hsi(security_src) "$(_name[$(_idxes)])" ); - # Normalized level for per-attribute inventory: fwupd marks some - # attributes as HsiLevel 0 with a "runtime-issue" flag even though - # they contribute to scored HSI levels. Map them to their HSI spec - # level so inventory attribute names match compliance conditions. + # Normalized level: fwupd marks some attributes as HsiLevel 0 with a + # "runtime-issue" flag even though they contribute to scored HSI + # levels. Map them to their HSI spec level so list items are stable. # UEFI secure boot (org.fwupd.hsi.Uefi.SecureBoot) → HSI:1 # CET OS Support (org.fwupd.hsi.IntelCet.Active) → HSI:3 "_inv_level[$(_idxes)]" @@ -459,19 +456,35 @@ bundle agent inventory_fwupd_hsi(security_src) "$(_level[$(_idxes)])" ); - # Per-attribute inventory: one string variable per HSI check, - # named "Firmware HSI L: " with value PASS or FAIL. - # This enables per-check compliance report conditions. - "fwupd_hsi_check[$(_idxes)]" - string => "$(_status[$(_idxes)])", - meta => { - "inventory", - "attribute_name=Firmware HSI L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])", - }; - - "fwupd_hsi_attr[$(_idxes)]" - string => "$(_name[$(_idxes)]) (HSI L$(_level[$(_idxes)])): $(_result[$(_idxes)]) [$(_status[$(_idxes)])]", - meta => { "inventory", "attribute_name=Firmware HSI attributes" }; + # Check names split by outcome into two lists, rather than one + # inventory attribute per check. + # + # Inventory attribute names are fleet wide: an attribute any host + # defines is offered as a column for every host. fwupd's check set + # is hardware dependent, so one attribute per check grew that list + # with every distinct chipset in the fleet while each host filled in + # only its own subset. Two lists keep this module at two stable + # attribute names regardless of the hardware, and carry the same + # information: a check in neither list is not applicable to this + # host. + # + # The level prefix disambiguates fwupd's two distinct checks both + # named "Platform debugging" (L1 Intel DCI, L2 debug locked). + "_failing_name[$(_idxes)]" + string => "L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])", + if => strcmp("$(_status[$(_idxes)])", "FAIL"); + + "_passing_name[$(_idxes)]" + string => "L$(_inv_level[$(_idxes)]): $(_inv_name[$(_idxes)])", + if => strcmp("$(_status[$(_idxes)])", "PASS"); + + "fwupd_hsi_failing" + slist => getvalues("_failing_name"), + meta => { "inventory", "attribute_name=Firmware HSI failing" }; + + "fwupd_hsi_passing" + slist => getvalues("_passing_name"), + meta => { "inventory", "attribute_name=Firmware HSI passing" }; "fwupd_hsi_failing_count" int => countclassesmatching("inventory_fwupd_hsi_failing_[0-9]+"); diff --git a/reporting/compliance-report-fwupd/README.org b/reporting/compliance-report-fwupd/README.org index 3b1eb1f6..726872c0 100644 --- a/reporting/compliance-report-fwupd/README.org +++ b/reporting/compliance-report-fwupd/README.org @@ -22,15 +22,15 @@ checks covering every individual HSI firmware security test. * Rolled-up Conditions -| Condition | Category | Severity | -|-----------------------------------+--------------------+----------| -| *fwupd installed* | Firmware tooling | high | -| *HSI Level 1+ (Critical)* | HSI Level Overview | high | -| *HSI Level 2+ (Important)* | HSI Level Overview | medium | -| *HSI Level 3+ (Recommended)* | HSI Level Overview | low | -| *HSI Level 4 (Complete)* | HSI Level Overview | low | -| *No pending firmware updates* | Firmware updates | medium | -| *Firmware status healthy* | Firmware updates | medium | +| Condition | Category | Severity | +|-------------------------------+--------------------+----------| +| *fwupd installed* | Firmware tooling | high | +| *HSI Level 1+ (Critical)* | HSI Level Overview | high | +| *HSI Level 2+ (Important)* | HSI Level Overview | medium | +| *HSI Level 3+ (Recommended)* | HSI Level Overview | low | +| *HSI Level 4 (Complete)* | HSI Level Overview | low | +| *No pending firmware updates* | Firmware updates | medium | +| *Firmware status healthy* | Firmware updates | medium | HSI level checks are cumulative thresholds -- a host at HSI:3 passes the Level 1+, 2+, and 3+ conditions but fails Level 4. @@ -38,100 +38,101 @@ the Level 1+, 2+, and 3+ conditions but fails Level 4. * Per-Attribute Conditions Each individual HSI firmware security check is a separate compliance -condition. The inventory attribute name follows the pattern -=Firmware HSI L: = with value =PASS= or =FAIL=. +condition. Every one matches an exact item against the +*Firmware HSI failing* slist from *inventory-fwupd*, whose items follow +the pattern =L: =. Per-attribute conditions use ~condition_for: "failing"~ -- a host is -only marked failing when the attribute is explicitly =FAIL=. Hosts -that don't report a given attribute (e.g., Intel-only checks on AMD -hardware, or VMs without HSI data) show as "not evaluated" rather -than failing. The checks are defined by the +only marked failing when the check appears in that list. Hosts that +don't report a given check (e.g., Intel-only checks on AMD hardware, or +VMs without HSI data) show as "not evaluated" rather than failing, +because the check appears in neither the failing nor the passing list. The checks are defined by the [[https://fwupd.github.io/libfwupdplugin/hsi.html][fwupd HSI specification]]. ** Level 1 -- Critical (19 conditions, severity: high) -| Condition | Inventory attribute | Platform | -|------------------------------+--------------------------------------------+----------| -| UEFI SecureBoot | =Firmware HSI L1: UEFI secure boot= | All | -| TPM 2.0 Present | =Firmware HSI L1: TPM v2.0= | All | -| Empty PCR in TPM | =Firmware HSI L1: TPM empty PCRs= | All | -| UEFI Platform Key | =Firmware HSI L1: UEFI platform key= | All | -| BIOS Capsule Updates | =Firmware HSI L1: BIOS firmware updates= | All | -| Supported CPU | =Firmware HSI L1: Supported CPU= | All | -| UEFI BootService Variables | =Firmware HSI L1: UEFI bootservice variables= | All | -| BIOS Write Enable (BWE) | =Firmware HSI L1: SPI write= | Intel | -| BIOS Lock Enable (BLE) | =Firmware HSI L1: SPI lock= | Intel | -| SMM BIOS Write Protect | =Firmware HSI L1: SPI BIOS region= | Intel | -| Read-only SPI Descriptor | =Firmware HSI L1: SPI descriptor= | Intel | -| Platform Debug (Intel DCI) | =Firmware HSI L1: Platform debugging= | Intel | -| ME Manufacturing Mode | =Firmware HSI L1: csme manufacturing mode= | Intel | -| ME Flash Descriptor Override | =Firmware HSI L1: csme override= | Intel | -| ME BootGuard Platform Key | =Firmware HSI L1: MEI key manifest= | Intel | -| CSME Version | =Firmware HSI L1: CSME version= | Intel | -| Part is Fused | =Firmware HSI L1: Part is fused= | Intel | -| AMD Microcode Signature | =Firmware HSI L1: AMD microcode signature= | AMD | -| SMM Locked Down | =Firmware HSI L1: SMM locked down= | AMD | +| Condition | List item | Platform | +|------------------------------+----------------------------------+----------| +| UEFI SecureBoot | =L1: UEFI secure boot= | All | +| TPM 2.0 Present | =L1: TPM v2.0= | All | +| Empty PCR in TPM | =L1: TPM empty PCRs= | All | +| UEFI Platform Key | =L1: UEFI platform key= | All | +| BIOS Capsule Updates | =L1: BIOS firmware updates= | All | +| Supported CPU | =L1: Supported CPU= | All | +| UEFI BootService Variables | =L1: UEFI bootservice variables= | All | +| BIOS Write Enable (BWE) | =L1: SPI write= | Intel | +| BIOS Lock Enable (BLE) | =L1: SPI lock= | Intel | +| SMM BIOS Write Protect | =L1: SPI BIOS region= | Intel | +| Read-only SPI Descriptor | =L1: SPI descriptor= | Intel | +| Platform Debug (Intel DCI) | =L1: Platform debugging= | Intel | +| ME Manufacturing Mode | =L1: csme manufacturing mode= | Intel | +| ME Flash Descriptor Override | =L1: csme override= | Intel | +| ME BootGuard Platform Key | =L1: MEI key manifest= | Intel | +| CSME Version | =L1: CSME version= | Intel | +| Part is Fused | =L1: Part is fused= | Intel | +| AMD Microcode Signature | =L1: AMD microcode signature= | AMD | +| SMM Locked Down | =L1: SMM locked down= | AMD | ** Level 2 -- Important (12 conditions, severity: medium) -| Condition | Inventory attribute | Platform | -|----------------------------+-------------------------------------------------+----------| -| DMA Protection (IOMMU) | =Firmware HSI L2: IOMMU= | All | -| PCR0 TPM Event Log | =Firmware HSI L2: TPM PCR0 reconstruction= | All | -| BIOS Rollback Protection | =Firmware HSI L2: BIOS rollback protection= | All | -| Intel BootGuard Enabled | =Firmware HSI L2: Intel BootGuard= | Intel | -| Intel BootGuard Verified | =Firmware HSI L2: Intel BootGuard verified boot= | Intel | -| Intel BootGuard ACM | =Firmware HSI L2: Intel BootGuard ACM protected= | Intel | -| Intel BootGuard OTP | =Firmware HSI L2: Intel BootGuard OTP fuse= | Intel | -| Part is Debug Locked | =Firmware HSI L2: Platform debugging= | Intel | -| Intel GDS Mitigation | =Firmware HSI L2: Intel GDS mitigation= | Intel | -| AMD Platform Secure Boot | =Firmware HSI L2: AMD platform secure boot= | AMD | -| AMD SPI Write Protections | =Firmware HSI L2: AMD SPI write protections= | AMD | -| HP SureStart | =Firmware HSI L2: HP SureStart= | HP | +| Condition | List item | Platform | +|---------------------------+-------------------------------------+----------| +| DMA Protection (IOMMU) | =L2: IOMMU= | All | +| PCR0 TPM Event Log | =L2: TPM PCR0 reconstruction= | All | +| BIOS Rollback Protection | =L2: BIOS rollback protection= | All | +| Intel BootGuard Enabled | =L2: Intel BootGuard= | Intel | +| Intel BootGuard Verified | =L2: Intel BootGuard verified boot= | Intel | +| Intel BootGuard ACM | =L2: Intel BootGuard ACM protected= | Intel | +| Intel BootGuard OTP | =L2: Intel BootGuard OTP fuse= | Intel | +| Part is Debug Locked | =L2: Platform debugging= | Intel | +| Intel GDS Mitigation | =L2: Intel GDS mitigation= | Intel | +| AMD Platform Secure Boot | =L2: AMD platform secure boot= | AMD | +| AMD SPI Write Protections | =L2: AMD SPI write protections= | AMD | +| HP SureStart | =L2: HP SureStart= | HP | ** Level 3 -- Recommended (8 conditions, severity: low) -| Condition | Inventory attribute | Platform | -|------------------------------------+----------------------------------------------+----------| -| Suspend-to-Idle | =Firmware HSI L3: Suspend-to-idle= | All | -| Suspend to RAM Disabled | =Firmware HSI L3: Suspend-to-ram= | All | -| Pre-boot DMA Protection | =Firmware HSI L3: Pre-boot DMA protection= | All | -| CET Available | =Firmware HSI L3: CET Platform= | All | -| CET Utilized by OS | =Firmware HSI L3: CET OS Support= | All | -| Early-boot UEFI Memory Protections | =Firmware HSI L3: UEFI memory protections= | All | -| Intel BootGuard Policy | =Firmware HSI L3: Intel BootGuard error policy= | Intel | -| AMD SPI Replay Protections | =Firmware HSI L3: AMD SPI replay protections= | AMD | +| Condition | List item | Platform | +|------------------------------------+------------------------------------+----------| +| Suspend-to-Idle | =L3: Suspend-to-idle= | All | +| Suspend to RAM Disabled | =L3: Suspend-to-ram= | All | +| Pre-boot DMA Protection | =L3: Pre-boot DMA protection= | All | +| CET Available | =L3: CET Platform= | All | +| CET Utilized by OS | =L3: CET OS Support= | All | +| Early-boot UEFI Memory Protections | =L3: UEFI memory protections= | All | +| Intel BootGuard Policy | =L3: Intel BootGuard error policy= | Intel | +| AMD SPI Replay Protections | =L3: AMD SPI replay protections= | AMD | ** Level 4 -- Complete (3 conditions, severity: low) -| Condition | Inventory attribute | Platform | -|-------------------------------+--------------------------------------------+----------| -| DRAM Memory Encryption | =Firmware HSI L4: Encrypted RAM= | All | -| SMAP | =Firmware HSI L4: SMAP= | All | -| AMD Secure Processor Rollback | =Firmware HSI L4: AMD rollback protection= | AMD | +| Condition | List item | Platform | +|-------------------------------+-------------------------------+----------| +| DRAM Memory Encryption | =L4: Encrypted RAM= | All | +| SMAP | =L4: SMAP= | All | +| AMD Secure Processor Rollback | =L4: AMD rollback protection= | AMD | * Categories | Category | Conditions | Scope | |---------------------------+------------+----------------------------| -| HSI Level Overview | 4 | Rolled-up level thresholds | -| HSI Level 1 - Critical | 19 | Per-attribute checks | -| HSI Level 2 - Important | 12 | Per-attribute checks | -| HSI Level 3 - Recommended | 8 | Per-attribute checks | -| HSI Level 4 - Complete | 3 | Per-attribute checks | -| Firmware updates | 2 | Update status | -| Firmware tooling | 1 | fwupd installation | +| HSI Level Overview | 4 | Rolled-up level thresholds | +| HSI Level 1 - Critical | 19 | Per-attribute checks | +| HSI Level 2 - Important | 12 | Per-attribute checks | +| HSI Level 3 - Recommended | 8 | Per-attribute checks | +| HSI Level 4 - Complete | 3 | Per-attribute checks | +| Firmware updates | 2 | Update status | +| Firmware tooling | 1 | fwupd installation | * Platform support Linux only. Platform-specific conditions use =host_filter= class expressions so they only activate on relevant hardware: -| Platform | =host_filter= | Conditions | Detection | -|----------+----------------------------+------------+--------------------------------| -| All | =linux= | 25 | -- | -| Intel | =fwupd_cpu_vendor_intel= | 17 | =/proc/cpuinfo= vendor_id | -| AMD | =fwupd_cpu_vendor_amd= | 6 | =/proc/cpuinfo= vendor_id | -| HP | =fwupd_oem_vendor_hp= | 1 | =/sys/class/dmi/id/sys_vendor= | +| Platform | =host_filter= | Conditions | Detection | +|----------+--------------------------+------------+--------------------------------| +| All | =linux= | 25 | -- | +| Intel | =fwupd_cpu_vendor_intel= | 17 | =/proc/cpuinfo= vendor_id | +| AMD | =fwupd_cpu_vendor_amd= | 6 | =/proc/cpuinfo= vendor_id | +| HP | =fwupd_oem_vendor_hp= | 1 | =/sys/class/dmi/id/sys_vendor= | These classes are defined by the *inventory-fwupd* module. diff --git a/reporting/compliance-report-fwupd/fwupd-compliance-report.json b/reporting/compliance-report-fwupd/fwupd-compliance-report.json index d4dd0219..f2a83477 100644 --- a/reporting/compliance-report-fwupd/fwupd-compliance-report.json +++ b/reporting/compliance-report-fwupd/fwupd-compliance-report.json @@ -185,9 +185,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: UEFI secure boot", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: UEFI secure boot" } ], "category": "HSI Level 1 - Critical", @@ -202,9 +202,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: TPM v2.0", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: TPM v2.0" } ], "category": "HSI Level 1 - Critical", @@ -219,9 +219,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: TPM empty PCRs", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: TPM empty PCRs" } ], "category": "HSI Level 1 - Critical", @@ -236,9 +236,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: UEFI platform key", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: UEFI platform key" } ], "category": "HSI Level 1 - Critical", @@ -253,9 +253,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: BIOS firmware updates", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: BIOS firmware updates" } ], "category": "HSI Level 1 - Critical", @@ -270,9 +270,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: SPI write", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: SPI write" } ], "category": "HSI Level 1 - Critical", @@ -287,9 +287,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: SPI lock", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: SPI lock" } ], "category": "HSI Level 1 - Critical", @@ -304,9 +304,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: SPI BIOS region", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: SPI BIOS region" } ], "category": "HSI Level 1 - Critical", @@ -321,9 +321,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: SPI descriptor", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: SPI descriptor" } ], "category": "HSI Level 1 - Critical", @@ -338,9 +338,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: Supported CPU", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: Supported CPU" } ], "category": "HSI Level 1 - Critical", @@ -355,9 +355,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: Platform debugging", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: Platform debugging" } ], "category": "HSI Level 1 - Critical", @@ -372,9 +372,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: csme manufacturing mode", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: csme manufacturing mode" } ], "category": "HSI Level 1 - Critical", @@ -389,9 +389,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: csme override", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: csme override" } ], "category": "HSI Level 1 - Critical", @@ -406,9 +406,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: MEI key manifest", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: MEI key manifest" } ], "category": "HSI Level 1 - Critical", @@ -423,9 +423,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: CSME version", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: CSME version" } ], "category": "HSI Level 1 - Critical", @@ -440,9 +440,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: UEFI bootservice variables", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: UEFI bootservice variables" } ], "category": "HSI Level 1 - Critical", @@ -457,9 +457,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: AMD microcode signature", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: AMD microcode signature" } ], "category": "HSI Level 1 - Critical", @@ -474,9 +474,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: Part is fused", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: Part is fused" } ], "category": "HSI Level 1 - Critical", @@ -491,9 +491,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L1: SMM locked down", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L1: SMM locked down" } ], "category": "HSI Level 1 - Critical", @@ -508,9 +508,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: IOMMU", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: IOMMU" } ], "category": "HSI Level 2 - Important", @@ -525,9 +525,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Intel BootGuard", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Intel BootGuard" } ], "category": "HSI Level 2 - Important", @@ -542,9 +542,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Intel BootGuard verified boot", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Intel BootGuard verified boot" } ], "category": "HSI Level 2 - Important", @@ -559,9 +559,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Intel BootGuard ACM protected", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Intel BootGuard ACM protected" } ], "category": "HSI Level 2 - Important", @@ -576,9 +576,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Intel BootGuard OTP fuse", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Intel BootGuard OTP fuse" } ], "category": "HSI Level 2 - Important", @@ -593,9 +593,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: TPM PCR0 reconstruction", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: TPM PCR0 reconstruction" } ], "category": "HSI Level 2 - Important", @@ -610,9 +610,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: BIOS rollback protection", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: BIOS rollback protection" } ], "category": "HSI Level 2 - Important", @@ -627,9 +627,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Platform debugging", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Platform debugging" } ], "category": "HSI Level 2 - Important", @@ -644,9 +644,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: Intel GDS mitigation", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: Intel GDS mitigation" } ], "category": "HSI Level 2 - Important", @@ -661,9 +661,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: AMD platform secure boot", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: AMD platform secure boot" } ], "category": "HSI Level 2 - Important", @@ -678,9 +678,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: AMD SPI write protections", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: AMD SPI write protections" } ], "category": "HSI Level 2 - Important", @@ -695,9 +695,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L2: HP SureStart", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L2: HP SureStart" } ], "category": "HSI Level 2 - Important", @@ -712,9 +712,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: Suspend-to-idle", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: Suspend-to-idle" } ], "category": "HSI Level 3 - Recommended", @@ -729,9 +729,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: Suspend-to-ram", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: Suspend-to-ram" } ], "category": "HSI Level 3 - Recommended", @@ -746,9 +746,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: Pre-boot DMA protection", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: Pre-boot DMA protection" } ], "category": "HSI Level 3 - Recommended", @@ -763,9 +763,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: Intel BootGuard error policy", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: Intel BootGuard error policy" } ], "category": "HSI Level 3 - Recommended", @@ -780,9 +780,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: CET Platform", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: CET Platform" } ], "category": "HSI Level 3 - Recommended", @@ -797,9 +797,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: AMD SPI replay protections", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: AMD SPI replay protections" } ], "category": "HSI Level 3 - Recommended", @@ -814,9 +814,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: UEFI memory protections", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: UEFI memory protections" } ], "category": "HSI Level 3 - Recommended", @@ -831,9 +831,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L4: Encrypted RAM", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L4: Encrypted RAM" } ], "category": "HSI Level 4 - Complete", @@ -848,9 +848,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L4: SMAP", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L4: SMAP" } ], "category": "HSI Level 4 - Complete", @@ -865,9 +865,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L3: CET OS Support", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L3: CET OS Support" } ], "category": "HSI Level 3 - Recommended", @@ -882,9 +882,9 @@ "condition_for": "failing", "rules": [ { - "attribute": "Firmware HSI L4: AMD rollback protection", + "attribute": "Firmware HSI failing", "operator": "matches", - "value": "FAIL" + "value": "L4: AMD rollback protection" } ], "category": "HSI Level 4 - Complete", From 1fa43fb4ab30074ad33d67776140182082a06acb Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 12 Aug 2026 16:55:06 -0500 Subject: [PATCH 136/146] Defaulted missing HsiLevel to 0 so runtime checks are still reported Changelog: title --- inventory/inventory-fwupd/policy.cf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/inventory/inventory-fwupd/policy.cf b/inventory/inventory-fwupd/policy.cf index beb2339f..0bcdb8df 100644 --- a/inventory/inventory-fwupd/policy.cf +++ b/inventory/inventory-fwupd/policy.cf @@ -417,6 +417,13 @@ bundle agent inventory_fwupd_hsi(security_src) "_level[$(_idxes)]" string => "$(_sec[SecurityAttributes][$(_idxes)][HsiLevel])"; + # fwupd omits HsiLevel on runtime-issue checks (Linux kernel, UEFI db, + # ...). Default those to L0, otherwise the unresolved level skips + # every promise that builds on it and the check vanishes entirely. + "_level[$(_idxes)]" + string => "0", + unless => isvariable("_level[$(_idxes)]"); + "_result[$(_idxes)]" string => "$(_sec[SecurityAttributes][$(_idxes)][HsiResult])"; From fc794e630d3d17e1861650396d24fbbf417f5488 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Wed, 12 Aug 2026 16:55:22 -0500 Subject: [PATCH 137/146] Updated inventory columns screenshot for the HSI list attributes Changelog: title --- .../mp-inventory-fwupd-columns.png | Bin 79973 -> 109695 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/inventory/inventory-fwupd/mp-inventory-fwupd-columns.png b/inventory/inventory-fwupd/mp-inventory-fwupd-columns.png index 194d80c9298a2d6a9737b5e6b81c75780a2b6b77..a330441d524ce037c893bc31f7b83c464dade93d 100644 GIT binary patch literal 109695 zcmce-XE>Z)7dAX;NOaMnCPS1UI)e!zI*||=j269&-g}BRdh|}B3qzFAOZ48&C}Z@J zU%Nmft@np zV6b@-$2X1Z|Gl732}%4=-4A4$Dn79G+np^~4C&ylDx!C(*ZqUu0X720yYS$TK7)__ zi1GK<+2)@rYFeri3bP(yK`tjB^Z#>yHa5-4@q+aAoIEl`PQ?_}8Zj2ddmxiLjMCSB zK7GrR&e~^I$k^@%a*0D*Q6r-AsHl^OKrPVi7m5`{Q;+@Je*gJHlhgc$=3vA|ssDBN zDH96x&hS|r(L3T(zJcAQ936^0GSE-(Hm*GB1R8fwj|Sw^NaYEYzRw9$6h**biWIrn zfbm6Y>hR;;vEUgVBdKd9G~XvcKfl=$gg7E8KsN8v(O!{Y6>ZSBp^wN0^-3)Lqj`?J z>2=(|{l}AGhK(NKWnMkKK9grmz5Sb<4~r!D z9rt`X{$t6_Pcw3dK=R&~;O)+u;YQeIi0x=1IF_PF5#B`P&QWQ;QOC#E?=DtN+{F0BOIE94x<|Y& z1KuUu5&3i4)2PAYB$l>jQJd&Ah~Ry> z7$nQZMLnl^qQ{4y-n{ssTB96)ZGrey#KZipg4%;bMJF=O0Zz>gcy69){# zP(0nE-4UwbDoNAFZbJ0WVlTNZH$I)aG5_`ge2DE0e#teg04`)gKZ*+|g+K z49Q>kNIy7?C#Mz&v1ju{tjiS8-g~Ad-)te0f-KpdP){t0HX6q0q}ee}1lckl&#$h6 za~t$Jhnt&Q)fnwj`*lGaVD-{I_U5djxnpKY7j|IhEmt5&hp}mJRMR-2*VO@~ln(l) z;XP0V*5i^vVD{ExJZaP&uZ$Y9qi!SbAXs3R9_0p(x(uWUg~a>poZlKr#i%BfGwo&l zDbQbNA-V@8dr|pwuW7~Cd*88ms$?^jkDF&p%oos!QVSb=me9th@?9jYPorKHkA^-N zQCVe(a=hwO3zE@-rO!^nP@YPRIp#7b zH$Aztg%R{~Qjsamowp*SxnfdLSO1x2#%tXI_FM;eRmO3U2%FU_utL(cWhfC-h%L*j zkwnOvHb39$j(lx@1E@dsduluq3{&tl#s=QWM(IQkZO|4gFG zC^FxTf7-=kO%0CkOdCO=@_058QVMzcyoJ^)VTW^x6U1D3`7eZPkkE^TqZ+*t(@rgX zKOy%zBs20)#0?tIvbns2~#`9*(M~c29Lj^5@x*`C`i&#&z>i$!9FT1 zN7s1GSK)Yj;e4Hn8}x(6LkuG9(E~gTDW^6es@b#sjyczT;?X{Qc}ZA!^4ohB=@^Xz z(t*=?O61uuU&k{|Y5Hs5&e{~wILD>aO5 zE)vJ|d~Wb7U)c%6_s&QBcbScnK%*+3K0D}HhNY?QWKvSlNG}>(s32?ezHvMlME&Xv z{wAL^xfk--qeRHVL+IuRWzKTGb9eZr;T1R!{RP7`Wv5$)`H~!|W6$)wc*-_t#Ja|y z^d?w@DF#)AhC51UN2;P=HCQgSyd--uCT-eV@-I=aTC6s0=!!=jH3GHof9%&aA+5D4 zlm+gR$hwU{d0mFWYF7Q#`M78hlbmd1Dh3bKvwsM4=`fbjE)hQSna4czbaPgWt|}On z=J{Y}2O52m^uFH{ubjUktwnWHjYhQWP(>;-k8h>|`vr~0ydFCZ3tn-mOl1h|jN<@h zJ%1R%Q?oAp3v7`(oWxYkC`|tI{&n)2do0$c+l4H(Px%!TH`p~fIf-Mg3`9$J8$7Ci zfE6QE`M`NC_i4;!k+QqtNel@FgOMB)H>Gy_U$)if_zyC=JA7B1yY_(OLllEV9#4ht zbp#6Q=j&TKgTu|?1@sp6O?Nz`eYHQT7UQ)NYT+p8@vlB5OGnR#&ujSfDq)Y3Nf{(o zAr&`?nGFo!^SGs@`9A96L?*3`#4f2TQgx{ z*n|fhyHjs3pQAX%Dc9^SUmkCvtpxIU^PY$=28KU^a@p4#;&1~-nCu8!k@V>=7^nK? z`u5x{DZfA0zTI0@d(g(U^)=pQrjpw{Pr*m$lhCz2?@#a3%ah@#y07E+$W^)~^bwAz z?`Y23kFKbgR74w2co)! zWPMKO4l(osbD93)v88fPkg`JV#rQhxVzlC&&?&o&BCFZ1(^kM}57iWg%lL*gG@_<{ zctF?$AA87TpI}eC+U2O?wVKg&b#PiqW(^NUuvy->(@K+OwmxBM0YL!)LBZ$66K^06 zV%s~2vgT(0*47-7v$ns#EmCK&uu3+9;vJV%jQ7s#OvDgj;A-!RC!=To5Skli)>`wm zr3q|kXlA|t^W+oKGx&V|@Ve-avnE+lx&sUXm#pmX*!W}-*EYet=&NgrWI~;o<45v@ zMz^%&sVgV-O`ItG4W}S7WmO;5n~#-y*3|;Ma5vxi%RQv4pGUb(o8@copI+EyAOb~Y z5_Ifo!n%{J?g2H3OOYATs?jeK`^?ye$i2^*#nhuHCI(%SCQ;DB{?lL*-a-lN%!m6W zuMyf=v`X@dfK9TsigRoTuo_qyozjGZSiq7uOEJ&z$!LX+QQ2$YHIpXQ%lPyv7BRBP zPgpl=y9Zq(Q z|439%W<*+w#S;Edq6I>Da-!=TGc=2>l2%5;{~}e*v{>;x*jWaaXaTok>Kh2n!Z~B( zA7uWSD`M$_^q_{fp8j0%)qP;kc|vt1J9T3$?uUn$msgCim2|b06n0W9BCmn$amY#tGDg@gB?S`!6l+f1RX^< zDkj;lhqc#Rd^7#w3*w85vyuLGsv212p@)zvh4MooL}54Fq7b@^683;-<+G?2CWC3J zPS$I^RO~@uY-Ik~tg>qN9bOb|qCUdvDn0WCTCp2$NOAV2FiowTZbihCuVNo`da=!E z9&M{4o;jk`{2B$b;r>kb!%I3@-Aat*d8b;+MvcpjL2|Z}uB#JF<10N#nTIjuL>yj2 zm%>Wn)I!qODdl!z6)bvTjX)eka;J|`!~MoA`@oGiSBA1@G3^C|6Q|yWExl>ss~S@K z?}-qEH$r3|-Uu<$Dt$)N$FI0dmq>D%HMF<0>we%Ls`K_@oq4Su)0ZgnFesy_YoY<; zKt{yT*b~7Y{yvyProgsT%}(BbDwEW^SL}IQc_}7d^lM9~8~>kO7WLv&EfYxg}i)9{v(h*S6WVv`=srv|UkxZ|?!~;v3 z6l2nBY?Hq~Ad`+VlC}3=G1JwQ4#p}G59+n{GrmWk2vxsbZ1va}caUK1yBDLpp{$iM zP({bq+G&S|=WHEcTpqEw`NiiR{M2BHTxb`pCA0C6W<`v+K$^> z3{0AW)_MWRF%&2LAnA zJN~^$&ZDUxzvhpbxw%^uKV93SuF-KQk2hpbP_QibJZ>F^x{lkKd4X9T!?r&;hnRD% zD(7cg4fsG&SOj73dK8`zmuzTYVqAKmC6=uz@~RXI?W;1^dfs6k@+HPAr?5|=Qwxao zlO+vP1CU`yw27&K;nAtrY1k`syn+6%ZnhfZX37@~ycn;ZXQxY>-*LZ0cjvWSOp!Kx z7AhZR`|hF~j(W`R@nlRC%VhDp8|rFhQLb~_Xy(CZ{-N>wYi!C^hx?(SPGuN37Vfc{ z3ERp2QyW%m8@XOYS=q7j-ptHQ!6FI$N{rDMb6nrnCDdfO;p`g@#+IoD9o^TOkiQfCF!wZgG zGiq>opPu7){-gS0vDMfTsfzF{OUyj`{A1V+*kIJcMPISBN)Oz|S-+)K$C?e6(nkyX zn)u{U$BWR&D`M5>hQ2N4wdyiP>U=pXpsVz@L_7^GEF|*$l0?r*9E>ta2)ij7kt{YA zu$0SwyzYNx?AQ&@EV4tvWWFztKxLf9Q;AH53&)tWE80dT#uVYa1E>Xz8W?tV9F@vU zjQi+t)C<$;=l?DgWiI5-6jSHlrBIUMfz_iASmMsJZ#5vza%pJCGsD45(J6W}@b?K* zL7uekZf+b3XzON^1FlOv;WD1it#=B8##lGAYUP)_Y}==^o5kUu$JlN(#EM`=2Hw1F z$6`kO9!7AtUcN+ZMRWfK5`YLxELY;!Ionzs;S&wZ&h*R07WF9)C{tW-zJEo~HR$rv zoA~=b1x*^3i90Ms?#1;1#jT?w#vMO`8nI|^QZG#mY)Lmj1nS}YUj*6*20phlmiQ9) zd^KoMt3s47{4P4fU@b2IGa!=g9GEqB5#X%@sc9G((0LS2leIyT`i8GoAO>}vuQ$!Fb#!>`KyhaZ z7p-KLITD5pG1I5u^6!Xt=Bq>Gf~awcphy> z3)Jf!F8UO?v3Vs+>;-mM98S)2@Lta}DX&0Cx-?x-k4U?HsKh*fcWlv$?qrs()IrF? z)#v+VwC})dCe$$mT`)mtiq_(@ACZEmg!(Ys2n;}S65eI)YydE z8Bw`6UV4qKQ~$<>@7159G1EpjPk)=O#;qK73KfDD?(~Mgc^W}Js6vXH@}fGjBxyCD zpq$Yx|5|x(P>%9Ei=B%#6W=E7xp8#&Q{$1_r+oabO|H@izBaS{aG^+Zu$Y4AB)12d zOsoTe?^yWq)M6#KI%NL5t9cLI5>@aXBmP!(f2gX(zh2tgY09s4WJOFSVsrm4dl2cD ztT*u?){Ym!a_L)?s1+A6MvATW=H@5YlUD(>Fpnw{?z^}p8ZTUgw>N3{dhm5g*WEde6qUhhTcJl327QLUCA^(Up$bJ&oujgfN z5xUnkD=`&?-+a$2%^VN&F(4+4Sk^hD3u zuKVu!=Imbtcl`9(l781*I#}$QHg8@08b42o#s-Ma_bQr4i!s2PkX)`6dm{2(W+MHE zR^_#;T?Iqj-fQ;<`-*e@!k>LIS*}^I=HTWCjkFW_t~90A3Og{WoVP_Aj#h;Y%hN^ z*Uwoj0+D$gZ-mlA5Qws&p&{QK(W5exV|8$o+lD%$Rx^IIvvVZ*;!>WaZ>zZT?885K zmVv%Wo@(7!mXVA&SpH162IC-ZXtf3&s$Q`bV3RR0|%-k8;38~+Ce37o~Xkf1fM zlZPYME&9Bgc_Q=r0VI*gneK_8j>qFod|kQom@^I64meA0771N;e9Edle1l%h5mx3M ztqyQUAiN&%)wkEz+n^qD6;21erRs65necex4NcdquP>e=dB(l0^xDm#3V^7l#zy?R zS3C(KUEA`VIu&SXjch;YcRGNn+4sWDyHac=wPcbqh#F*C@nV=qtT`6h>3To9CF^)M z$8(O9m^e=(T;Ccoy8Tgag;y!%Sl*&;Y#Xa%+}&qJ1p8e~hGP6sA~{7h5zut~^5kaC zq3a-YnKtWQj2lo&EwSS6oS&H)?=~uXs6=9%U-U}h#OGds<1OICp6qxU!R0K z5h!4%?J3#4nzNPjWf22DIg&E-D0H@#zl8tc@!MwAxV_qo7IP7#NdgLMf1jc0*JOw|1dfT}Q zM{>+~W{wnX`Hnp~ZK*mjnx@(N<`CdLfP=J+B?v^`lm&l>afmkoK87tSBP;Ee6l_9=@~UqNvEsyfX}C zp%}_<6)M6zRaRCyIyq5<=Z6PMoHCGzFXq?WFL>+W2>-65spt{9W5bDBNBN@ z>Iq_zUYiP{NRe+*&E|Z`J&A-IxPgAfbb^cY^U3MR!{ARQmA7&$|c08aF_831z$3Za9z@<9I+c}=>a5{#Bck_o2BkWSyX5fI&)d_*l z*e{v564D|^IJ^3yHxI=kC(D2Gi9hFxeZ?zU#)t?-9Am#2@>csmGV)Ase7g|?>30zW zNE7v)30r+e(TlUY;HnhlJPOu2Q3B)JaJ<(|ys}~*eE{8G?IATtam>cb)lWOsYh6(D zXCg-rC*5+k^eT)5_Td7>1M~CndR(Kc#nkB&)bXB^^Z0&^PR>^R$9zx5utYWex;X%& z4=oXt^FL!IlAUG7<}bZ*ZZ4@QDG89Elg6z{u_?G|i_hmNLbP(e9qs<^u9K6~J&W#n z<(h-;yDz4<(N|_~!>g-VwK`oyvIP?}W1hq_XJ%^C%D_#V0!m?Jx7ygXiUV>Utryc% zjKmP+U@|fiZJUFbvJI##fGawCNCEc;T-teS=m`s`Yl0`@h1$LVxMD5o(l0h!CR@wf z#wPRZ7kz_{y^YP*?22yX3{mj=9SA8Ysc(*HtDiuIvcj~Jq@Zz=2dCBpH=yid=|kHe zSM{-%>GF-qUKp|TVeF5AFj}Vj#p>?|i>1`oVh$I2gubaMcV0E&j7RC-cdaljeh(S- zQm1<6uqhEQecGb>`Y}cI(#y-BM)({+yKfiil!_S&^tm==k8Yv_M~G!UQ*1vZ{@mQy z8&eiH!tMKK9V%5}cJ+s!gQKr)+Kti}K*tZ&bd>Mb0{H>P7}lF6l0UE;TI{&qZ@h4Z z|HjG;xvO5v$%0o+gf(NfJ#ZVdk_r6ji{A<7=#xW*GW6H};nt4z(IE96I7hvfz4`D; z4RPGo<7VtvI&-U|g8F8JbhbTJZOi>T+-7@{k75_xf>k8J3d82ri+frVKc>GV!@d>u zpZImHK+FqOhg~2^)~!0FurChJ>*||DGdPDMXSpUUAtxD_PZK}$wXM|{@4f&*$A@%j zdt6!kb0pbbY^u1~6@IWsW9%{8;?xEP1_w{x(uDcu#0Gkf#T1?5jPc>Dmf7NAd!#|ZxrY<@X5(f49{qoShZU%IkzlNBcl8JU_I8X2i3 zijj4xF-}fg6j}+?4&U~6+i<70Z3uQF2{4su`r31T9;P!0Uu&w^fne-CX~HT1Qa4+@ z&^$)1rZ%g6b5Z8K2qJn0x&ps`6hBI55i=4vT)9yG{;}kDxH<~C91LX z$v+&2r~001t=M{hS3XUQb1yJTLqgeSth;#jp5*Du<(d^g^4%(zG8 z@o2h=L@8?1R_s{&2F>mY7|o0vweUn5j))`nk4d1>;y-)On~)FlVvdiQPNOxltddpYP=T3hFv3vLsY3oTHA}hd4A`JX5fqfn< z&!}ZQ0}wfiwK$widdqrk%mUsCv|#vfk$x`7+56M6!}k#3l=PYvSz!V1jFC~WFoB%Q z1NYf)_>1Zq^Gszt^2tPpDlgrOaJ{^Eq^(Nd%FjU$PICicYPB4P5t9<07i-zsiem}i zc`!Egh%FqWJ2TXgLz8Rl#LN4fSkFvYaw{X*RwS}GT!5b1LWx%v`AmIcpFn|r#PN6d zyY$VU%ZBD%6SJP#e_S-p&w}-Df52ccpJ)<#A~&ZH)+1%S41 zfXV!H{p0Y%cyWp6cMHWt3+<${welfuEL%W?YugWB2}xs)=|wdW`*X0*d@tz4}{0$1fFz+CqxeJkvlB%igocKCm06);^1cI9iCu>I z@{KGGD=#8y(bTXEWSX7GtW92zRt?6uws$u!k0CNAMel7JCN4Pr*4-_3b|d29-Ovp; z3r*i+0gG$JH}CGh;jnu0K0UTDmIGY1VRY9+tdCSQake>Y`Yy<1o_*>z`(}K4c8MA7 zlhZ#(R-*zlp2cPjIGjep&4)yk&yzZi9~(E>Mgui1usa_WMpwbh4DnCf(ld9zH-0R# z%dv`4<{+VkPNN}QjC&Jt_;G!U90HiJe zoh40L=`Eb4MyVFUQ8P@2IKw`Q;^0K#G3FRQG(202gW8p43!3y9>nFyPx^(Iz7G#-6 z=E41hsnbO|J*0*{7VYKh>+5O78=I&GF*f?X-gqxYCELpo0Tg&K)ivSBU&O(|XR4zc zX`0sCr^H<=C7S4$ITb(|(0J)8t=+zL@WvFGI*$>t+RJv>q*z*=W+AUPIiomDfANIe z8C@LU%zgv3Czu{e7MD}dn+B97V@J~2qoYoOk$Ebmhd1L+9uS)L3N;vl?=w<_XKNYS zDJcxIXmL&PNMq3})9}4=RPt7jle4ph#U&6$Ro1Gu8b_=}BUd}#+*gQEUITnyn^s9% zu^R>B%g_RX(UzBRuWs-g3k$q{%?z$m_XB{zvyILcV7jrXT~7O*y}bYq8a%Mo97b8Y zi?JEE2LY8SSdgBceo#XZV1y(*H~ttkN)C0W1-*S)*BaM)eC80eWc?W}5wYe}@4m7c zI$^th5d_@s!YyUy73$a`RWNj_6x%fC0FQ5Ny~K^p{QSxSl5}!lyGXIAvDj3az!6Zn z5N{xS%@w1k>{2cGtP@95&Mpq2GQN=Hi?8kN?R9ltKPGz)eDadMV7?r^9<}J3bi8W8 ztoh4cPtNaMb3J><_Sxicl=Q7z;iiAEkb5BwRn;0K#?P3S0GYOz82bE5o(f4u*(s_% zarV96qim6X<3$gDw~!&>q}6oLX6!AyfX_N_&x(s&#;+c8mTRD_=19*u-)%B4GRs*7xl$V#8GZMd$Ih?3 zX~6AR`F>7{T=S`{pRt{m$)<>p4$H^m@rNd@=(_s)`tF62nHcni|0N{$^yGx;c#SVN z9J`sF+|>_nZfKZw5LdF4&W=^FDAY}{0(eA?p(wmlJx}CS(A%q^EB^Po1_wEUi9v6l zayNe;+scw=H~H$4=2Jgo(Sx@4{MLL(nBQ2gQ2Nhmxdc)mweLh zteBle7qf%b2CAHWPa1angc&Ia5$@b$q=ovt=(?tijkCqYt3Uf%8P=JZz<%LIgsM`w zle*R8oazk?QUJ0tH1w7~WbC4fV{1LvWA!VvasCP0d$-xv(+PW{>Mde#|4Z@@Z!7{G zrDM*6I}Rt!O=vGW-Z)w*@p=M9)s;d`CY#LT7s|ndT|1wMk5_s^wW9koTGw5qQg1BA zI))>aVmXu3O@qqYN6fB=uU)!q%4w$yRJT-*QK~&@DCFo&^P${}ew%8;4*EtY{Bg#V z^YZFtPWij*S?))4Ef$#N#4^?)s1EkV76MgMMQH`kvk*5#c^bU0J=uI!b8a(t;I#<@ z%)Q!_=?1aQVQqO?ZvGlufkMZwjdwg9STmG`adGQG&Wb3NR+9eOqflt9KEc+SzRGR( zia#BXhn1rTxJs7Q2g#^u#Fpm6E&ZpPpL*%K+;6ZBSyd9AjgVYxBqEP#`EdP7v2n+hUhhnjQ!3(Q-0T1J{dBMt8Yhk6Jh-HfB zm`Hb@99pZmV{_%si`vZ1rWHoKLbP|*6w}*ULGsj4)@15lT|icWjQ$dDZUHAblkMlgt15KlqQ)1geLiQIg&qqD1c%LQv_O=W5 z)ul&s?k7E|U8SOP*-eKN4=Dpd=VQx;&lhOMZzn#@q?UwT?(1``T3^j-i?OO zqkXoBahJmrKTDcY*~oD+&F~#N+L*qzY8QZ|S|%QDsb3Pjq2yi7N?mPkP=%IIOE3r+ zh2z-CQc_dbIMZ{N#m0`HV>`zg`nS8rJ@JPKTEtKXwUYMm3uZIa$PrX6fm3ZeMmkyQ zb=uH|UB;%;p6{Z=+rbUj1eVN4zHOqKz-#%nPT47G)AmJR?SohgbrU!@zw7=ZECPlL zy*&A&VsXX42P#72%NIFYzPQ|k;L!##`v{HD-O3S)lb=p8|G}8H2OIP)nR%H zd%T*hq5gy$Cy2~)?QlouE zycGQZKIS`7J&CE0#4B0NJCKi5o-(Ze5FW~CI2{<_SOXy9M*tgvafn>_(Lh@Wc|JwR z&c1r%vipQ(W{yCJ=vSym5J)-Wb%wQR9UYzHde=B!2oVVTrmbEALr1~W2_8bPhDS&D z&^wY6^>v5fTz1_WbgTPi?`FgVJMptM#R6?-OW(G}#>SdvoP)$Kqx+~O_TAmxq`QRW zZ~xJ9Lfrf7e!pzkVI$Y9ZLZ&fXwmkWF+Q@^wijXnL;*k)U|BdAzg^c0d(^Qpl$zGB z-`v{j?v5qqEBCRnk^FgyW7oH0igRi`nzcwx*0IX~mhA9e6<9$@TaznMtGEV@?ip6( z-+(G{pX2lqbndi$u2~-Q1iVRQbjrxqOAOq&;cI+h=^Es+;~QHTr=qQ=)kD+o_Ju@U z6FyL78@7Lq2=cktl=?#;bENlt-Y=_L(rH1cH|4Ggz-D)7&QvZT^At;ES_@4_1$AT} ztC)>X@V>R?_1fYf^{6c)yIa~tNJ>o2F{;U0v#k3w{j&d$biJR^Xu|5nRaF3$dX1mwgW=?WYp43U2@3kR(DNdBoJ${HaZ zla?*BJ0e2*cznWF-ihwjrzz#Nep1KqSgjEEvcSwZ3YKh0-zYIhv@k#w%}Zn-gae<+l9_a0=)I-0^1C*( zTyHxF3j}7o^(8}AGVwA6ozmwdg<>L*&w*Sp*h_Yk0=UG_`(<3-G9Di&woKE=9n%CJ zfDr=oIFPq1k%&xem%4B#ImqQjh+?@w8>0VP zJ#X++K$!XO$hvov>A%zE;rsvVHvdnTb`6kw%KP8ne@lNF_rLYTTH3Tcp{Yn=9>f29 z?c*!|e|-D@Np1Xp{dE8DLgxRUE=>$*t_9^jfPolaARGxS;xz?8V{P(SF$9^U)BSz; z7U(C^^Z*42Frtw+K;Le>1JeRBYrG|1)e zKVA?gTZJY6P>AtVkpWwc0p|Dd;A?<4=WG!tFOFL0aF&n9lY^GqCM$M6)0%v#i_xdO z{!MDaME}l*NHmesHPBCy(l9%MkUPusiPCGJW!Yx|3?LICJ?X-LF$U@a;u|2Jxi{1Y zf1ZAp0iNr!GAN$}w5-kDU7pm5ybXGnI)gD)&(Pd+QtlH%BSzRqQOmO>O_W-M8TGVU>Bqa8lFz8x6>ohN5&d z)-@QAn^eAr_L(K*_2=6`SYi`fAbK1gg<)FSi#$;AAr1ALLX$uky&GG>(Yev$w12+o@dT1T1#mD-t zt*1P{@<-C>Z@i{wkft#3e_=>tzT_SSO=Tdl{yt|XgF6rKShFlxo2}`~kqhL&+O`L| zT8upOFSzKAm|_3(5PW#u`9HG%xa-*Lo{f!A>$#N2GFJZbfu5d))D2*_@i$VF9#qy| zS(%a=He-0TMEP>h5A)k@dTM5&v_mfK-h}fQgkI%7d2vA!Qaq17H3jirbY~ZE>lU#U zB5LX?Y~<{KZ5sSF>NhdDIWaT=t5kPY4$$ZnXrsi} ze`muI2Xex}3bA1-GFX>#M=ef;Sez3&P*3z#ykfAcZTZ(VZ-#j;4c0#Q+G;L#a$+^7 z^oZIir#im2XNq;I%3ZE-#*LeJr!rNVO1S6&M4|~(BC6qUHn6VtRtzPH!+fLW!)E-j z6nlP%i=L;-i`-9#io1m;){xk}4YZKT)?Bb}U2RqpnVX-Faf2HeQfm4HB21-Xh3jgn z7u_byX1oVTVJWFQZO=6AAd7Q53}HzO<<8|SI8~#U{;s~I_%}tE7!9t>#n}bk+WP8R zqsk^lhiY3J8^O^#gx52YDN`e>na&|gxOm2wZ4Gn8mMwv%i z0-#-v%vn4nE7`CNrphmzs85)AZ1P2qk8W^ojxeSptN&RYz4~Z$ti!e{oOk@2is8sl z1(o(mIPVCjZdrYx)oGq?l9M;Rk=lvhMQ0P<_y zlsu)4a}O_D-%~r)&f`P1oV~}8wtcU)q`wp3V8nAL%c^^1C#O@LFsAGb?q(TWm?sME z|9Qh2`bpZ!iQ6Ot zcIJj?tKUYL_TKz?@41LEe)sl4M2zK39dY87Q^qDk>Nal1nKLZl3JQ-Xc^vpALyKU-4%!CQr;5Zy7lfu~gpWMt=7?y(MnK6Z% z)LDK17Rjuu8loCT#}6WTghKaY7{Up4-vsff%S$e z4Hw2TAy9N=fp0sI(Il^#Q5%5pU~hGB+n!??Xjtgz&s5&5h*rL<5sA7N$L;iz)aaoC zr!`X;u$vF%+jApck+N*uq)R=&l!v9c`O2FJ{46Jl$PP=a6(Tl`=eswSrjWC*7QY4C+xDO`j1KFn{i-B_XAd)Z>B^cMvepej`C;gl3bDU9S+&6J z*e}fhjV)P_G~(A9Sh@JcZ*XuRMt4Y7v-b|QfyHxeAB^%;{s=Qp+u9-C(b>uR(RZ=y zh|mJYlM7ckQWeTtyefeR(w?=?FOBknjwC5;|69_$Q;Gh1G7sokr}k=bc&B0$^1%n& zLFkFni+n(ZgulEpaYoedW0_F!H!&NHTvRoY++8Ch1QX}e`Ir$qWrEjKV~rUlxYPX9 ztFC;et)b3^lX9t1Ex>=#UrZm7KIh6Z=FZypAP+294duJT0lt$x`QphTGL1Kb>|!b@ z;tR^D3y_Uuqmi2B<1VKMQT875L62)Pv!gqOrzw%k1m-4Du zv&7v8)WQk2dAh9=yI;Z8)*@_u2ChT;HxOeO1KJ6EIG(~wUwELSyw?2Cj8U+jN#^qF zM0uZ?oIt^*ePLEInl#+G!#rB&3pDP9Lx?KHiG=N%c&+a43cOvtwXn@6_xJ2 z(Jt4o8mJi%bHwb}e-vZJ{IHZ{aAOQ~`8NZz6>g>o@g-{IG&enAmexiH7%%EmQTKWF z%zv5-tVWk0qmcr4`lJdC-h?K3v7n;(bGYX6_U(H%E!W-;Fuh_l8-XE*E(R52Ms;wN z@#2!j7_&MqNkM{cFy-4%pIe?S+D_w8Fsk41onVJhMJ4S50|}PcP8W4)xXB_v1B$Y2c_u~`?M#2AuNZK>7vs3pQA82h87}<{9w2??ZZZDS>o0&n8wRdmOcYhL%hCe z*$e`Xpq^Wp2b!vUY6$?7R0)&B&R}O?tb{k5J&s>>bPCLpFwEqr2Guo4F|7HAP{cpi<3Y_C9-c#sv7Xx&x1nH#biMy6BpDOChSXCx|n%Cd0 zGY(Hr%1=huXL&2Ap5Mv0~EBKW#THwimtkP?eQ1qmkJ9rnbE~ONd%6%)`Cn`Gdl0AEpg^ zM@OkRE?4XEm4C6uvbKX&Ha+DV7~D#GHkUKI5Ph8718Z)ruB)4LRP!x9+Kifo8#;PP zV)YnO=><(%#($Q`G1AxD=JHkNuiO_FVR-t{RytxTpia&Dp^^k+Ea8mti7T7G7!tYf z4=A4GsTeH=C+^y1#P%iT@hIoJ7U~ECI+1w-;D0lc_0?j}+QTfYd}a;a(+-JcP1DP~ zUdra|v29p#K&Qe|P{QT}G5$f0SLNt}<|qv~uVI0|e^+O1{{fCEdHV5HT7s~r%3(y zIa7Ww=xv4!dB{COv*+dgCv_4Gsd|r}c+D{(o(nTsTIsGN#gN47C`q&SpwLmNhn7H} zdUS7-OQ6!IB|r@sCDzn7H^h&i5&i3?V|jESwkI~u!Y8+m zDB1cK6jJ|iH3Q9v{8Ct}8 z>_}N%PAz>%?`LVzr;bM}$F{A{Qt)oQfep#TQH|))mRB7;V;!EDVCeIXGdPg{9)4y` zww%IjW}VlS)>)Oh1ohuC9aWewd+eDx;lgZp-xQ0U@4BC=6PZ3_XJE4G&1rP@Png?d zMntn9x{!r1u!DtDRKfu{DMxduh_1iAv)_c>@9Q|JEoOU-)E2~C$>F>4|-O1ks zqwU0*^CuufLhn{9;LnOxz)spB-nz9afFU$O`j4QQTaC%hXIQ7q{1qp;BT~op+4Fke zM*STuoEb61#kNOYtZp{jo&PDCHp}go`dOE2Qyx#60fM#9HN)z7UOpxLa9x_LYfcoJ zID}f+CI;T^=ev2GVgZ?|(LdJ_(TxV7%~M6e`MN2Z-V*swP?TK^=F!xfM-mV6-7DJo zEgV}-+*z|3Lf0UGP^mZV+bN+#F_J~eHH86CD`G-p#4V!t{d*bvJhzYM7lXDxz3O^G z_axoHxt<$*ifWb<-BL@6fHI*tV2@sOBJ_%iggy{0<(t!lDVb!E4K)7KDR@y~%9R8t z`?2#u?SBVyf2rmEp4a<(hzs%*xy{-~O>NuF`iuBWaaZF51jTqpq9Y1776Y znX{BCn`z+5J(@<``&Kt0#r$14*V%HNE!X}NsXIVO)Gzm5yD5P>9N8sFvv5o<1nQ=Hk8ax4(T&>QzVZpZv5T>w(@s%%yA5K%sTuF z(kI6sP$+x1UOYK6^-TYIa$M<;&%I1>ZVULax^zJ-cmAF-;OER7%=~I@)jp3#E{BK(vuiK#Gsu zY3iQ{GmXy1kl09bK510v;{bSbW8hJO6$~stF zD|QreqMNSWqHt1{sBn-L3thUqgSdw&Agk> zqG{^kzFxIQBVvNYF0-I-QD@hl_}#)CDr1pi@TZWaT666xbwf1$1U95|_((YCho13SX&To&BD}4?L?FU+Yu6d9gt$gd6&**C57|?Kc>!NfAL4zHGSR(o5#zjjJ zYU(A*fThg9pFFWK!kW7fX_D(9e%F?B5IEnQ)N6BRjC`VfYTHetzRW&kAP}YLrw+!u zH{b9358I#kcMSv5zst*O)P)AtlvBiy3ctuJmEV+qiLJQ?8jaN1^{wEIWdGg4ey8iD zjF#_y8(E-;dy17jRm(=yQcG3aNjUo-Ay?nr+-Py&=vKr(GTHX?*Oh*!{)w9c^rSbR3l%(s2;N>80(8PV%=)~#wuNQx7^<>Wq&?j?%FlC87;9WX& zjBkxJ_sTB!WKa)kRvz#xpZ#(?Ty9l*bXP>*sRM3UiMjgycMGw7e9qqT_p<${xqYi? zFEwbuFvsUlm5K9yYS4ybEOzNn%+k11ebC?2zso;tWUnyJmq33)X6bAXXfc=`tl!N& zyXQ<5@7=!D3W$s=fLiNfWa&3K0k#AD1EuU?d|c33YFi-;b+vf0c>1d^_x6YTL)Ge3 z0asgrdxwWQDONxu>Fv)e%5GPqSgMZr8z656E1pO+`t{gRj_>B&9MxAh)p>-;=-XrWzbEZ~>i?ej=3dObJ;SvVN{e%2*M1Lh=l;$b3tFogllxod{P#Ey(B}US z!rlTZs_u;&9YR4mB}7Uj1f&I&W=Ls}7`nS#Vvuf>Zj=;ZKw_jB=~M&+1O!w{x;y4> z-uM0AyVhOnTi?;;S~!Q9J^P%q_wzizc>MDL+ZlD9fm#$;*p0L)(j>yW*a4G@-tD_~ zuWo4g_;DA!HY>3qFb{~_P(XHWTg=-zuPHbk5J_CQol%4QWKxZumOM}eJXQ2kS@62> z!i8bgM(}h-`|=QK>AA#i2k|vnZYR9wT+aj+8b39kADhAzicHYO>M!$zjt?YvJkeb| zm#e7#`wO4y>#Y!1u{`K8^pWc|0Q8Fl z(t`t8$`<^}apB6oV4ow+p@;gO+^E2a#0mUzi=nZmDMlX)^3(Y(Q_Q0JS+V*#CmOYL z3K;nt=qvc8OUqonuKeQO6nt(fc*FG46Moc!2Kz)cQr-Xq!uZWCsS;_BAE%hLTvLho zy3Igq3GMez)2b_r&JrKtct9};^QyuM5vYV1a<=cFk9MxM`I%pru1Q|jFfgdI!W>!8 z6J0MR)z6mH^Ho^Y#jo~65={@*)jv+A*DXaWq83hq;QU#q?dZck$?G`O;SksZzsOtK z+Mf6ZpPT@QyGsY(BL3>w^=eWW{+lYXZ&A_@jZ->$s4cmyt*e(l#6+-gjX)nz!AGeQ z<%?=lI?0nHuN6?|70H5EzftI-z&~V9sfolTqYE43rNhk|r*@L$i}qnp>48|i`Z`?C zxFz^%aYSr(%L@=!^nw>o0;j;eA$g}ut?VjW4|UG%dRj)IrYE|1zdA+EZo+E;oL z?lGKx#+jU&s+d@xx~{1{TQ>Edy(hULAhAeT(B9T|F`5)K?41WTAa4MC;$YQdLw?b( z2Oqt>%2+tAs197_3_jht{*2xiUpQ$&p)a6^Ncg5>`vDKU4_s2o(_c7}6xbNdnGguB zU@QBg`wxdFNz0U;bhhs9=B}XvrNibL*GYf<_ zwH$h>;98Kh#QywT{jwVjgnXRkY@z{uE_oeO5TCN79=yeXj)PyrYbVpkDeMtLk-i96 z&_y+B(RkhxKhd0)YSZbS*5QTdoSVq+5Fiy>gWny27dqZ!u5jb#k#uPlbm*j8>@;e zwJ!AW5NiLswz}`rJfLyl&oZ0AhcpT+mor>nNDdCe(NXZ-+3P`HlV-Qy)j=n|S99o% zDD)isGQy_l6+3_(oyo&nw=|JS$r3B4&IiQn?T zc9Bd+@aB{aeONXw=t-Bi?4Qh1wb-pxTT`>`Z(>4$+Pk--GM1VZ&~{}w5otUAluq#a zzJjAMYbMwJ<$lTY_SI#%z3r1$VWlyfIuJ~>apGc5dGnaIQe#VJm+l0Q1jMkbC7-mO zwB20i_t+iF&odU#uZZ7|Yu(MD8c4~2l4>a*U0_)bA8iQtE)M+p^XIK3Z^ZP^pR@_A z&z}#$HarUPNHqu7j*j;WsT1Y(@=@ml)$q1xz=gbtyiog}OfN1a_x&X=!z4)}$BBbZ zAE3_WPp<#KFP010Uz3qRwh_1W5jw$343}M8L&3W{fvD)q&&1c?;O8S~fPFIu0tiT| zE9=+%yh(B?Tg8V!jRVP&+mbZuD@A0$UWm+IBess~Vh_Hry*Tj0+lZ-rYNLIXPcxSV z)ZX1Eg(rg#+IA?JNuS+2@V*CN=Q8bulOZ+2N*xM0-R_B(-qxb=rIkRFv}}~@o~-X^ zJY{TRLWiEzkJi-%4ig6HJxZ0S$>W6tuNYX|of5>6q1z&fTAR&}1@05P&>r-j8!t5= zE8Apzhtx81G3F4;SGn$8IKKvu`HSk{iHgUj?S~Pr=a(Z_n>#wnS3gl_4M-RvKu{Yu zDiG0&fA?4d*4L8YS5rY-c4EORS+*s)^JFDhH{X~hoF;QzFm7?)xXs~7ml)(TO)VIZ z>7KP|GE2)nq_dYOI%AV-M_}WC=DF&Tgj;%UbTORuuS`x>Nd6{IPfs`5 zTS{pp!us5%sZ4mgNb>xJ>(vH)3&a6Y@Fl<@gl{rjw(XpL-3dnHNPKg6!dFLq^90$F zJG$kz%<}z|!V0OYK6OB(e$cI0IB8o5qW1kbp}c%Df6~4K-pJC?abp49XL|a{^(rpO zxcN}{`rH)MM5)Pp+)Kz(x11Fm``lBtn2VA5YGpvTw|_nb|6`eCd@YGSM(>T5NgUsY zFSh_9&rcB4p;v9MdrpE+(EHsf>VZ4HLAII{TRs~%sRcvuZkJxQ@kJK=ng?~!(td!f z_CH#DI-YvV{L9-Ebm6SY0q%MjK54s=P3BKHJT9w%Gl!$^}e?39(D3R>| z?x{KL^7TfSadVX95Gh0AB8W$pO^-_!_Ei_C``nXDp~ac8g?>A(SH4EE7{BHV2~q}@ zD%$slP&)zbV3nDdJUv0}78rru{OSb$cjtOy=X{XgFL^zx{gFET5E56BUw=E2}c|5hxVr-5yg`cB)%9O{e# zJr19rU2qg!N1Z#m?hr?rUQJf}{n_h7Al1c>4pHbN^ja6gl^v*i^{NmrL>L%2!Z+E1 zlcN~|HuvjBOwXF&2L}avG_I#(4T0{|YC}PphFLM}v4fH+`_pHVa#k+ z1Hhc29b4x3doRBwUD}xs6N7*?@WS*O047cA{`8NcK#)&brL*x5-{2M=#xFV&-Ivab zWSl{;>~ELI$qBAY1-+vVzzZ}rt(8GUAZ^525QjZ zTx(H*i4AX?ueDC?0v|0B|L8l;y`Pd}$yH0>lO{>!iC+ZkQ1z6Ol~@)DNF6zH4(oiC z##K1dfOSK&NkxwmS)Y5w#2C!bacH;Wa&VXq`<2!Q_E@f=XuE558zM}2Fj{J?uvtO9AXQpo<7 z*D<~(9lE0T_<|JFG7Xp#tp(Xv9odVBmnUu7uv`vwno{a~IqEcNEmO5%8^CL+K>X9b zIC@~%+BCjaT;r?TtM(h4Gz-G=noM$I2#f+XIs9d+u~)(@xR-R%CbO_-6e`u#pZEFo8Q|KsVbn8GA$EZmdcv zH4w>5CLy&iNo>1MDwIRm=Uxg3eNN%Oy=eQ|S0^2XO1vmkN&iJbK&9qQ{l-h4V9sGD z1VZ-_aG(*)zlrE-0C!||OEE-su#w;3Ui2|!(2zgDX9+P%2uz}kl){nAR{Wa;-%SI~=yW@yj z^y$nYs~B|-f`-GHwmAPE&4ZL#-W>w@Dg5<$y)h}1jirj{BWVtLb=DL&3k2_8$@6=F zD9?XqBVp#+1m4iFBQogz)8I$p)NEX#pS8ced(A!UYcE<*hV}7uHEXRZRU60i`{lxi1~JXADv;-^sjNmgAPP_~ zxnsx|P8i^uWJLcx6(I-Ic4jtY?sQSW$c7mxwg1RWc^g~CPr|rj6ssE+w(7wQCYLU{ z>nAp`$1%sITV@;I=4alfb!SkCa}b(zutpyf9Rn@? zS-T<93nS&5Y_5_85FmRAz!6&+3z5hoS6@Ng&S4g-%001+tk^=S(Em7TAyhl@0fM}q zZMp|WdiKDj1*CH;D?a(F${b8%Fug~wz@27Haf{rz_2V0BaO;w3PUnOq$x{)bEtuTJ zef{0})RN`ffae-uY{v5e+HjCFC_DonGM|c)RNBPe<%*RBm%EN;rTm7)NFNb8L^Z2|?;)8Iv{~ox{O}K(?q9;}jvoT9{IF9gwD3K+)ytC@$pjcakbD!E2i5dZd86sj{!tZU$K0VL;>1_G%fr* zwU&wD0}D@2PE~E2dC_wp>!@3znZwD^iX1|8iIf{XJuW2J<8Il@qUS>YU5VEBf2B%b zl6B5|U$$|XEfAD%rU{6cwW>6T5;c_5!nlBi9S7$mt!Aw4#_$t%3pt|ZC)AEeEvQv| zmh&{MqF#5nj&Oc`YehKt6gGNp7M3`vRazqk2sI*|kgpi#B~wox*NlamJO}Kzve+v9 zI`72~`e6oa01qguqkF1(n`$MD=%5Rz{;eHR1T?&Fp2pMi)@cQPBqhzCHERl#0d$I8{v%+5z6-gc6f5$!$@21#ZW1mW@sO+HIpN ztg?T8Ucv^vkzdmlX0(tlw&ra}$OXZxNQLytm&f{0`UxMuvnCtQj6VTUhBcGPynHwA zsyz2PS?>1y%jes*`vg=yvp@80ZJ5TCfq%yr#Fj$k{m0D1mB?eUksB@GzL-KKh6|Pg z4#1f^7Q8Yjn5*KC^JHrmrCK=dlH9gd zQ?ZfVs)zdS@BL@fDTWV|TCZG&(f)T!Zm6*TY}R6zE3Vf9by~4RDI8>PfPT;hf7Nhk zy_#|DXQmrFPHy5PVS{QThkgu}}!Hs)>}3L8OMKF^jpkXoqZF8d26l)4vZ z8X|xseUe@U5AelnyPV>de?>6jg4J7}V|F5iS7p53r~`yWE>G?Oed%c8>&(LFMG)KB zl7@#JpWWZq66BuUN?feZRwdoTf=dvMIS5sx;R)am*a_xGFjeaJd@!k=`tYL9JJbIY zTis_U4ePF0_2_#BuO_qX$Z>Wx2c$#Av<9rq{^>?S$FNd%q`8PGZTpV!`cao3o`O4n zo*JcP^1tfM$N+6 zTD~(7oSd8>(inlR`tH!T&w*u;J{3augh?Z{E#tKYsX;^nAc0WE%!LEpinP@;GXS?m z*6D!)X=}Y;9EFi;UFo8eQ)R}*>+|*vk!2SF&tw)&iu=%~nLRT;Ey!Ia1gBq!&P^#fEcckQL`yIG}iClTs;LsbMan*9x|ZH58p z>zgUv@0Y>Kt^8dO-gM%0037h7bB?!fm<0xRK))CL!{B*-7S;mlV+~-!Q>v9Mr|-CB z+@)&v&nLc(MamItaW^b8sA~FSgh!6`X3?}$6{37+TH~$t%+_})C;Ov=%^|^mYQ_f#yE=-M0VIhz$ zp#biCH^=88!rs>?y0kT{V!DfOB6gx9+Y{t(^z9q6duP@JwO|>Dlg|fXjO;iP(Tad} zN>}EjXUGXO?oXOH(*}W{iAKWgk3nypEF9#@RGfT9;eOeQ#NQi!zq!*4cf-|#Fi5$VB;8%ivoN+y(}I^}J9(L4OxYjD_l zUJ3C63CoI*0mE+MC$0Iz%cT(g3lx9B5t4EL(9W+(flj_?&x)U~_sRN^T3OJy9;t3` zW5ki4*yEfN=EC}_DIh>e10XSGzo;0*MPfgTDn!~zs?c}z5?#))|++YZGLU7kH zZ4yT&>h`hPHcmW_iC%3^TwL;Rud8#Edz7YJr&-YE#!Fpl&u#$_PmrYhv^YDPbD!m}Hn}91=5I`Xc;QIQ zOtEMWzOF8wfYHDD4NSt_eG9}x3q)pS0-q*uvP@{|ePU3PvR4c%|0dlGZ2+u2`Nfxr z6%jR2N7?jca}n-7)zf@o$t2l^ag)D3D@FILojPF~&52JD9KjdHk7Oaszi_?1tU|R0 zIdBxBGm8Q&xYGvi6Rje?E$%hWIlRo*+YVXvXT!G@%TFDk8f2H9zG5OM`(kroh8gZL%-i zV#_a<2UYrxq1`KT?pA85Z^U>47kJR$6IaL&G`@Q1e>DNm;2)78fsvz1B#TbD4wC-6 zkMo%^s`3Q;l_^3u>~ls}%oBhQ)4b{EIM`*ld^DoXj2G%)Fn$Z{?*p(D7E;ot zDjo|>#(%e{iC;iaHu>8RL*W@P+FS;f3%ms5f_a3Y{Rb9zMKzR{{o7kw%I>R1-MPQ^ z>oFgB%w*Z&1HZjD5&P(*EhPjjq3}H&jyVFMgSl`*d>qBjv;8;Sd)k;eH*(9z@yX80 z*dH0u1nV>ZF1Nu<=Ra?v`K=6UYkkgCU9VFdsqzUzXN)B8fgq&(4k_D!*TK2K&dZCD z9Vg?xR!9wqg~1v<`c8;ViHr?=6o;qac~m(ev{(hJTqtAfOi;Csi_?=K8u>gft~Ybf zPmAc``zgSHM@-EM^s<+Xd}L!02xw=J*PK3;Y-eNM^5IlV_826X@&?PY?Hf$T5~Eq+y*%Txbbkekgi&n5$D!+*zd;zKm|?5edT*HG=6 z*N5*N&L*t_ z3&c^52n%FgOU`PKouG+{$)j;~2Lxcn6^eYC)D8XYJ?Ala2Y@P(U~Lp--7dZZD-EyI zt%%arq!Y8Qysyscr7yAjF<_hue%1NsJYeArAHCRyvQuG#-iTU$HZ$bPBIv@370Sj4mAcB25GgumhLl zGlzkBV$7gFbuoLa1M=lwXD?8R#q*uCK36X18j3B<+#(YIAq9UqHmUuWuF>}}gdlA~ zCk5a=ZXjt%4IGCWCY6CH?CdP+Vl2}q?rY#%Cc7~>an6jQRu5S)aki;(D-?T9=5d5h}IJ9_CM4A(F2C_bT}8jU#|U( zf{sh_JYMTVe;hAC%bbg7%mZLv$CmP1z2z0Et9_{n z8d{)G|9pV@)}BC{hMfULiA~$li|2^vFq*QJ$G!vk73^uekohxH z?EUw&F_~p99o&n5*DN&}QW3=xUMhSNQcIQTxzF?oq^ar#mPyF3Ot0A17UQHp9n(Ng%fLTjXQIMT2}ccm5lxYnJQlg|5e=<~w(ofp2ovB8euAHE(af zId?a8OHs4h#wB5!VD?_wdlO%@liNq`9Nl8^64xlUfE?+!Q&2JZQ* z;~ZmB64^wnDsmH`;1>)`x&C=1jjs3wZ2g0m*O3pnOS;m}kB`58Io|z}MOz$yU!`n( zx?<2Os!lg3yA;$sUwY=~x=d6(&QJ$MRY2vgT6HL?6XPkmV~vW;;au2woOU~`!%xh| zcxp5|69^~|0(FHr^yriNQU;)oTg@(_E1lHZ#}b)aFf zgjq603QU*OO3GxH4w$QlYfo|DTctnw^IhD(<0PBAa^gcAK7QT9-t0XqnJ8n)f8U(M zE=1wmRvp35E^P(f#vDG(2IY1)-4-uBSNDqvmxIda#;TU~Prb3meH?wd(a}XmZi2BX z*#FD=K*_W5S^jD|({0BHAfxg(IlRE?)%dr&1caGcCNeOjMVqKyBlf<6fn$Tur*SUr zcV%}G?`8W509DLKlY<%{&6H;cRl=gI(%wb4_;*ZA7>ASTN z^dQYUsUi@Q)2yujp!+DX2vPIf%R%mubV$=b)BMXC7i63v0o}usGh!o>%PFSlNcD?U z`L^F@`Fu7av{zquuIF*CcV*jm1m@&Qzwux7byza9YAG|_`=_)cW^~h=qmjxw^;LBH zwR+$dXMW(H8?XpK@Sw_)ro2oJ@a`M*CI}SvLBan_ZscG)FsEwcDU6|;A`=04<}WHH z3fccjE^h`+k4LQVRvPLu`B4sgaS2q^`wuSF0Tu@AwXgmrU3HuB?usQD5GoX{RvDeO zowV(YTxYqS7nvTVp>|D!`F5|U=wv&F{qx~p^iW4ffPuDW)j8b0Ar3!UN1-FomsHnf zu4jmaYX*QnpG-PT@Ynw{j{sYXo7nPMM`lM;GPBYXM45}oRbqeu#0IcAQY6=ok|d9R z0JcFvrb=0Yd>8CFbN9^26+nVVuFpn-`v4)NO!9gdu&mwy#3YmtleB#5LrXvadSvAC zIl_Co=pB1(!DTsI_l6I|SK7J_7E2~dYqCKP|E%D#SW3==0glxu<5ZrkwnZoW$r=89 z5QADW*X?dQ-Dl?S{bscCobI1$lG}%ad@8_L*214tL+B!aj2MO(ecxcn9q0+@fYtdh z)E;1$JVOD`hK{#n*uaMg6$*MOfW&-$etze2eh0pCPa==#rN!JRWi@-Oh|(XHm_-pK zZDxKPO^Ao_6wJ^iH;XeZB)XQXLgx`|Lu_PR(E5)9QW?6e5vNQ77*aPyS7i>-_%t0( zN>HYQasY2;It19f^?dHz9-An;n^4$ip94o;Y&p#(xmOwhBmkS!psavbVe4;s=(4yp z>vwmIUjQZm-tRswR~tKi4InlQ;M8{%9|<_mvNbu0sV<*zFi4*p2+tU{ioesP;rL~^ zednSbt=E2KxNw!faKn1KSVeC(1TI-JwD_49%{g2Rqb~K}*Whj-zWg1P^W~f~-!D09 z{@4*k-?L{pe<_EAJ&gSWN8_WAbJDOfN0Q^h=btOE1W{A3y1U^v(XT6eFqh$r{jWwfN zKEao4@aAzBIDk9{*7s`{MJ}$c;paSP=Y}ayW2&ov6g0nBh7Uov6m>oI5=C|b*>*W> z=^KT)*obHDGrTo^Pm|2Fk|B)sy{)!u!Snp_8S%TD*hoJ;!|z67Y;o&bo=lv3`u z$o49HewE;M5d*qR^6F4v- z*DqY&DnPX~oQwpom^N1(a4(#e?Tevf3a)L@zZkCk=-5n8`cO7c`u|BUe!U|Q99G}d zO%#?tx(lJ_vW%xRta!0}cYvxA^lKQZt5k7zbG5l@s^qI$lsLIeL!ZZ?1ni?Y4| zU$_AD4Dedr=ns9a;6#hs5w>0qZyN zx4?+l$)KJ@)BeTKMnwhs(iVM`0iVtgyZ*5}>ZSr^xcZ?D>KqG~gr>lNHQ)cZNpJ`X zwSUOG9Kctz(VV8NvD8$1=~_O7;nB7SnT{+m_ao;RV#zXI6B2D zAfTaA_Q4=PGw}orIZ+oEI{_Pdfz#;IyoEfKQjpIMeSdpn6XWi_r!BE|1O^hkT=m#D zaS~xuMH5fPR zbP+=U09KO%56gi8_Eh`v1Op&a z0iaese?xrlbg}v@T>Z3E^6IE+^$jj*#*Q_@FaKn?ZY1b(+7^9Ta3uh4$pU11^SK;A znqALUTz8F5Zn13w7Y4t(13V{d0 z*X8iDYBaD`-w#6HGF$->2O^0(J_SVj(^wKUxzX`_?zzK-RsQ*GN-|K`v!cCIz!Tl@ zc?PbZ0w@g6PXmFciLt@iUu|{adtXkjIxaxs(!NrG;#VD806xO+u`xL1nCng+@gUIW ztJhmpD3G4~HAEG*(!n>&%PMiJnr|+a+amy%uevqR!#9(oRak$2iRIF+nC6OQ2DMO6 z!&nfIo0WpN@FkIC1mCSh-jD+lajj^LJAmJFd#+DZv~hE z&U)mX6{_=mJ!;E%4hD+e-O*)eajJ(g+t9wpMdBiR$)FXXb?2=q_c1O`;rBqkTVmfE z)y4)|(on|yX*5C5I=Z&&nP-;Mb-!8lN9cOr7apo*PZS@Ky{YYwJ^g^}C4L0op1tNF zJ+EOnf3C#;Vz4P}==4oO^uHpF@T^ggeinI76}_V~8y+?n0ak32Gv*Tdz>K|6Wd3I^ zH;ZK~zOoRlx8qixui9;hNX15)*5Jw20^skqqKPH-D_&4?S~^q6^}>2CdC*6wvzz-) zUE=yw7~VC7-W<7_4MxE)T5lVq-+A-&B}ZIJaclb*dOAZjXm2J+6pGjYJ^BO@A4wMg z%9LV~e!3hbNVT!l%%G9ywUwJayr?~Xop;Eu2h2krHbkKiOCbIS;Dzp2y~Nw3pbf>e z9dF%gK%UR+fl-N937OZk`z5KX{&{NPX9RMm>E;D-8j2Rf=-0zE7 zw4#+$N)|~ZKLxuwFl@GkZ?6^vU7xWrz%TECFtV}A^J0|TIx%uFW37vhunwm$eY{+h zcKdXMpGDK=$BU?r2+JKIe2k1C_Zq z^ya~ctpTgdU%#7oha|On3WDIJVx@U|(z&(8XP^r9Sb*Fhz5^@7 z6O_a*=FLPdZ(@pl^?#iRe!cOf_Y};%Jork53CV46%m!tvL3z+j4jjLhf90yV`tzr{ zT|_`2tLbK}|HR~eqN1j+BB#5Zma$5{b74)_L`*9C7kq=sb`R$+(}ezqD?x}SfiNO9 zYuMK(pnWHUDa<4JIcII%_(7oSCpTKP7`tx_Us)4uR|HkvXGv~8?FMyv>MP6EyBO2& zcWf)VHe)B2zk_(2ItJ@e;!h}??zANT^j}W#`O0@2laP(g6*bZ6A=Xm^hWF303&|U| zdd*|Q^xw~7?9~4wBtZU;MjZhDf;6~i#@y9j#e|5x9s&WOSl~_CkjXae6dzNTV$-r= z=bKF^&ht@G9CVUm;8+oR{$V0=v9aiPJ75sSxN!hbOyq`E;a2Zc;YAyzR~@*NK(%n5 zRdA5@zY@s%Uzm`eJj2j5*nxoLD(8B_SB7@9aEi#`C1gCI-#I0&_M0dR{+iRia#V26s=XBc%pT-Ubziv(?>CHY9{W_c zt=>m#awN#S%1ESSW>z3YIqgryXqd-DQ%L@l^J-vNT3WSWM$qr`)ynjerSZ)ic;2tp z*=THRg~oHx|2P?}LZ}n&GuyiR1VQ>RUR-*N} zXcP9UNWgp&NwfqGVq(gw9yVIaoFxFguEa1Ilj@SLWKxe|$NkWlavMCs+3-mgB`CCN zV{T(d0+Y~fb}_6U?-spyjbd2z`FPlDCl3xxvq3)Zk@##83u-xU_rMhDdn&Eg)QS5d z=xC{Sk=+sD{{y8_;^i{o+0r9e1IvpuX(aaei!HzjY|k4EJo+T%(xX#5o${bi7^fqG zZ-w;2$l5n@AM0*J2NfaD-ky!|)Pq7Xbi?9Bqb7wfB!4XU_mq!X&g&&hXJ8u{AR>?kB6;d5}&i=Op zn>G^=4|;kcdLp(D^s1~4N>d-jH4;I$j5`TNa}bo( zZH*6|`j&IeD;$j*IMU%X$^E+SrVWqI6tV$eh+oK*c}T_wU8NMSE=mDag`4Pkl{rlWg9aPts4GE9$G{ z>1$DZ%5`>WZ;-j++z1Ge*C1`&YYeFcs0on3A~$FMMbyUUBArz!n$K4fe{Y^QNYWCI z7&%`}4bS+(c_$gU0RFUL{NtC3v40~Z`#GFP1BMy=^Y2?B#TXVQu-XN7u)hut2Xs(E|FlgFvDr7FjI`cG%46&cW3#(g`V zQAWZBYFoGbZ;1E}dI;UYYoa5V&`7O~Wz)11=o;bDFecdM6GU$M4;7Dho@qJz}RQ|V+;0fv}nwVOqgn0fHy%)3u;UYd84-7*o}oWqrwcn_e(6xJNZ z_cYv@CpCx;?=t=INWI2Sx(Lp~=2|Iw!ejrc_1Dmd^Q#4aQVhTnXR_>}e&DgY!v!&q zcQ=lTjxBVf1N82ti0I9&lc=Ja)n?G=1?+ScQ&oNLT3=Pn5!SjudSuB7)r`;L89Dsp zuDlzPB^RD%LFO2dRmsO(Ou>JAgWmm|^2zsjzdU+GFPC0ff_y9*S(OkSldWpt_E)(; zk(A?Q;G0L;y*+z;6cNBde-sMs)nF?AjxTs$h@qF2e)WTChDD)&( zZtvF60o~oxxx1rXp2~S-_Oka@jc{lOm6%q_qS!&PKkWDapUeW=I69iB941tPcGr=} zl3;J?at_tGl%*`69j@THDc3AmQ~$;-QO}amtF&&YZLhhj@d+Tbls_G$eIHIofl zYe4UpL${mB_8Z^~fa<;{;g``>42Kf2w$()?DMg3liIyPxV|AZ>FMrL4!#Y(dH z`_Dr6huuVN77=W+P4vk{^c^%gK!LI*6IGFzYAr%w&(`YgC_B2ynH%om4D}7_(~cHu zvpAYu_J53jSrzy9_dCkGOCrR#=i*cf{>|dWU)WY((Z_i>DMxn&6BgIgC$TA)N2ry} zIjs2CG&QBMlrR1j*)m5~m^YUvqLkSr`4<@aArJ^~5$m`dpWMrv#&ZwdCg{U! zXU8LL_okCBHH$b%jKbJbU_cVLd{pT375CQ?F;Ux)O2b-OISQeY1T~m~O(Z(xG-4y*jFAyS}B4li| z6kEa>ka~w(P;u|e)lw6G7NNjs;J5PMhwAP#ooCB+`>EudyD^xkt0yC}r1vkzmg_Np zd+tjo7AWv%YB(h_jD4Y<5X#B$kc^Wds3`-6*f}I&lP^W+667r%ou@ZU9EGyN0sfdl zq6r|>#NODZTuf~aYs4HdXyGVHt+hbZD05g4b=f^_gH-`9;~nC}N{B+Rcxy?|r!Ef| z#Vd+YE3sVGhUZT2QYW(qW^$waJA2C<51#e zOFKE(n;NU-YwsZ{rjd7%&vRP%qX)+8o7B67ecygu8ctIZ9flIQz6@AjDbuhnOWtP{ z)ix(mejAvLjeo>@0tt3P@0uZPO&&RAe-kD^%$`S*CtYw?s^~%ZM$^Ed1Z8VQWQF zc8<It6Q-HY z`dhG8X6ESGmq^xU^a)-iFT6L+L~2e!9Rf_XY^rp>d-?ru*HRYG=gp%6qloW4m+=z$ z5E>2jqZr&y-5d=V9ek(9bzsLXEHf6HDq9U(>5zJ2`O+h+P7zNP5klea%JQz{h_xxZ ze{3Zr@(xE7z3GZzvW)gKvmS~*4`(TwQH!{A$6*;=D__NnfgpznerUD%gt&BX8yj7}tn&Z>;U(z&@oZ(J1Nhwi054*!gM%{>?Z(A|hWH&Ag zeO?M^GiJ8XRbVE7S|HM9iStBgKay}n3F|v@$K1mna@>%h)JqxYAMD^sFFEi0Gh@LW zE5I~i0P_~P@Dr)3Z>E~Mi`8e|Gf9!sF^@zT`-GM(TlcMimaE8F7m2L+-tu29H*tsG zzym^yfs=lwnA!((9k#+S!toNhZ4nQIdP?pG%I2THrG^f=7VkZqtT3$r&4qjVLrN1SiwZWGJ+`zRO{nACLR*K8U#|5%w5pH_OWEuINo zY?m`vkrqDpsOhRUDUw!vMy^c2!D`X`vD=w@>eyaO10mxbq+t>H-gr@(ePkBhM z_4QyvG`*pP^78ck!I;TrubSuTJ6gRsi$wW5njQ6HtfmYrx~+2N^tuBvLdGT*5;b2g zB@!hXgGecFZ6(-|C?Vz5y+oC*Nk+9Y)*lmk>>I7?*a>Itw@{S>4E)k*I zc2=5q_?|iZwjpJye01U{?P1cFj|*`g_Mc`FzFPRw5@}h+hGssZ^!+|N5I4+NzzHts zXHl~VhZpO&y@H}%@-=0jTbj!y376B4K@*eA%Cyl|%H+*_%(9@N6XI)EOzIh?!W{(8 zCi_5mCWq4B^^kCm7!I!flYTXh2A#cWRjU9+Wrn7dORWY0%5j#$GA;WlyONH)Y@^vS z6O}Kt-eS$|vTfl;a$R=_2lXLu9!bg-Ks7Dw9Qr48sx`j6GnbjWE%1i<1GbJYi^u!l z{b~1UZHuE_2#P6r7utD=pRMb08hP3YHhf`x9jOqoX5^ZRqXWI*;COyP{^3^r50;Yt zG4#-UK%cz_MbyHJxs-hW{gt-LaF$e0$emPjT=#~PO4oco}a8jE!`mroiO zBbI&D3oipS2|UaCX}rTwcky#&Hbu=h!1w5?-_JEaKYttisajuu)IZHT%8VjdCQu1~ z{Ut<+_qX(9k3mA+31_X;vXmqBhh!;6%-K+n+i8)3L;+Ycnqi@;Cgj<8L_F_e-oCno z3>xTbhDPET_F=p}ufwH{eDtv^Y+KCgp$=@fo0@K(*#8yw_d^Jsz9#lP3}b2*YUi*G zsnJ*Qx3Z4~sgkmr$fA8vg*1JKE*AZn(9SOMJ z=Esv{q=nRig^r`pMo)4YbAF8oXJ-3d3tkpnh;|4=sQgDlJJ#nfqosB&#&9vE7+_TO1gagTrwz!Pq z&qk!Su$sJl$SKSD@T@*FiT}d0yG=q~8l~!mDa3GY^f5#O?WC+x7ooB*(QRi>|t( z%Ag@r`PM89f;&;KPo}LLHn50A>x4y+OH-JXAIl+z`Q7lFo%VbWa_(c~ub7xNTP;cJ zVP|a+BF=XY*;N#y2cGcVvJ>x8T5@_pBt;!2Wmj)EjK#!OqQJyhjZ4ank${c+ znfEgZ@0^OCyrr6T);xW8cy!&q;F8t1sJ5Ai6~Q+cOI9^zpDkr|lBBYA2F(GZF5uf# zg1G1)4euy6AxSTb*@B)`dzHdi=1LJpSuQJyV$7B3OAE|Of@tPsH=~i*KZ6vh? zr7BG9b$sdbe%!x*OYIt!rNCXvke7L$1JQiwE@P%R-D2V0fY;zMEiu|pm|7PU#yKP& z!^@?3i7@;Gg?bX3-b_3At1fpOmM23x92+UJ(Bl>Kp3;f7WS;pr?JWZdi4Nuiq4&2L zNk1iN>xAT!9b(uK7QLW#3?q3f6#vBj1$}_*%xL8AM~Fv@q*(8TUcgxJQpQpWY4(!~ z?H@8})E}J3TWM<&tCCM1GU936aYnqteEqS(i;&hqBDGedpNTn~)Vcf>vs4Jel7ix0 z19nh!arv@rCEEl1;SZIpQlZY|6qFxP1Xv-ErCUZ+n^O3v^3 z>-Y|$A;sqPN}LBj$j999KdEbo35>M4Qi-+1$jlhwaKD`>H77rBeybo`q$BlrrL`T z>;msenZK3Z$09{5?gE5>^yxacXw_Na%ZJS1Yf|>J}?yC0Lm5V%IaqHEt?n zCPGA^PhFaPx5^H4u<(D^N0wIx8;cF=ZZTRZ_fRAAnh-a{Y8oR`-4?98ewIU>? zrCde@=0&V$UWq{_?WinR9Pk43tya{_xRB2z-;RIRyk@~9WuavaWX8Pn(L5T$c+^38 zd$*s2MD;f7d!hI95JE=WCj>hxk=Qv3>@S3!sBg)Vby|H!wb^JU;VNOuzM!RKyyZPS zoN~jbwVa;tO2-W+D*HW}TAoNmIf6Xq15lzzf(LI&mjv|TUYeLM-YCdsh zi0o%5mIF5?W|KyKK+O>McWC)Z_dC3Jpc;pUIbn2K?a^}kearD67q)J$b>-woeG3o4 zE;`Da7bXjxxVuKui={tJ=zq^xdJ8wwVJQ^@m;WyNEm>zNjEaDF!eQmNx}~TT?^rs& zClk3#H&5t~1^J+vU3pE8icxa}uOU_P0mNru_L)S6+ zh~*&9Lb2<#MEYA<&PSi%Y<`ox@?V+PBjvNtrfk{te-(IEchwh+kzWVAO~WO)6Ls>N zyr`aHB(848>Rmy%AP*(}3!gWYD{UgKmXF0p%8ahWe?Z4gnYn)A#te5$>1u%wAro8Mj z*2O2Kph+U?aqx$B+^i`LqcGBrz{Cei9E7j01+R8=?!1Bm*>!WM48*?V4#b_m8T-wc zz_!J|VTg}m4<@fsggb^v(pw4fNfwru!6Wg451Uoct3M0uzIxg!Nz_$I$}teuUC%3Q zbQ?Hhwm787In*fX;v+&=pR9}>7!j$4e5STgO$khwa9#W=QFJsn)) z5v-p%B2u&YwK56n`4pl;$942BsK92EKiKE^n}&o+U2yf!CVr)b**^oCe$X5V{X(%~ z_jTdF>+rN342cgr0tEUORE=nDR~=nZ4wZg=vAm}iO;VknC^K`>q& zO3#sj_x|_0fqyf_yINNniJs_>e*W)wz~dNj`&gU^5}FfYPQ4<9`NlYp_y1w-y}z3J zp0Lpfd;|rg3j)$a>Am+NH8cqd(gg&hOYcnq14!?^gGiGu1c(p>>Ai*`y@uWc$vuAG z_pWvSfXffAunfuB=ggj&{X8?z5D7^z?#Bj7AEcE1dmH|Lj$7l2GGoR;5H0Xm^!xpT48>W>W z>Pm-S+fVCV7@=rvbOP2w34{}vOC^S-zbPr)lZ=syLwi%8T3OtgU(psz=bL>SANZ5` ziHaDXNr^Q~Gdu>@wVciOx@%vV2baCXs3Q)Vp=raRam1<)k_Tt>-`KjKNzh|WJf?6*bi?mk@{0Z`BuCMoVYJUFo{ z&0@~Z61CVyn52|4P;xrrzr?&ZeLzF259X;eQ&nQw-Q9It{CbFQw6~O-3%@#*c`lwxdmB*QAgBeonO>%EXh@G3ie-)ac~TIpi!9-}o9GC$p0M zELQp&TBI$tZ^-sIN+#w;iSh+Zy9(f0jD?M9y<4{ zPAK>!E$;_vu!v;Ier`Bf@qJG_^xJE;ioC2o*X zL1W$iU|w6rX)+PnkO3^vrwJ$Y+f*guz{x`ppIU*43;ZsNluI6u`B4;B6Zd}9D>sWh zJofhJf{_7CY?6e znML_T*6u9UK{JTXJIdc|z|5Ti^P5EWsGeS~%LVVetiEDfc1VG#gO-va0f!Q4#OKer zhe2C;*xNMt%~T`YRp^Plu;xnwg$an)iO$qx@eMs@t@unp!}D3rdFAUOfkS0xrP}x; zy@(L-jy4AH5!!_DX4H!8CFA}UyN*y&mcZ6=sfps?;+e9_N_7I}u(zv`307?mymY`y3bxHPI5LeJ-3v_@4I>Mmen3XR{EC1nsxZIVWr*?a zcQEF{z#MzmxZJlf0NQ!mQ=Uxjdg4_@p(G%pE#UbJXGb?YgCtYLfYgqKr$m=MS*pHs z!9SEq-EyGc1XI{-!1)q;iO?Z&T8=-EBhzg)6bw`W4@_j}K*>QouQsEtIi3SeVEHiH zsloXYp`KwCrd)w^>&vPyOc60_{I9}1WkT>{6r(~4@kGDAj1T#B_cJopETQH6#A=>D z^NiZMM{pf;yGa$wII1S|z><8Rlm&?X@>`DzCxfyq(#vEWxKf5%fuY8`DDS1IiHX1) zyRrpuEbdbaV~&)6MSqG=_NXp)LE>j=19P!FB_58*rpn{vW9;oLTNl4D@w2494<%jf z;Itj+wv8aTTwHy;QZR!Egr~q1#!poAOl~kxpS~C9!-dm?F|(?FS5hON;&}feNj>Qv z;HXM1lyr+DO=gG@KnsxxFP$<_1zs)^MhZ*;e670E>P)GeyS3$J_^;YM)!MGAh<*4y zy^^N;NBS?Yhwl6VZ;D2jn|$x6W$zTmGu0UVt|Ds!_f&_2FmL2;rh6M8uJn@HkI6pg z^MnECqiM<50Hmufl>I1?)l0CNdC0N)9nz3PtG5qq$EseO5|#AF)r3-zNSaBICkrZ= zVUn#Bde}182DW5*1(YHGJZ0h_lJKuqO9l&O(CgA%p;*-=>lG5a>kmb=9;g&dqQnnw zz8qfNxpe5UduSle+DlPCOYO+yX&S=g6=_1^`_{=*zOmxSBlxu@PVPiO<>h66BpRdF zm||CVV%|yddb8QIFq5_sh<*)a1|>Yov?C^$F}%BLjr6Kuao^ieZSdtzu#{`|WqC!r z%^`xrMGFLii-WqC1VB}b^vcy6E`!^@s&cYtqc3?|w!1fu19ge4wNB`8zMr@2 z6gvD`^^mH&6N4Fguh|0XoM5y1sg9#hr_})~RLg*f$idIOgG>09Y4ajbsR~EmRhYX? z)%G%+*6IY*{ZHGh&3(l8IEC+Oe+zzf^eirQU}FIIsw=BFDn;p=*_%AfKJ~fEvni2c zqlldm+PrTj!D}(zi=IGxx$hXE`N!1Cp{M#|58E;4}Ko3J(qNwO=i`=i-J?r1U#A zFCTv~cCoT09qbffQ^WZn=AdL`cRpL>Eo#430^wtt_u8zZZbxlfjgTwK)?$Xuv9|*# zo2MlK808wty=<;~1(E^*BRTN)jT=T&*D1yv_azM^>ZWDOJaCN4fySa?rPUKp4yN%qk5tLx@TQ4vUJ{_oOqMfpAIedDE z>1M3P_Ibi@B$={#&=4Xocx$v@RLe+?o6i8yKPevY04D zgc!sndk|`y9kjbWJ6p8J=zrAtrI-|`v{JNxDz8-IcRiJ6;?wz=MCLXae)T5to1V7p z^^*S~ddMZ%1v!m~tHfS-!{NRI`V+#YVpfK!<2M+5#{C0c#^8lo3!3?+z|Ebq7(fkt z_Gh2)kU{&&Mp>TMZcOun8_Ffvd%Zq3;BL>1$GR6mKjfD>eFIqQgb{p~%E0_1=Jc@T z65|kbmxbwAlJi9y8xTEG(GAlp6M{tFpAeo1rj1y!5yTN_Oc&q%V+*un8e={pExs_^ z^tuE)=;(|z&fM`5b)}h|7p8FOv#MKWZYGTQr5^gv+#UGM47CB@VVvr@8p&qjweuSe z!$joW?qfMqcB48wf8Xiz58kU|Qk^uit-r3%yIidwY2VF0HM{8-3;NLg{OnhV^!JE5%Yw(&5o@b4AHNXy^xnS}sET+m&x`S&-fX#AOn%nZJN zNyKh1{7ZMFFSc{!?v~h^oqRRHr{R3);H_+I9sXha%H>QD4ETn z%bnQH1bxaULidJwJ=z>IoTLr=ahjjjzS#~qmD%xMDl8QDimdne@2_9= zak9G+SP8B%QXEo~-#?@9vc^c^=~rW=zmP3HK#F`%$daVI8+aK!7v> zf^aw(6G2QDZgEcj~Wi&pTosrPbGvgBF(OKsVm zZu8rd%tP#UuY7|AqUW-G%hWd4b)FA+EIU-XU@ek{j2`%?5naUbQPCd!62aJ#q3Pl> z0%Vw&qh7fuPXVR!EEhk--u?SGY@AdGeg;$Q>}Al=GnQBCCy5nilfaZxBf{>2f)!#q zv8ABi99Med)Zd}+(m&s;dLwGyaa1?MT)4KL$cov}fqy(bmy^6TWMl9gd;-6jqjtVR zU6ST6R<@rIGfJO9DOOokm{q(S=j(P7V23|>)fIdDWUsFRQTlf+TkVb$5nqCWQO$KN ztIS`X1}{9=GP$g%!LIgVqeJM#q_4^juh;v^%+HVx9UR2&$kg5r>=0I1laLJ9?O{c6 z67Dyj%`mN(PrbS$<77Q5);3?l8xMZ0#mQb&F5lf`$pOCS-d_x?T5k1QQaFDhyZHW} zq};$k!}^23>3SX5)`df_YIAPzaYZxGInoTr*mz&&TfCLKz33&HAcul0ruJgZT5dj0 z)HUdXpHFO2R3!~RVj{~ZZCp9_N5jn&r~cSW}h-W$?d!fNXkr9Y7kP z0JJ}3^}UDwX+lA2@3n-~+ z0^CTf4Qx$R&~ureXBUsNz55*O zpu_fKKDM9Q%pxQ8Mc@EWGv?ri?k8|Klva9Av)Mg+!56rR=vZzAq~|0~CHMjY?o7>K zV-qmJC^GhC(iAR_M%olN18gOJBQhqS zpi;feUAd(~#{qak=$x;6;t~jcz0c38lH6u~E%>{)BwEZlH^qN{;0C+`bg|Mpj)vHB z{q7}HzfmRpeV_}^h}fUoiSQbCd}u3@U~Mhg2;?Z*FZ{8KGt^NsLbNM4c^|$1VFb#! z0Ib^yg>YsK<$CRM=r}L<3^mSVL=;47B78Om%)L(3*_OP}PuD0YpdYuHzbpifv{R_B z+6Y2`ba)(D#QuGXZo_iS^~KA6I~HUegU{biAQ!r7+sc7Cqvrh&+Fc4EjZXfKj)TH6 z=mR*G8h+C#Tuhl%`X}RS()~!9yl+gFTZV=+B3kYq9-0KT&WoAA9|0o7o_o{hXC87# ze_sL;M&F4+qhPX~uUV06tez4QrmSkulP13eaLN@Iu$Lab^}^Bgs8)5oEy}|z>jYi6 zv-qqK^7;cAn$Id=EMiWUu=|_udJpLsk4_3vnY2esYdneVNdw;_yV%hRz=)?>hL=gO>WKbdmP0j0rj;*CF zxkLJqQ|ETnBj(Y%%9t$C<-n1^G&8qalsFO)Py!MM;<&H3H}iX%z^o5sQ={udssBMs z^J(RTj*PWqz%AYK%|YPhBDRZ**W0lSQk9Fnbq55l5mn9V9O0xP>?MwkNW`(r(RB+qKg!9oDG|^VdWcBUkz9$=;A>wShTSqr-;TZn^jtrO z1Pb%x;y z5(D~g0k<76?9113n7fDgW$Zm$+cER-b5pV)Q2?I!=G9jGv{aK^?M@#)LmRdxXz!}Yrt;F@t9XG_riU5;ssKqRQRhk{piEm8lc)+BYU`N zu~E&gek01<-z>W}a{!Bfrno_re`n`I3G_LFcHUMEitr4XM!W$N_O^Lq1J zax$$6VIB!*?Ou0expq9WPA4P06~Pn zjwVagX{pJ{f9n>9Or{PaK$$cGPHshANU8Mp0I~h2#UCMUIncHS(sH0XJl9tQK$`*X)9N8p&(S75u>EKp= zadiWGbr3L`sulNjf0jURi&l7tjS(ic_E=kbqpk4^{oO3WI~8_6!pqUe@=hkgP6o4` zQ-AQ3VP37+saIyFfl$oF=OcpR(pIn~^0+|1!fWt&#CKY^tN}^PF~t8t&I3E7POtUV z$rx~Y6xtHYLyrMFK0B*yJ3LtSL-p9%nm0L}-mo#idYj6o4!Yg{t+ls_ZMsrsw_9&` zd5*xCL1eJq4iqjw3Baa$ga^I^Hxtm-B+REhwKp{#qOm-tPyhYSa3$Gj5(P_4IAeX) z=R4oUJj3H{{sURi{C3?Jzc}miAos}K5Fb8^Rv-o5dP=xMu_pdlvD?)#v((ABgON00 zuhoKPm@~yL)2<1`6!t??D9Wnp{7e`Ep5R|)J^l=Wj(<@Bt{}m>wkcDBou7y9UJ<+6 zm*;lYhEJHSDenV9HRLVM44|%2oz*{m@7LHTjbL-q56Qw6=)a_Wpedw89`I=!kQ zH2gqWX~2y+QNS{ITgTuy`!-)=cEP6JLEiEA~e-8 zSTA4ue3rMf`-CiIT0Z$kRxOZAdq#h-z3!EMG~3(l>YeMd|3;M{&@(oHb5%BCGBRC1 znLB^yKB<|B_d72qg6nv2-Q)RfwdJd*Ko1fa1iQ|P_`>8VmHg%jo-^YW?$1iF6i!D$ zCAA@?CCNJ|P<|trVlexmjogQh78V61=J&fqbwYI99*33q1*Cr8`Hi?(wtbSGj~h8K zzxOlPihI(mQ?c0a#LDa0kN>*{A^`r`%-0j<7PlLo!Wm5CSbdMz+yY;nVDG*i$@F#2 zW%GJCF&4pX5)lrJO!vKMe3yRb(U&~T7X3&z-K86WgG=q@iB?>5ERf!M@v9`omP2nq znxcQQ_{Tume%-JCkqr`~?`-1%TZI)hq}PY)oU$1o&6L$omp(TsR(?1U$zB`wa2zK? z`p$@dFi7&oJNSr4P=A}l)Nu8xXmQZp8!YRj=h%w{qWS-_ z88kL=KsChwkN@2hq}wlC4I}*og%Xfm4+#*}$89~Yx+jnmOG@_lU2YnCPDvy|2S)@VxAjnE1-5zKH2;8Lf);(-(ike))?J^Y$Vl!^qd zM^WKMNz<9oMAnNyxhTb|^W=uM<>u>9Q>lZ(1@ZrV7C-P=Y6QA}wum@H$Y@n$ZAo=$ zBHCmDyQ_)jn&T@#;k)aKX#N=z$&q=?iq*yR)# z=Cz=lhtYWd@4ngajtf^=n<*ii=zDj#TS#>Yh>AX3i}5CGGW75d+*3;6qs)tK_mKQ= zgD||>&HKe#;jc4R>Sok}KDnZJY*OD(C}}ve%v9jo#`LDULt5s{Q<}y#-u@W(d~NJM z-xLasdjoxcGq8SAAqD;ZA84tR_?{9}}rpbN4hFtjjj40-B>t6=Kyi z?6?u4c{N^r2fIH~oluWfc{6{k{qIpqKq-l#G;9J1JsdK^EIqDWF0mHr?JttvrUDJd zR+%pd!(dzkE!7U0HYI{&$}rcZ7^owXBMp}3l%-Q++Gz9u@mK!)qQb0l#nc1Y#J+$0 zm4Cc-6?_KhAWc$gM4I*H3lqE?xdi&{>I;Bme&i=R3vm$)Hp5Sp`XFy!*qKJf+dIyJ zMnjr9UJ`V$yK9JX%AKyRs6dBQE-FkZPl$D?8vO6B>eOoT&_WpFTTHYGh;Dyfeiqj* zrc|O-uC+D zg_Srk6#`Zy{^(G8#I(4Sf{=yRdI+op=w7*9`?WV|=t}4DiEFDkc*;-5`cGmo zQXqwF)?zBD&zb^SpGTO42Jc<2;4&9m(5NSJGmg1r?0WmR!c4@HRN6(0_Ye03v|c*6 z32K|W#F4U?zNCEE;?~mbcE$~vAYlQY6D=?ioxV<{P>QhIbdyMZP&VLVq(`X?`X5ON zB2_+ya1S(0&zTJK4=KNR&LXHKlr@B+;6Y5($hiNqdMwCGZ|;8WvR@uRx_S6RT3WW@|TC+gMOXf79@-TiI88e z%K=zXs5oI{1CW$rv2$x_OLpTZ6xY570{Gdqo zsL+!P7WKqo_cE%Nn#&kdu+VVUNa)P}xq9EiMzF|!<%hS$F%1w-5sPhZSPF#TTe4!d zaG*8`th5(*cGNW@6R0Xb5#>fmx4W{^v(*o{zOI=0&r&oC1(npL^vykNeKJtzWnfL( zD*(#etaAsaMSbKlAj%F{b-gyJE>h{UUJS)7AxKR9FWX5Rmj&8>GLH|9R+hFeR+JM}`N zs@7zd_7lzcku74xx0$^OcyWx1kgolSd-OXRl^)l}-aG;>viNVQ0lwNI-Fm2|f0YFZNqF_nxO#l^<{c4fii9Y||;Q3~XE&Z4<& zX`TL={-1jyOS9Kb?Wb!o2|CUI6C96MR7>ywK;$m`B)dl@0FXto;``QozPc4tv~-L* z=lxKAgR@L)+gyfcj5OOLTlFz2?$b>l**4l(7EF@PeJ4g7<~k_dFJlQV7wWa^xrNkU zbMllS_W?oPrs+x(-Ky!mn4VR-c}PD$(VBQBdeqLT!(k71ndAoTmy6D>AC}pYP-Sox zXNrxDE$xyfQ+5sLCi^0X_1(($y(tH7{rwK0^P7OV8>gz-+oAbwoMJb%nd{tf?^ASl z5*;1@BP=d%%(U%psL}^rCDDo6SA~DLuCgeyPGd-Cs z8Hx3BOpeSb5l5h9LT;((2oFz)i?G_BWjWKU1|$mHn96A?@~3h7E$~J`ickai>TaYxYJM+>FbTOoM)rmAqSwX$baeFSC5haw z_@e}E@)S%aL%AAO8#Oo)4O;i@EcjB5?hd{~rok^_YJ%6KOied-;8IqzdiqW0vQVb3Mr(i`Ruh!R02r{_}loAb{ruy))D|3PJ!79t50G z>buLqc6)It=HCJT(;>z(AG?Oe=zTcVexyJHpy^yo0DwB%G+{5|gmk6f0**{K>t)!P z(*TeUGHdy$4gfeW+JKB$3^!zE`QwDG?B(xYm%32f2Cw0X>P3wjzXT^^0E=yH+M*&1 zhhJBaNTaulFq=hKdgTPW%!xR*LBARG!Z3NI>?vc&#=wGDEL8qIjY^Bj?w=*Q2V}?J zsoVcT5hTk^PEsJ6i2dE&`6ef%9A+RGpcpu&p6v_f`TEZ&%6Lsf)hM{(ynuB9SW?ip z4{Fq65f)5}iQoJHsmDe#X3uGW0xg~_PEv}AjrG1&c*y*|wFeSa1OXBfRZ^U^ZHj9B z8nG-bj^{XF4y_45rfBJdif(ehwgruKeKl##}Sl8VHS2H?c=8tKr+y7Z2FZrag z*M~tfdCMNBRe9*Hw1_XN**p#D z(GyE)nsKx1r!ByQ!x66aQ`f`~Rj1b`q+t{O(bmQU^~*n*t-tH1?-gW1rAN%+m|AZ* z_9fV^>b#sS@60>+w4lP=Zwh{k?oEd1Qt~9~*qie=dk>*%6$udh$>a4ZN`7l@dxihf zImCW&RD^JpC+}WnX$CLtd@dv4q6wHCkzPWvl%Jqo?9ONgJ8<*)!dC}l8~fIs>mVAH zTga0L-%O^Ex0%!9{6s9@Uw1hx*#tq%m5QKzs6yrWq1m^5QE=gNP=(64zG&R+SdVn8WfcQ{EWvX;4V0LH#qpH zC@(cCoIfh+cbBc~^>LOn<}+lskZ`0ze-Dek=3DaHd#c?Nw3lTT-03?qU{kN;fkb%+ ztPGfErGNAA_72>oV1#W<8%J(;osJk|XV>zs)@_F+*Ul68SW;(7>097#q=?{>- zi~xij_UlLD6&E}&+w-ui1}`{MT0WWV#x^&@q-4&Vji(bvByWddhjMq56rjKVUR)Qi z0g7I5WjY{tm2UkqWPB-NU%K}x) z9QAXK^lSk~Ux8|kD!X4L;v=fv;@UT8G-e&;qM)QiraV^gr80VrhE!w>wsegh4m_A! zzFr~$R$LR*7meLu#9TN9pKSHgyPwgSp?CR$HXKsG=VuezhRx0~q^qUInEaHWF~XXk zJE_J8Fl<3@Uch)0kBr?LUM+<5PBBIBB^n^1J~INKAa&d6$P%Dla#%fWUjo@V@IL^U zCyFn?-kjoteINS}v>Gh?EWAPUG&xE8cB-ZZu)vy@U1h$j-ngLkYX5`l3-6BL-y3Od zYbY0gz@sn(ulB*w10*>gZ)=w^2P8%80oMa&Wf{a|+7s$wUnDTP z?0&<+wh*+{4)pt-8eY%FNfB7DhGLgR5rwavj#75fnVI>#rpc2pFA_k3MC**d(MkPE(4Dmht#2W*{**s*dz*Cs6Oa`64(+~?!}MXV6^=-PFK2ogy$2Pqi{Au~z?OVhAJqFBBz}YV zs>C@p&0}v48U24x@dYkAm|btw1U-97_JyHNG}ek^qb*|}@qgnIrtAZ$B2DSs7+J9` z3z1!z?t5fvNKQr6WF?sZC}c;9PKgzyg&^pPCwawBbn0RF36N+rs<31Z9{X>LnFU0i zAk0b@ZsD_E9~Xpa83Bq%;KzMBfsaEN0=8F1Fe7Zd9owIrkj>1iJzB;Lepd+&HE2`P zh`CDtt(?ory_gDfuc?33eo9TS`^S7ZMT4S)b!N|^_wwm--VJwxb|PTIffKt^T!6J= z*Wj`iG~{F=WB=$|AOdT|m1d>-_U3qFN6O@~hO7;$OkB@&RfBHo&1ajOR2z-%_DF)? z<->IzN~0@2d1k43>%oeb%<|{?rC1W*c6*>Drv+3JO6qmE@;yxJ3ux~(sg_10LZ6e*LGHL zlOewqL)FBxc&gMNa)bpSjh8+Gu}RM;E+)p)+Z#}-JM)mw>@eX~w*#hRK>BM@xd*%> zBn|rVqsz;T5rAFFVaGLilOYkhliy}EHtMGRQkhb1sPGhcv((=(JS$pjUiLhEQkIJc z3jTRlJ)t8}+g!#KOTkgXh*yr=rA*5+W^aZ#H*NB5b(N8pUcJVwe0j=Q0s?WAkQR=8 zs5Yu)$r^B8Yy~EA2})j69b*2n$)UsTGOtYzVqAQN9L(>c@ELrzNtzdPhiuA>+j;Wl zJy}f#gjG~k;J1w~w|C2!DLE`+83^ZCJ7g1z>poDBoqgRwyWwWY!9)B^Cgm0PV?FAK zdOt!k=6AutE;K_)pQpo4Q>KN7p1yo}c=yX(B&+IQJql2mwc)+s>0Pz-w$aU+lsBCO zhrR0l=0&wi{cBE^zl`muA?j>z=dxbsrMBmJBiMsWv5WJw4%Co~S`}@u=a5Us)l?11 z*(Fc&|C^+ie*lu3v;nQw_o~2=Nc}Y}w~Tt?*(>W-aRNj<0f?QbYqG)+K$ux4(l&w2 zM<)<6u8}AVeerU;WQAwqtzV`ZxW($j+lQ``391-OMAkg&LSRJJ{#`z{N$S8}I5Q7B zZU8^ps3GqJlAG3M__-E|^w|+m`H&T*X+aaKqnTbRusAN8v)VA7Da`V)yJ;)OYgfI- z|C|ep*2AJt%}wj=e@DSFqi87w;_)*uM|WR5g?GdNkfH^Iw<-To8aNemygU z8j4AA3GxCnO{8VbcJ@#TuXQPxv45Wd0zr=EE+nArfZN(jxvM`Cwt2k8lsLV;T)>#N zXg`_aaGhqJrA?*xhSLD$f&IAWw3T;xYi{blIMlpguU6i0fZFlh;4(9+n5u@al)jv< zk^u<%;8W!C)Lsk|0kG?X&?xVJ83f_)PcJgLig=UeQIlpIDDhz@?}c-3>=7UKXe{m) zWgfiU&UjTJJdKDETfCie36KgjhqCCZi>AF%PwaW6RwOit;>m1C2G({|DJCoPB^n+(!k($<<0YrbGAf~AwvnRXP9%Pj)s?YPh|*HpDJ{Xd{0Vr~RL<+kw);sxK(d8$Rie>fuN8AM*lP9-c)2Jx=#X3E+O#Id~v-yjH2q7KVFzrfjEtp~I>J5`s~Ax_Z6cz0DsZTbFmT<)`CD0q)sEjZfiCYc~3EC<+A5}Vl#Yey4<+;JaQpdZVT~@T|7CzhvAx&14dZL<6Hlj{$18eg8RM&>?L3x{KQ60=?(r z*kn{;P}6$58a-VTd|M4W04A_P23oy9OvOswin76onC4rA+}+-Cz}06g2CxYO#_R1y z`1K;4*`+;j@LQfqtwMGHgx<#K_0FN+(h^IUNA{r1rS`E`U>VF61srbS>!}-IsQsfC60ve`I4WYXV9Fjil?i)1XBkh1 zHTo4L9Q^j&ryXzp>)UaNe|p=^-5Q@s7Z;FrZ|TWqe&$~Uze)e&hArRnc1cYXE1UYb zW7F$GyeRDL726k86+YNH%YZOr;Qn#&O&WH#De&7X_I(=KpmVO8CQbJb@7I^Hr&wg0 zud!&>VpCx6OviE6zdBN#Mi}sBO_T2#V=0&OvFvI7uV$ePXR!Fe!G=?u3 zab~F*UO_1bWY~G=Y$3kZqc#{&&Ip&q^d%&W4e)0058ssT`;s8^*qsYwn3eN(VJ^Es;Se1WOeAuR55$d7S zSf2_Qiwvy_sFay!;vs@xmy_V7SW*H+G;2t)fT@J&(>p&u&<}16@Cv%~{Tw&fSC*3G zY00S70NF1D9gJZX>#Z5esfMoEmBz5$b&5E5M-2{{sYLb?g0AfYf5zDc>#UZFU!mag zv9Xo#@qE(2K$!DsLtq!bhv`fmcgbrQ>fR@yXCY=2KU7{*Y;aBd;ip#wc7}veY5wJ2 z8vD9U#83rNO)72xR7vU+&Kd$hsZ-zdiw?;{$;@%eL8S2~{>kFT%&nM_Aq!^#(5s|= z@r;JgzkxNW%QnB0RfH;)`*bXAphNex_i7cxbSd$e$*3N-3`0NCv&GoZ^3?-aVH~09 zxcvw8g2*z7uK* zYB-)Y@aHt)gq)p$x2lEXE#{eWrj}N%jb$6Nri#Yx?W>3pYF1*ye&%31O7_%ICrs+KSYYCWqAG-U1jbKrfg#)wGtJ~FbhTC=KOtCA*` zU^OLve9HUnr&&#}UtK$tDQRHK)L1#FacTSq{TF;3_D|IvogIbkfqSW?HO2CNQCgMZ zJJlv*)YHAHoLW*@eCv;$llZ?)(v>E0tqX#LOD+r8kLY+vNJSJ{yz}3c7Zi-8$9W6E z_JfR&Er4u*tb&F0dkrZ~ujUDEF6GaiE@ywyoBeJ&<|-n)=cT!jJ0or9clox`7nwn< z-S7E-T*nr^vagzXLKLAV{(kYDd%y?uTT@!MR?_Cvi*Uir@`t)o;fhdD<2jrb@}0e$@tC#Y4L-k@UsPS{_#{< zIIANYGXEOB{(eGu?;P;V%eIEo#g9J&Guf1CHOL-;5?Sqlv zQscQx)JPJ!t-bx?CaW6(4ya01>#@IInpzyuhpMd(K14i#oD;Smw@3`?eYG={aT@)7 zYPfB9dNCWW(tWaD2(Y;S*+Ud6m^s$`Q8ML{3QSt5Nx+PAvSflN6;3DS=Lz@iE-x;r zh4d%CPw(-xAKiQ;)9c=dFTHyNYME{&xo_l+o8s(9 z_f0+T!1WLP)Nqm3surfvUw3`4vZEHy41 z242@a)Z1>+RBPoa?-LkcuxFO$|MGPw-8;yYnYW0H-6I4mL?5>#a2@+quL#mV`}+Tp$&&+SUeM4?_wgvH4!%pd#DG|TZ^ zPXa!XvUn;3O0~?&t)KQO-#Cye_D=?B;R14ku_R(#>Fg;+uAW4QcVOoBS1V<9#`%+r z1|L7}mGX%2J;qVzn}3mn1{&*%CF`sjHXnpC4TVy1X~-$Jl6I=nG(feQqr5H ztJ${`gbJCaf$i<4zV|3qE$Ypl=6p!S*C+O{PEy(!xZB7L?ca_qI4%`twF}qx8k$d0 zuV6?tiUFbdIkm| zchkE?Wd(KX0FTOP9FTBj$I=o+mMfu6-^#;78cKx}*}Kh>CK`*27yLcjmgC?zs7BF%&N?2;PglqK5O z(CEBKI|ITQ1sB8z71cTUn0)?2IS{yLIzb4ZxKVVlXf-V{sSuq%vV119(&&?=NqAi3 zN&Y(_S&>Z=OmOc30f;E_y#Fn8-rg>!LDo{}8h4m)go=2+#hjWtJsTSK6=9G5tn0(V zVlm$y7c#IDm zFgT<8q&1pu`)W+y$e@A(Tr0b0CsG9Y4#{DfKc8DVm`TJ{joE;eqD00+!lGY4zuq%l7Gu&5-qnFQ8Mz(nL*tMKH{$|eFH&JP|KE|~puB30^ zpnt;fv_^EafjBgi0H}FU!W;cADIhBRl@X2Uns zQOtpG@tfFrL;1T}(TvHFkQW~bH#$2*F&K= zP;|`4sv*sSGVeD!Bl%Vbm+j#^w2#J}zm9XJ3`k*#36zxP-P||Y^caz~PCpev#C$;K zMT+TfMGC=`i-s`ZD^nro3T6?jDuAS}*sdWfekb8th-bw0aOpv%Z`A z$>~WIKRxgfyD!HY>Z%=(kazZwm1f`#MMkq-bud9c507Fh)}T*cD)*a+5dE_o*`|z( zveNo&zxYEvxmq>mBFFsLiQfm}o)W1usV8b`e)aag9HcXfGyFu52K-j>zC7x>VTdc` zxVm~{X)|a3Im}S%Ko{*vZJiz%O-imy5O80V@B!$*@(+bT0m$yo!9i2>^*7U+X540- z5EdLm&NS;)r>C^ey|jBg2-#v-g|M!Z18BVO>d(i7Hqutv_NZ}$1#Kb?q9vSg;d9%`j7 zB%Poi4VhMlO?^$yrD0l#>y)kAQw6wEXse0Mk3Z9B6 z1L1wbu|PiF5r-z`i0(x*u~O zBxlECAnipMj(sHMd_ti6?MI92z{jm^rKVY!m$ew#lho&G{<-+$Kc06NY$^A~}8Dt`PrY6$+YUxuxoETrT7p&pfxOA@B9xuK!eiuZ+|tFsg{ zDeKwTg76|N*g;iL&?^AI_FT44PdV5q_TU${Z^P7rLej?74GRVKD=l}2NT`03m zRX$S9rTWk5mu)w{;lmXX2jZI9#R>d;Rh5d`T#pI%Fkzzdd-wILs30Iad9BFVCl}=5 z7PNLhRTcTQ^vbHK@bXDZtzRfi#Wgz3aYnW`>HMyHZoMnTtlnUeNv>eG(MYT7!m-sF z!`dgIV%5OFMNS?HB2`dENJXGqVbj&d_)zurxVTt-q)=fMb4U_Tm1#YsnJuDckDWDN zn5dvL-Owjlk$t|r`(D-Na6e(AdTY~j5J$eCps%9~ z>i-k%nW#y0m`a4V<5y{-6*=PMg_xcFvcz?Cjdna=p6BlCdd8k@w z1HJ)~{Hx^#PjLuS_~^GprGf$*s@zg4vm_jgA^VyCkncB(t5TxY!qhmaM2%5P)!C@G zquC~$I{#(Am=@%R(S}K$Wr9G8`-kSGfu$eP8LlJd@S`iQs~>O_`c;e*`Q6-?H&P(_ z?KW5Vu~&&lZ`XJ%$4QhnQe~zw8BAV9s*p4tG8a(GIQZ*yULnl#M1y~iIq6iwj`*Ez z-E+ZLnGqqe0$tf>r)fZ;3u(#Xqm;E)(ni%Dhi)z;ZVzy8i}Qg zTbYh@6(yEZ}ZgFYGJl z$lv>}U!FYusM@IgyO9f>R}(DTLH|oT{|pvadlb6Wlcs`7^f%`!6-MQcX9l@%XFmvO;<1#p zhOysUF)BSuehhO?dg3spSv|}4G?bu=Jr(?|sEJzh#n`*~R8Ul5SQ(wo*gfs8kKhf4 zg3o@QiaU465B`kow2~GL6YxmrOLY{=0MJPnt}y2+QN#U2`*gmvu%p`2Kv#aA#v@&|A(}Qm9#*~NP+0l zC5Y!U`-K%dAET-0Mn?XdmA>H!zmV5#0?|aD9X#jnCm}~CzW==G!^Ktgn8DG*$Ui|L zP*TdkuK)J>&c@o7dmAwrwwbFZfvNLg28esK)?qd8D>T4*wlvU zdK$r^zv$Y*kMFPgRC<&$C>9B`%5(9U`iXXMatt?_>3DwHtYbsQrOyX4_e;B}$Cwby z#8{WfdmdqLdHC(8p@^WQP#Q10|cviEp& z}wPT$oQST<&2o6!~2&v&?-ZXeBSD zYIqq;!dh-iSeZ;tbj|IsP#y)-WiwP`Dq10`>ZrID@Ex}sY%deW$_CC|GdwNWmdw8q zqznRF8mal^9!TX@E%)6hhAfxjx-WCPCp$4s&|zAY7*x{Rbm`XGJKX3y&>wg*^K5zS z`6>RS8v%&@k8Fhx&PtJ^Yr4$bM7s)V^pF2Q7Wy}vb4bx^PK8RqBa@IwQBDnZ12VBr z)a%&6f$H)Svw%xAZjlq5m=|rGwOFNHGhKk=+Hy;WLf@d$J7=^HO3+?I-QP?mcanlDba;NosZ8vq$Ph zIL@VG2fBq>UF@^ZvUsplSjf~G8(e!+Fxz!-%&l^EA;d068QGH1fjfJ~WW1D7rAfo;`Zy9jcozBi;YMm)^}aHVeT*7wW)Yq$=4_rowe zp3UYfqe{l--Ty=}s%<(fHJHH2?=JYMVcj> zk@Z``AwTFa0)GES&+ZdjK7ZcE5eX>ZQy-9o)hwa1^_=fQsoT7zd#!$^%mcz)0_^L1`N#_SzWhc#>_%p(G1JS^1j!`qVT$v*J)Z+ph9A#FwF=&`eas-l8YD8Nntn2MV!g_jo8b9X&Pn00z=K07UDY(La8-lQtRrf{x z5XYIY=kV`BdTXBaB(p?xH&2gRp3b4jf}<_a{Be+2eb+^G^;w4HTR|i~1ajoc#XlSU zwULndY1+pif1D!B5+0&L3F+x&;Ip6M;^N9rFsSq}qhFHi;rc%&Q^4J+@|cw}7Y6oJ z>yv<)W$Op&yL{t>ezVS4Fdi667}S%kcHw^3)h#C75}G4Q+5q&C!(m-LtJ(m)cC>2yS)3R${L3h) ze~(-qhdYPIgIJo(^tvmD2!1CcTGR1*@ofB7fJ#sGnnCjMxA}0&3i7-_r*VN$pwiK4s5I5q&k8TTP!$7w)|f zlg+_vVd|`ReP6~Y7J5g+b@k0Hf5yTXgQ+2TWD2t+sIIjY%Bo2u+W)#3Pyv2`OX8zE zxr;6trZGd{F!79Q==ELxrwE7yQ)0fN0~ykCRA;TZ&`imC0mWI(N(#wlT)rISD0G4Z zQ|Z;?1P2w|(MH?Y!KK)f-MO``K8-I8xZJ?5j1+TF#ScEKChZv8LedFpsPi*Hobjeb3nX@gLu;fke z%Ktmh0`+)&w(;l8B@vdUmzv%Lxm|0%)FwI82j0HPYXI+e^PL=3q0Cd!>~ZxEyZ3oJ z^HE%w3|gDnRXYjPcuzoobp!$qFq{Us{T+?eu!!$QnKy*}O#Tdz6mliIYgLH+*fa7@ zj9tF)Pp=U@Cp8^W($z@YEUVZ__H{{WR z>)l^a%d1i;w@O>X5Xw^{>kdJn#ThY(C@*%yBK7)CVB7*)?XTW! znF0k}6iC%ig3QM62H`A7lZyT$u2B+f4APGbe z%!J8q)RKoZkG_NnmJ2K0$Zz3DP|+cUXeU*_7|YWMNo2uM5c^%{#XhC1{DV*f8-BCI6yX3_t(ld+x00K4s@VZ zF;YV3@ACg6%u~cGJ%t>B}K8{LGG