diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 6a853ca..646322e 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -54,11 +54,15 @@ jobs: name: Acceptance tests strategy: matrix: - os: - - ubuntu-latest - agent: - - puppet7 - - puppet8 + include: + - os: ubuntu-latest + puppet: 6 + - os: ubuntu-latest + puppet: 7 + - os: ubuntu-latest + puppet: 8 + - os: macos-latest + puppet: 8 runs-on: ${{ matrix.os }} steps: @@ -66,18 +70,26 @@ jobs: - name: Install Puppet run: | set -ex - distro=$(lsb_release -cs) - deb_name="${{ matrix.agent }}-release-${distro}.deb" - curl -sSO "https://apt.puppet.com/${deb_name}" - sudo dpkg -i "$deb_name" - rm "$deb_name" - sudo apt-get update -qq - sudo apt-get install -qy puppet-agent pdk - - name: Build module - run: pdk build - - name: Install module - run: sudo -E /opt/puppetlabs/bin/puppet module install pkg/*.tar.gz + case ${{ matrix.os }} in + macos*) + brew install --cask puppetlabs/puppet/puppet-agent-${{ matrix.puppet }} + brew install --cask puppetlabs/puppet/pdk + ;; + ubuntu*) + distro=$(lsb_release -cs) + deb_name="puppet${{ matrix.puppet }}-release-${distro}.deb" + curl -sSO "https://apt.puppet.com/${deb_name}" + sudo dpkg -i "$deb_name" + rm "$deb_name" + sudo apt-get update -qq + sudo apt-get install -qy puppet-agent pdk + ;; + *) + echo ::error::Unsupported platform + exit 1 + ;; + esac - name: Install PDK dependencies - run: sudo -E pdk bundle install + run: sudo -E /opt/puppetlabs/pdk/bin/pdk bundle install - name: Run acceptance tests - run: sudo -E pdk bundle exec rake litmus:acceptance:localhost + run: sudo -E /opt/puppetlabs/pdk/bin/pdk bundle exec rake litmus:acceptance:localhost diff --git a/.sync.yml b/.sync.yml index 1fd8a82..e7741c6 100644 --- a/.sync.yml +++ b/.sync.yml @@ -6,6 +6,9 @@ # See https://github.com/danielparks/pdk-templates/blob/main/config_defaults.yml # for the default values. --- +.github/workflows/pr-checks.yaml: + additional-platforms: + - macos-latest spec/default_facts.yml: extra_facts: identity: diff --git a/CHANGELOG.md b/CHANGELOG.md index 207d489..2832564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file. ## main branch +### Security fix + +Certain Go tarballs (see below) had files owned by non-root users: + + ❯ curl -SsL https://go.dev/dl/go1.20.14.darwin-amd64.tar.gz | tar -tzvf - | head -3 + drwxr-xr-x 0 0 0 0 Feb 2 10:19 go/ + -rw-r--r-- 0 gopher wheel 1339 Feb 2 10:09 go/CONTRIBUTING.md + -rw-r--r-- 0 gopher wheel 1479 Feb 2 10:09 go/LICENSE + +In this case, the non-root user in question mapped to the first user created on +the macOS system (UID 501). + +When running as root, previous versions of dp-golang would preserve file +ownership when extracting the tarball, even if `owner` was set to something +else. **This meant that files, such as the `go` binary, ended up being writable +by a non-root user.** + +This version of dp-golang enables [`tar`]’s `--no-same-owner` and +`--no-same-permissions` flags, which cause files to be extracted as the user +running Puppet, or as the user/group specified in the Puppet code. + +GitHub security advisory: [GHSA-8h8m-h98f-vv84] + +#### Affected Go tarballs + + * Go for macOS version 1.4.3 through 1.21rc3, inclusive. + * go1.4-bootstrap-20170518.tar.gz + * go1.4-bootstrap-20170531.tar.gz + +[`tar`]: https://www.man7.org/linux/man-pages/man1/tar.1.html +[GHSA-8h8m-h98f-vv84]: https://github.com/danielparks/puppet-golang/security/advisories/GHSA-8h8m-h98f-vv84 + +### Changes + +As part of the security fix mentioned above, it became necessary to be more +agressive about ensuring that the owner and group of files in the installation +are correct. dp-golang now deletes and recreates any Go installation it finds +that has a file or directory with the wrong owner or group. + ## Release 1.2.6 * Synced with [PDK][]. diff --git a/manifests/from_tarball.pp b/manifests/from_tarball.pp index fdc1650..0f2c307 100644 --- a/manifests/from_tarball.pp +++ b/manifests/from_tarball.pp @@ -46,6 +46,9 @@ String[1] $mode = '0755', Stdlib::Unixpath $state_file = golang::state_file($go_dir), ) { + $encoded_go_dir = $go_dir.regsubst('/', '_', 'G') + $archive_path = "/tmp/puppet-golang${encoded_go_dir}.tar.gz" + if $ensure != any_version { # Used to ensure that the installation is updated when $source changes. $file_ensure = $ensure ? { @@ -70,6 +73,38 @@ } } + if $ensure == present or $ensure == any_version { + # Remove Go installation if any of its files have the wrong user or group. + # This will cause it to be replaced with a fresh installation. + exec { "dp/golang check ownership of ${go_dir}": + command => ['rm', '-rf', $go_dir], + environment => [ + "GO_DIR=${go_dir}", + "OWNER=${owner}", + "GROUP=${group}", + ], + path => ['/usr/local/bin', '/usr/bin', '/bin'], + onlyif => 'find "$GO_DIR" "(" "(" -not -user "$OWNER" ")" -or "(" -not -group "$GROUP" ")" ")" -print -quit | grep .', + before => File[$go_dir], + notify => Archive[$archive_path], + } + } + + # File[$state_file] changing should only trigger an update when ensure is + # present, and not any_version. + if $ensure == present { + # If the $go_dir/bin directory exists, archive won't update it. Also, we + # want to remove any files that are not present in the new version. + exec { "dp/golang refresh go installation at ${go_dir}": + command => ['rm', '-rf', $go_dir], + path => ['/usr/local/bin', '/usr/bin', '/bin'], + refreshonly => true, + subscribe => File[$state_file], + before => File[$go_dir], + notify => Archive[$archive_path], + } + } + $directory_ensure = $ensure ? { 'present' => directory, 'any_version' => directory, @@ -85,31 +120,13 @@ } if $ensure == present or $ensure == any_version { - $encoded_go_dir = $go_dir.regsubst('/', '_', 'G') - $archive_path = "/tmp/puppet-golang${encoded_go_dir}.tar.gz" - - # Only trigger an update when ensure is present, and not any_version. - if $ensure == present { - # If the $go_dir/bin directory exists, archive won't update it. Also, we - # want to remove any files that are not present in the new version. - exec { "dp/golang refresh go installation at ${go_dir}": - command => ['rm', '-rf', $go_dir], - path => ['/usr/local/bin', '/usr/bin', '/bin'], - user => $facts['identity']['user'], - refreshonly => true, - subscribe => File[$state_file], - before => File[$go_dir], - notify => Archive[$archive_path], - } - } - include archive archive { $archive_path: ensure => present, extract => true, extract_path => $go_dir, - extract_flags => '--strip-components 1 -xf', + extract_flags => '--strip-components 1 --no-same-owner --no-same-permissions -xf', user => $owner, group => $group, source => $source, diff --git a/metadata.json b/metadata.json index 9782448..513f75f 100644 --- a/metadata.json +++ b/metadata.json @@ -85,5 +85,5 @@ ], "pdk-version": "3.0.1", "template-url": "https://github.com/danielparks/pdk-templates#main", - "template-ref": "heads/main-0-g2f62871" + "template-ref": "heads/main-0-gde8efe4" } diff --git a/spec/acceptance/golang_from_tarball_spec.rb b/spec/acceptance/golang_from_tarball_spec.rb index a2274dc..946a317 100644 --- a/spec/acceptance/golang_from_tarball_spec.rb +++ b/spec/acceptance/golang_from_tarball_spec.rb @@ -2,17 +2,41 @@ require 'spec_helper_acceptance' +# This tarball contains entries owned by gopher (501) rather than root. +source_url = 'https://go.dev/dl/go1.10.4.darwin-amd64.tar.gz' + +newer_source_url = 'https://go.dev/dl/go1.19.1.darwin-amd64.tar.gz' + describe 'defined type golang::from_tarball' do context 'repeated root installs:' do context 'default ensure with 1.10.4 source' do it 'installs Go' do - idempotent_apply(<<~'PUPPET') + idempotent_apply(<<~"PUPPET") golang::from_tarball { '/opt/go': - source => 'https://go.dev/dl/go1.10.4.linux-amd64.tar.gz', + source => '#{source_url}', } PUPPET end + describe file('/opt/go') do + it { is_expected.to be_directory } + its(:mode) { is_expected.to eq '755' } + its(:owner) { is_expected.to eq 'root' } + end + + describe file('/opt/.go.source_url') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\n#{source_url}\n" } + end + + describe file('/opt/go/bin/go') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '755' } + its(:owner) { is_expected.to eq 'root' } + end + describe file('/opt/go/VERSION') do its(:content) { is_expected.to eq 'go1.10.4' } end @@ -20,44 +44,65 @@ context 'ensure => present' do it 'causes no changes' do - apply_manifest(<<~'PUPPET', catch_changes: true) + apply_manifest(<<~"PUPPET", catch_changes: true) golang::from_tarball { '/opt/go': ensure => present, - source => 'https://go.dev/dl/go1.10.4.linux-amd64.tar.gz', + source => '#{source_url}', } PUPPET end + describe file('/opt/.go.source_url') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\n#{source_url}\n" } + end + describe file('/opt/go/VERSION') do its(:content) { is_expected.to eq 'go1.10.4' } end end - context 'ensure => any_version with 1.19.1 source' do + context 'ensure => any_version with newer source' do it 'causes no changes' do - apply_manifest(<<~'PUPPET', catch_changes: true) + apply_manifest(<<~"PUPPET", catch_changes: true) golang::from_tarball { '/opt/go': ensure => any_version, - source => 'https://go.dev/dl/go1.19.1.linux-amd64.tar.gz', + source => '#{newer_source_url}', } PUPPET end + describe file('/opt/.go.source_url') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\n#{source_url}\n" } + end + describe file('/opt/go/VERSION') do its(:content) { is_expected.to eq 'go1.10.4' } end end - context 'ensure => present with 1.19.1 source' do + context 'ensure => present with newer source' do it 'causes changes' do - apply_manifest(<<~'PUPPET', expect_changes: true) + apply_manifest(<<~"PUPPET", expect_changes: true) golang::from_tarball { '/opt/go': ensure => present, - source => 'https://go.dev/dl/go1.19.1.linux-amd64.tar.gz', + source => '#{newer_source_url}', } PUPPET end + describe file('/opt/.go.source_url') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\n#{newer_source_url}\n" } + end + describe file('/opt/go/VERSION') do its(:content) { is_expected.to eq 'go1.19.1' } end @@ -65,17 +110,142 @@ context 'ensure => absent' do it do - idempotent_apply(<<~'PUPPET') + idempotent_apply(<<~"PUPPET") golang::from_tarball { '/opt/go': ensure => absent, - source => 'https://go.dev/dl/go1.19.1.linux-amd64.tar.gz', + source => '#{newer_source_url}', } PUPPET end + describe file('/opt/go') do + it { is_expected.not_to exist } + end + + describe file('/opt/.go.source_url') do + it { is_expected.not_to exist } + end + describe file('/opt/go/VERSION') do it { is_expected.not_to exist } end end end + + context 'as a non-root user' do + context 'default ensure with 1.10.4 source' do + it 'installs Go' do + idempotent_apply(<<~"PUPPET") + group { 'user': } + user { 'user': + home => '#{home}/user', + gid => 'user', + managehome => false, + } + + file { '#{home}/user': + ensure => directory, + owner => 'user', + group => 'user', + mode => '0755', + } + + golang::from_tarball { '#{home}/user/go-install': + source => '#{source_url}', + owner => 'user', + group => 'user', + mode => '0700', + } + PUPPET + end + + describe file("#{home}/user/go-install") do + it { is_expected.to be_directory } + its(:mode) { is_expected.to eq '700' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + end + + describe file("#{home}/user/.go-install.source_url") do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'user' } + its(:content) { is_expected.to include "\n#{source_url}\n" } + end + + describe file("#{home}/user/go-install/bin/go") do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '755' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + end + + describe file("#{home}/user/go-install/VERSION") do + its(:content) { is_expected.to eq 'go1.10.4' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + end + + context 'enforcing file ownership' do + it 'chowns bin/go' do + File.chown(0, 0, "#{home}/user/go-install/bin/go") + end + + it 'reinstalls Go' do + apply_manifest(<<~"PUPPET", expect_changes: true) + golang::from_tarball { '#{home}/user/go-install': + source => '#{source_url}', + owner => 'user', + group => 'user', + mode => '0700', + } + PUPPET + end + + describe file("#{home}/user/go-install") do + it { is_expected.to be_directory } + its(:mode) { is_expected.to eq '700' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + end + + describe file("#{home}/user/go-install/bin/go") do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '755' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + end + + it 'does nothing' do + apply_manifest(<<~"PUPPET", catch_changes: true) + golang::from_tarball { '#{home}/user/go-install': + source => '#{source_url}', + owner => 'user', + group => 'user', + mode => '0700', + } + PUPPET + end + end + end + + context 'cleans up' do + it 'uninstalls Go' do + idempotent_apply(<<~"PUPPET") + golang::from_tarball { '#{home}/user/go-install': + ensure => absent, + source => '#{source_url}', + } + PUPPET + end + + describe file("#{home}/user/go-install") do + it { is_expected.not_to exist } + end + + describe file("#{home}/user/.go-install.source_url") do + it { is_expected.not_to exist } + end + end + end end diff --git a/spec/acceptance/golang_installation_spec.rb b/spec/acceptance/golang_installation_spec.rb index 29bee79..5c5dc06 100644 --- a/spec/acceptance/golang_installation_spec.rb +++ b/spec/acceptance/golang_installation_spec.rb @@ -108,13 +108,20 @@ ['1.10.4', '1.19.1'].each do |version| describe file("/opt/go#{version}") do it { is_expected.to be_directory } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } + end + + describe file("/opt/.go#{version}.source_url") do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\nhttps://go.dev/dl/" } end describe file("/opt/go#{version}/bin/go") do it { is_expected.to be_file } it { is_expected.to be_executable } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } end describe command("/opt/go#{version}/bin/go version") do @@ -148,7 +155,7 @@ describe file('/opt/go1.10.4/bin/go') do it { is_expected.to be_file } it { is_expected.to be_executable } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } end describe file('/opt/go1.19.1') do @@ -198,33 +205,33 @@ context 'multiple user installs with linked_binaries' do it do - idempotent_apply(<<~'PUPPET') - golang::installation { '/home/user/go1.10.4': + idempotent_apply(<<~"PUPPET") + golang::installation { '#{home}/user/go1.10.4': ensure => '1.10.4', owner => 'user', group => 'user', mode => '0700', } - golang::installation { '/home/user/go1.19.1': + golang::installation { '#{home}/user/go1.19.1': ensure => '1.19.1', owner => 'user', group => 'user', mode => '0700', } - golang::linked_binaries { '/home/user/go1.19.1': - into_bin => '/home/user/bin', + golang::linked_binaries { '#{home}/user/go1.19.1': + into_bin => '#{home}/user/bin', } group { 'user': } user { 'user': - home => '/home/user', + home => '#{home}/user', gid => 'user', - managehome => true, + managehome => false, } - file { '/home/user/bin': + file { ['#{home}/user', '#{home}/user/bin']: ensure => directory, owner => 'user', group => 'user', @@ -234,74 +241,82 @@ end ['1.10.4', '1.19.1'].each do |version| - describe file("/home/user/go#{version}") do + describe file("#{home}/user/go#{version}") do it { is_expected.to be_directory } - it { is_expected.to be_owned_by 'user' } - it { is_expected.to be_grouped_into 'user' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } it { is_expected.to be_mode 700 } # WTF converted to octal end - describe file("/home/user/go#{version}/bin/go") do + describe file("#{home}/user/.go#{version}.source_url") do it { is_expected.to be_file } - it { is_expected.to be_owned_by 'user' } - it { is_expected.to be_grouped_into 'user' } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } + its(:content) { is_expected.to include "\nhttps://go.dev/dl/" } + end + + describe file("#{home}/user/go#{version}/bin/go") do + it { is_expected.to be_file } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } it { is_expected.to be_mode 755 } # WTF converted to octal end - describe command("/home/user/go#{version}/bin/go version") do + describe command("#{home}/user/go#{version}/bin/go version") do its(:stdout) { is_expected.to start_with("go version go#{version} ") } its(:stderr) { is_expected.to eq '' } its(:exit_status) { is_expected.to eq 0 } end end - describe file('/home/user/bin/go') do + describe file("#{home}/user/bin/go") do it { is_expected.to be_symlink } - it { is_expected.to be_linked_to '/home/user/go1.19.1/bin/go' } + it { is_expected.to be_linked_to "#{home}/user/go1.19.1/bin/go" } end end context 'multiple user installs with mixed ensure and linked_binaries' do it do - idempotent_apply(<<~'PUPPET') - golang::installation { '/home/user/go1.10.4': + idempotent_apply(<<~"PUPPET") + golang::installation { '#{home}/user/go1.10.4': ensure => '1.10.4', owner => 'user', group => 'user', } - golang::installation { '/home/user/go1.19.1': + golang::installation { '#{home}/user/go1.19.1': ensure => absent, } - golang::linked_binaries { '/home/user/go1.10.4': - into_bin => '/home/user/bin', + golang::linked_binaries { '#{home}/user/go1.10.4': + into_bin => '#{home}/user/bin', } PUPPET end - describe file('/home/user/go1.10.4') do + describe file("#{home}/user/go1.10.4") do it { is_expected.to be_directory } - it { is_expected.to be_owned_by 'user' } - it { is_expected.to be_grouped_into 'user' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } it { is_expected.to be_mode 755 } # WTF converted to octal end - describe file('/home/user/go1.10.4/bin/go') do + describe file("#{home}/user/go1.10.4/bin/go") do it { is_expected.to be_file } - it { is_expected.to be_owned_by 'user' } - it { is_expected.to be_grouped_into 'user' } + its(:owner) { is_expected.to eq 'user' } + its(:group) { is_expected.to eq 'user' } it { is_expected.to be_mode 755 } # WTF converted to octal end - describe file('/home/user/go1.19.1') do + describe file("#{home}/user/go1.19.1") do it { is_expected.not_to exist } end - describe file('/home/user/bin/go') do + describe file("#{home}/user/bin/go") do it { is_expected.to be_symlink } - it { is_expected.to be_linked_to '/home/user/go1.10.4/bin/go' } + it { is_expected.to be_linked_to "#{home}/user/go1.10.4/bin/go" } end - describe command('/home/user/bin/go version') do + describe command("#{home}/user/bin/go version") do its(:stdout) { is_expected.to start_with('go version go1.10.4 ') } its(:stderr) { is_expected.to eq '' } its(:exit_status) { is_expected.to eq 0 } @@ -310,33 +325,33 @@ context 'multiple user uninstalls with linked_binaries' do it do - idempotent_apply(<<~'PUPPET') - golang::installation { '/home/user/go1.10.4': + idempotent_apply(<<~"PUPPET") + golang::installation { '#{home}/user/go1.10.4': ensure => absent, owner => 'user', group => 'user', } - golang::installation { '/home/user/go1.19.1': + golang::installation { '#{home}/user/go1.19.1': ensure => absent, owner => 'user', group => 'user', } - golang::linked_binaries { '/home/user/go1.10.4': + golang::linked_binaries { '#{home}/user/go1.10.4': ensure => absent, - into_bin => '/home/user/bin', + into_bin => '#{home}/user/bin', } PUPPET end - describe file('/home/user/go1.10.4') do + describe file("#{home}/user/go1.10.4") do it { is_expected.not_to exist } end - describe file('/home/user/go1.19.1') do + describe file("#{home}/user/go1.19.1") do it { is_expected.not_to exist } end - describe file('/home/user/bin/go') do + describe file("#{home}/user/bin/go") do it { is_expected.not_to exist } end end diff --git a/spec/acceptance/golang_spec.rb b/spec/acceptance/golang_spec.rb index 3fe8f5c..66d112a 100644 --- a/spec/acceptance/golang_spec.rb +++ b/spec/acceptance/golang_spec.rb @@ -8,13 +8,13 @@ describe file('/usr/local/go') do it { is_expected.to be_directory } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } end describe file('/usr/local/go/bin/go') do it { is_expected.to be_file } it { is_expected.to be_executable } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } end describe file('/usr/local/bin/go') do @@ -47,13 +47,20 @@ class { 'golang': describe file('/usr/local/go') do it { is_expected.to be_directory } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } + end + + describe file('/usr/local/.go.source_url') do + it { is_expected.to be_file } + its(:mode) { is_expected.to eq '444' } + its(:owner) { is_expected.to eq 'root' } + its(:content) { is_expected.to include "\nhttps://go.dev/dl/" } end describe file('/usr/local/go/bin/go') do it { is_expected.to be_file } it { is_expected.to be_executable } - it { is_expected.to be_owned_by 'root' } + its(:owner) { is_expected.to eq 'root' } end describe file('/usr/local/bin/go') do @@ -86,6 +93,10 @@ class { 'golang': it { is_expected.not_to exist } end + describe file('/usr/local/.go.source_url') do + it { is_expected.not_to exist } + end + describe file('/usr/local/bin/go') do it { is_expected.not_to exist } end diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb index a7ef175..3ef0de1 100644 --- a/spec/spec_helper_acceptance.rb +++ b/spec/spec_helper_acceptance.rb @@ -8,9 +8,19 @@ # For some reason litmusimage/ubuntu:22.04 doesn’t come with sudo. RSpec.configure do |config| - config.before(:suite) do - litmus = Class.new.extend(PuppetLitmus) - litmus.apply_manifest("package { 'sudo': }", catch_failures: true) + if RUBY_PLATFORM.include?('linux') + config.before(:suite) do + litmus = Class.new.extend(PuppetLitmus) + litmus.apply_manifest("package { 'sudo': }", catch_failures: true) + end + end +end + +def home + if RUBY_PLATFORM.include?('darwin') + '/Users' + else + '/home' end end