diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 60c26c1..1c4f916 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,8 +18,11 @@ jobs: ruby_version: '2.7' env: PUPPET_GEM_VERSION: ${{matrix.puppet.puppet_version}} + BUNDLE_WITHOUT: 'acceptance:system_tests' steps: - uses: actions/checkout@v2 + - name: Install libcurl dev headers (needed to build the patron gem pulled in by faraday-patron) + run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.puppet.ruby_version}} @@ -41,8 +44,11 @@ jobs: ruby_version: '2.7' env: PUPPET_GEM_VERSION: ${{matrix.puppet.puppet_version}} + BUNDLE_WITHOUT: 'acceptance:system_tests' steps: - uses: actions/checkout@v2 + - name: Install libcurl dev headers (needed to build the patron gem pulled in by faraday-patron) + run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.puppet.ruby_version}} diff --git a/.gitignore b/.gitignore index 988dcbb..32aeb2f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ .envrc /inventory.yaml /spec/fixtures/litmus_inventory.yaml +/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6372531 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md + +This file provides guidance to AI coding assistants when working with code in this repository. + +## Overview + +Puppet module (`ayohrling-local_security_policy`) that manages Windows Local Security Policy (password policy, account lockout, audit policy, user rights assignment, security options) via the Windows `secedit` tool. Built with PDK; supports Puppet 6/7 on Windows only, but unit tests run on Linux. + +## Commands + +```sh +bundle install # install dependencies (PDK-managed Gemfile) +bundle exec rake parallel_spec # run all unit tests (what CI runs) +bundle exec rspec spec/unit/puppet/provider/local_security_policy/policy_spec.rb # single spec file +bundle exec rspec spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb -e 'some example' # single example +bundle exec rake syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop # CI syntax/lint job +``` + +Set `PUPPET_GEM_VERSION='~> 7'` (or `~> 6`) to pin the Puppet version, matching the CI matrix in `.github/workflows/pr.yml`. + +Acceptance tests use beaker + Vagrant against Windows boxes (`spec/acceptance/`, nodesets in `spec/acceptance/nodesets/`), e.g. `BEAKER_set=win2022 bundle exec rspec spec/acceptance/suites/`. They require Vagrant with Windows VMs and are not run in CI. + +## Architecture + +The module is a single custom type/provider pair plus a supporting data-mapping class: + +- `lib/puppet_x/lsp/security_policy.rb` — the heart of the module. `SecurityPolicy.lsp_mapping` is a large hash mapping every supported policy's GUI display name (e.g. `'Maximum password age'`) to its secedit key (`name:`), its INF section (`policy_type:` — one of `System Access`, `Event Audit`, `Privilege Rights`, `Registry Values`), and optionally `reg_type:` (registry value type as string) and `data_type:` (`:principal` for user/SID lists, `:quoted_string`). It also holds value-conversion logic: user names ↔ SIDs (sorted, `*`-prefixed), audit words ↔ numeric IDs, and registry values to `reg_type,value` form. **Adding support for a new policy setting usually means only adding an entry to `lsp_mapping`** (keep entries grouped by category and alphabetized within their section, matching the GUI name exactly). + +- `lib/puppet/type/local_security_policy.rb` — the type. Resource title must exactly match a GUI name in `lsp_mapping` (validated via `SecurityPolicy.valid_lsp?`). `policy_type` and `policy_setting` are informational: they're always derived/overridden from the mapping in `defaultto`/`munge`, never trusted from the manifest. `policy_value` is validated per policy type and munged through `SecurityPolicy.convert_policy_value` so comparisons against system state are idempotent (e.g. SIDs are sorted before joining — see commit "Fix SID order idempotency"). + +- `lib/puppet/provider/local_security_policy/policy.rb` — the provider. `self.instances` runs `secedit /export`, parses the INF (encoded IBM437/UTF-16LE, converted to UTF-8) with the vendored inifile, and maps each key back to a display name via `find_mapping_from_policy_name` (unmapped system keys are silently skipped). Writes happen one policy at a time in `flush` by generating a small INF and calling `secedit /configure`. `destroy` is intentionally a no-op — LSP settings can't be safely removed. + +- `lib/puppet_x/twp/inifile.rb` — vendored INI parser/writer used for secedit INF files. Don't rewrite it; it handles secedit's quirks. + +- `manifests/init.pp` — thin Hiera wrapper: `local_security_policy::policies` hash creates `local_security_policy` resources. + +Both the type and provider carry a `LoadError` rescue block for loading `puppet_x` code across pluginsync layouts — preserve it when editing requires. + +## Testing notes + +- Unit specs stub all Windows interaction: `secedit` calls, `Puppet::Util::Windows::SID` lookups, and file I/O are mocked, with sample exported INF data in `spec/fixtures/unit/puppet/provider/local_security_policy/` (`secedit.inf`, `short_secedit.inf`). +- `spec/spec_helper.rb` and most boilerplate are PDK-managed (`pdk-version` in `metadata.json`); avoid hand-editing PDK-templated files. diff --git a/CHANGELOG.md b/CHANGELOG.md index a16ba60..72cb178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.0] - 2026-07-16 +- Add support for 'Domain controller' security options (Allow server operators to schedule tasks, Allow vulnerable Netlogon secure channel connections, LDAP server channel binding token requirements, LDAP server signing requirements, Refuse machine account password changes) + ## [1.1.1] - 2023-01-03 - Fixed SID order idempotency - #124 diff --git a/lib/puppet_x/lsp/security_policy.rb b/lib/puppet_x/lsp/security_policy.rb index e60c8ec..6ed52e4 100644 --- a/lib/puppet_x/lsp/security_policy.rb +++ b/lib/puppet_x/lsp/security_policy.rb @@ -561,6 +561,31 @@ def self.lsp_mapping reg_type: '1', policy_type: 'Registry Values', }, + 'Domain controller: Allow server operators to schedule tasks' => { + name: 'MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl', + reg_type: '4', + policy_type: 'Registry Values', + }, + 'Domain controller: Allow vulnerable Netlogon secure channel connections' => { + name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList', + reg_type: '1', + policy_type: 'Registry Values', + }, + 'Domain controller: LDAP server channel binding token requirements' => { + name: 'MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding', + reg_type: '4', + policy_type: 'Registry Values', + }, + 'Domain controller: LDAP server signing requirements' => { + name: 'MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity', + reg_type: '4', + policy_type: 'Registry Values', + }, + 'Domain controller: Refuse machine account password changes' => { + name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange', + reg_type: '4', + policy_type: 'Registry Values', + }, 'Domain member: Digitally encrypt or sign secure channel data (always)' => { name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RequireSignOrSeal', reg_type: '4', diff --git a/metadata.json b/metadata.json index 5469715..eed74e4 100644 --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { "name": "ayohrling-local_security_policy", - "version": "1.1.1", + "version": "1.2.0", "author": "Paul S. Cannon, Adam Yohrling, Corey Osman, Ryan Russell-Yates, Jordan Wesolowski, Gerben Welter, Thomas Linkin, Andy Adrian, Steven Pritchard", "summary": "Windows Local Security Policy management. Forked from cannonps/local_security_policy", "license": "Apache-2.0", diff --git a/spec/unit/puppet/provider/local_security_policy/security_policy_spec.rb b/spec/unit/puppet/provider/local_security_policy/security_policy_spec.rb index 10fdd80..68f1e41 100644 --- a/spec/unit/puppet/provider/local_security_policy/security_policy_spec.rb +++ b/spec/unit/puppet/provider/local_security_policy/security_policy_spec.rb @@ -96,6 +96,42 @@ end end + describe 'domain controller policies' do + { + 'Domain controller: Allow server operators to schedule tasks' => + ['MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl', '4'], + 'Domain controller: Allow vulnerable Netlogon secure channel connections' => + ['MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList', '1'], + 'Domain controller: LDAP server channel binding token requirements' => + ['MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding', '4'], + 'Domain controller: LDAP server signing requirements' => + ['MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity', '4'], + 'Domain controller: Refuse machine account password changes' => + ['MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange', '4'], + }.each do |policy_desc, (reg_key, reg_type)| + it "maps #{policy_desc}" do + mapping = SecurityPolicy.find_mapping_from_policy_desc(policy_desc) + expect(mapping[:name]).to eq(reg_key) + expect(mapping[:reg_type]).to eq(reg_type) + expect(mapping[:policy_type]).to eq('Registry Values') + end + + it "reverse maps #{reg_key}" do + name, = SecurityPolicy.find_mapping_from_policy_name(reg_key) + expect(name).to eq(policy_desc) + end + end + + it 'converts a REG_DWORD registry value' do + expect(subject.convert_registry_value('Domain controller: LDAP server signing requirements', 2)).to eq('4,2') + end + + it 'converts a REG_SZ registry value' do + expect(subject.convert_registry_value('Domain controller: Allow vulnerable Netlogon secure channel connections', + 'O:BAG:BAD:(A;;RC;;;BA)')).to eq('1,O:BAG:BAD:(A;;RC;;;BA)') + end + end + describe 'privilege right' do let(:resource) do Puppet::Type.type(:local_security_policy).new( diff --git a/spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb b/spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb index 42ab554..9aea68e 100644 --- a/spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb +++ b/spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb @@ -27,6 +27,26 @@ expect(resource[:policy_value]).to eq('4,0') end + it 'creates a domain controller registry value and derives its setting from the mapping' do + resource = Puppet::Type.type(:local_security_policy).new( + name: 'Domain controller: LDAP server signing requirements', + ensure: 'present', + policy_value: '2', + ) + expect(resource[:policy_value]).to eq('4,2') + expect(resource[:policy_setting]).to eq('MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity') + end + + it 'creates a domain controller string registry value' do + resource = Puppet::Type.type(:local_security_policy).new( + name: 'Domain controller: Allow vulnerable Netlogon secure channel connections', + ensure: 'present', + policy_value: 'O:BAG:BAD:(A;;RC;;;BA)', + ) + expect(resource[:policy_value]).to eq('1,O:BAG:BAD:(A;;RC;;;BA)') + expect(resource[:policy_setting]).to eq('MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList') + end + it 'creates an event audit value' do resource = Puppet::Type.type(:local_security_policy).new( name: 'Audit account logon events',