diff --git a/lib/resources/grub_conf.rb b/lib/resources/grub_conf.rb index e4268b231..d502ad29c 100644 --- a/lib/resources/grub_conf.rb +++ b/lib/resources/grub_conf.rb @@ -36,6 +36,7 @@ class GrubConfig < Inspec.resource(1) elsif os.debian? @conf_path = path || '/boot/grub/grub.cfg' @defaults_path = '/etc/default/grub' + @grubenv_path = '/boot/grub2/grubenv' @version = 'grub2' elsif os[:name] == 'amazon' @conf_path = path || '/etc/grub.conf' @@ -52,6 +53,7 @@ class GrubConfig < Inspec.resource(1) else @conf_path = path || '/boot/grub2/grub.cfg' @defaults_path = '/etc/default/grub' + @grubenv_path = '/boot/grub2/grubenv' @version = 'grub2' end end @@ -71,35 +73,73 @@ class GrubConfig < Inspec.resource(1) ###################################################################### def grub2_parse_kernel_lines(content, conf) - # Find all "menuentry" lines and then parse them into arrays - menu_entry = 0 + menu_entries = extract_menu_entries(content) + + if @kernel == 'default' + default_menu_entry(menu_entries, conf['GRUB_DEFAULT']) + else + menu_entries.find { |entry| entry['name'] == @kernel } + end + end + + def extract_menu_entries(content) + menu_entries = [] + lines = content.split("\n") - kernel_opts = {} - kernel_opts['insmod'] = [] - lines.each_with_index do |file_line, index| - next unless file_line =~ /(^|\s)menuentry\s.*/ - lines.drop(index+1).each do |kernel_line| - next if kernel_line =~ /(^|\s)(menu|}).*/ - if menu_entry == conf['GRUB_DEFAULT'].to_i && @kernel == 'default' - if kernel_line =~ /(^|\s)initrd.*/ - kernel_opts['initrd'] = kernel_line.split(' ')[1] - end - if kernel_line =~ /(^|\s)linux.*/ - kernel_opts['kernel'] = kernel_line.split - end - if kernel_line =~ /(^|\s)set root=.*/ - kernel_opts['root'] = kernel_line.split('=')[1].tr('\'', '') - end - if kernel_line =~ /(^|\s)insmod.*/ - kernel_opts['insmod'].push(kernel_line.split(' ')[1]) - end - else - menu_entry += 1 - break + lines.each_with_index do |line, index| + next unless line =~ /^menuentry\s+.*/ + entry = {} + entry['insmod'] = [] + + # Extract name from menuentry line + capture_data = line.match(/(?:^|\s+).*menuentry\s*['|"](.*)['|"]\s*--/) + if capture_data.nil? || capture_data.captures[0].nil? + raise Inspec::Exceptions::ResourceFailed "Failed to extract menuentry name from #{line}" + end + + entry['name'] = capture_data.captures[0] + + # Begin processing from index forward until a `}` line is met + lines.drop(index+1).each do |mline| + break if mline =~ /^\s*}\s*$/ + case mline + when /(?:^|\s*)initrd.*/ + entry['initrd'] = mline.split(' ')[1] + when /(?:^|\s*)linux.*/ + entry['kernel'] = mline.split + when /(?:^|\s*)set root=.*/ + entry['root'] = mline.split('=')[1].tr('\'', '') + when /(?:^|\s*)insmod.*/ + entry['insmod'] << mline.split(' ')[1] end end + + menu_entries << entry end - kernel_opts + + menu_entries + end + + def default_menu_entry(menu_entries, default) + # If the default entry isn't `saved` then a number is used as an index. + # By default this is `0`, which would be the first item in the list. + return menu_entries[default.to_i] unless default == 'saved' + + grubenv_contents = inspec.file(@grubenv_path).content + + # The location of the grubenv file is not guaranteed. In the case that + # the file does not exist this will return the 0th entry. This will also + # return the 0th entry if InSpec lacks permission to read the file. Both + # of these reflect the default Grub2 behavior. + return menu_entries[0] if grubenv_contents.nil? + + default_name = SimpleConfig.new(grubenv_contents).params['saved_entry'] + default_entry = menu_entries.select { |k| k['name'] == default_name }[0] + return default_entry unless default_entry.nil? + + # It is possible for the saved entry to not be valid . For example, grubenv + # not being up to date. If so, the 0th entry is the default. + menu_entries[0] end ################################################################### diff --git a/test/helper.rb b/test/helper.rb index 5948eece0..61835290a 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -125,6 +125,11 @@ class MockLoader '/etc/inetd.conf' => mockfile.call('inetd.conf'), '/etc/group' => mockfile.call('etcgroup'), '/etc/grub.conf' => mockfile.call('grub.conf'), + '/boot/grub2/grub.cfg' => mockfile.call('grub2.cfg'), + '/boot/grub2/grubenv' => mockfile.call('grubenv'), + '/boot/grub2/grubenv_invalid' => mockfile.call('grubenv_invalid'), + '/etc/default/grub' => mockfile.call('grub_defaults'), + '/etc/default/grub_with_saved' => mockfile.call('grub_defaults_with_saved'), '/etc/audit/auditd.conf' => mockfile.call('auditd.conf'), '/etc/mysql/my.cnf' => mockfile.call('mysql.conf'), '/etc/mysql/mysql2.conf' => mockfile.call('mysql2.conf'), diff --git a/test/unit/mock/files/grub2.cfg b/test/unit/mock/files/grub2.cfg new file mode 100644 index 000000000..5236f7f86 --- /dev/null +++ b/test/unit/mock/files/grub2.cfg @@ -0,0 +1,144 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically generated by grub2-mkconfig using templates +# from /etc/grub.d and settings from /etc/default/grub +# + +### BEGIN /etc/grub.d/00_header ### +set pager=1 + +if [ -s $prefix/grubenv ]; then + load_env +fi +if [ "${next_entry}" ] ; then + set default="${next_entry}" + set next_entry= + save_env next_entry + set boot_once=true +else + set default="${saved_entry}" +fi + +if [ x"${feature_menuentry_id}" = xy ]; then + menuentry_id_option="--id" +else + menuentry_id_option="" +fi + +export menuentry_id_option + +if [ "${prev_saved_entry}" ]; then + set saved_entry="${prev_saved_entry}" + save_env saved_entry + set prev_saved_entry= + save_env prev_saved_entry + set boot_once=true +fi + +function savedefault { + if [ -z "${boot_once}" ]; then + saved_entry="${chosen}" + save_env saved_entry + fi +} + +function load_video { + if [ x$feature_all_video_module = xy ]; then + insmod all_video + else + insmod efi_gop + insmod efi_uga + insmod ieee1275_fb + insmod vbe + insmod vga + insmod video_bochs + insmod video_cirrus + fi +} + +terminal_output console +if [ x$feature_timeout_style = xy ] ; then + set timeout_style=menu + set timeout=5 +# Fallback normal timeout code in case the timeout_style feature is +# unavailable. +else + set timeout=5 +fi +### END /etc/grub.d/00_header ### + +### BEGIN /etc/grub.d/00_tuned ### +set tuned_params="" +### END /etc/grub.d/00_tuned ### + +### BEGIN /etc/grub.d/10_linux ### +menuentry 'Test GRUB_DEFAULT of 0' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-229.el7.x86_64-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_msdos + insmod xfs + set root='hd0,msdos1' + if [ x$feature_platform_search_hint = xy ]; then + search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f + else + search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f + fi + linux16 /vmlinuz-yup-kernel-works root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet LANG=en_US.UTF-8 + initrd16 /initramfs-yup-initrd-works +} +menuentry 'CentOS Linux 7 (Core), with Linux 3.10.0-229.el7.x86_64' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-229.el7.x86_64-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_msdos + insmod xfs + set root='hd0,msdos1' + if [ x$feature_platform_search_hint = xy ]; then + search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f + else + search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f + fi + linux16 /vmlinuz-3.10.0-229.el7.x86_64 root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet LANG=en_US.UTF-8 + initrd16 /initramfs-3.10.0-229.el7.x86_64.img +} +menuentry 'CentOS Linux 7 (Core), with Linux 0-rescue' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-0-rescue-02ab91023f244febb8c38be42e37cc2d-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' { + load_video + insmod gzio + insmod part_msdos + insmod xfs + set root='hd0,msdos1' + if [ x$feature_platform_search_hint = xy ]; then + search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f + else + search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f + fi + linux16 /vmlinuz-0-rescue root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet + initrd16 /initramfs-0-rescue.img +} + +### END /etc/grub.d/10_linux ### + +### BEGIN /etc/grub.d/20_linux_xen ### +### END /etc/grub.d/20_linux_xen ### + +### BEGIN /etc/grub.d/20_ppc_terminfo ### +### END /etc/grub.d/20_ppc_terminfo ### + +### BEGIN /etc/grub.d/30_os-prober ### +### END /etc/grub.d/30_os-prober ### + +### BEGIN /etc/grub.d/40_custom ### +# This file provides an easy way to add custom menu entries. Simply type the +# menu entries you want to add after this comment. Be careful not to change +# the 'exec tail' line above. +### END /etc/grub.d/40_custom ### + +### BEGIN /etc/grub.d/41_custom ### +if [ -f ${config_directory}/custom.cfg ]; then + source ${config_directory}/custom.cfg +elif [ -z "${config_directory}" -a -f $prefix/custom.cfg ]; then + source $prefix/custom.cfg; +fi +### END /etc/grub.d/41_custom ### diff --git a/test/unit/mock/files/grub_defaults b/test/unit/mock/files/grub_defaults new file mode 100644 index 000000000..3b6388a7b --- /dev/null +++ b/test/unit/mock/files/grub_defaults @@ -0,0 +1,34 @@ +# If you change this file, run 'update-grub' afterwards to update +# /boot/grub/grub.cfg. +# For full documentation of the options in this file, see: +# info -f grub -n 'Simple configuration' + +GRUB_DEFAULT=0 +GRUB_HIDDEN_TIMEOUT=0 +GRUB_HIDDEN_TIMEOUT_QUIET=true +GRUB_TIMEOUT=10 +GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` +GRUB_CMDLINE_LINUX_DEFAULT="quiet" +GRUB_CMDLINE_LINUX="" + +# Uncomment to enable BadRAM filtering, modify to suit your needs +# This works with Linux (no patch required) and with any kernel that obtains +# the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...) +#GRUB_BADRAM="0x01234567,0xfefefefe,0x89abcdef,0xefefefef" + +# Uncomment to disable graphical terminal (grub-pc only) +#GRUB_TERMINAL=console + +# The resolution used on graphical terminal +# note that you can use only modes which your graphic card supports via VBE +# you can see them in real GRUB with the command `vbeinfo' +#GRUB_GFXMODE=640x480 + +# Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux +#GRUB_DISABLE_LINUX_UUID=true + +# Uncomment to disable generation of recovery mode menu entries +#GRUB_DISABLE_RECOVERY="true" + +# Uncomment to get a beep at grub start +#GRUB_INIT_TUNE="480 440 1" diff --git a/test/unit/mock/files/grub_defaults_with_saved b/test/unit/mock/files/grub_defaults_with_saved new file mode 100644 index 000000000..e355432f7 --- /dev/null +++ b/test/unit/mock/files/grub_defaults_with_saved @@ -0,0 +1,6 @@ +GRUB_TIMEOUT=5 +GRUB_DEFAULT=saved +GRUB_DISABLE_SUBMENU=true +GRUB_TERMINAL_OUTPUT="console" +GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet" +GRUB_DISABLE_RECOVERY="true" diff --git a/test/unit/mock/files/grubenv b/test/unit/mock/files/grubenv new file mode 100644 index 000000000..17dc4df9b --- /dev/null +++ b/test/unit/mock/files/grubenv @@ -0,0 +1,3 @@ +# GRUB Environment Block +saved_entry=CentOS Linux 7 (Core), with Linux 3.10.0-229.el7.x86_64 +############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################ diff --git a/test/unit/mock/files/grubenv_invalid b/test/unit/mock/files/grubenv_invalid new file mode 100644 index 000000000..d2c801918 --- /dev/null +++ b/test/unit/mock/files/grubenv_invalid @@ -0,0 +1,3 @@ +# GRUB Environment Block +saved_entry=Nope, not a real entry +############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################ diff --git a/test/unit/resources/grub_conf_test.rb b/test/unit/resources/grub_conf_test.rb index 311fb9bb2..af1df99e0 100644 --- a/test/unit/resources/grub_conf_test.rb +++ b/test/unit/resources/grub_conf_test.rb @@ -6,24 +6,81 @@ require 'inspec/resource' describe 'Inspec::Resources::GrubConfig' do - it 'verify kernel include' do - resource = MockLoader.new(:centos6).load_resource('grub_conf') - _(resource.kernel).must_be_kind_of Array + # Grub2 with `GRUB_DEFAULT=0` + it 'parses correctly with grub2 and `GRUB_DEfAULT=0`' do + resource = MockLoader.new(:centos7).load_resource('grub_conf') + + resource.kernel.must_include '/vmlinuz-yup-kernel-works' + resource.initrd.must_equal '/initramfs-yup-initrd-works' end - it 'verify initrd include' do - resource = MockLoader.new(:centos6).load_resource('grub_conf') - _(resource.initrd).must_be_kind_of String + # Grub2 with `GRUB_DEFAULT=saved` + it 'parses correctly with grub2 and `saved` as the `GRUB_DEFAULT`' do + resource = MockLoader.new(:centos7).load_resource('grub_conf') + + # Both Grub1 and Grub2 use `/etc/default/grub`. + # This overrides the Grub1 default for testing. + resource.instance_variable_set( + :@defaults_path, + '/etc/default/grub_with_saved' + ) + + resource.kernel.must_include '/vmlinuz-3.10.0-229.el7.x86_64' + resource.initrd.must_equal '/initramfs-3.10.0-229.el7.x86_64.img' end - it 'verify default' do - resource = MockLoader.new(:centos6).load_resource('grub_conf') - _(resource.default).must_equal '0' + it 'parses correctly with grub2 and an invalid grubenv entry' do + resource = MockLoader.new(:centos7).load_resource('grub_conf') + + # Both Grub1 and Grub2 use `/etc/default/grub`. + # This overrides the Grub1 default for testing. + resource.instance_variable_set( + :@defaults_path, + '/etc/default/grub_with_saved' + ) + + resource.instance_variable_set( + :@grubenv_path, + '/boot/grub2/grubenv_invalid' + ) + + resource.kernel.must_include '/vmlinuz-yup-kernel-works' + resource.initrd.must_equal '/initramfs-yup-initrd-works' end - it 'verify timeout' do - resource = MockLoader.new(:centos6).load_resource('grub_conf') - _(resource.timeout).must_equal '5' + # Grub2 with a specified kernel + it 'parses data correctly with grub2 and a specified kernel' do + resource = MockLoader.new(:centos7).load_resource( + 'grub_conf', + '/boot/grub2/grub.cfg', + 'CentOS Linux 7 (Core), with Linux 0-rescue' + ) + + resource.kernel.must_include '/vmlinuz-0-rescue' + resource.initrd.must_equal '/initramfs-0-rescue.img' end + # Legacy Grub + it 'parses correctly with grub1 (aka legacy-grub)' do + resource = MockLoader.new(:centos6).load_resource('grub_conf') + + resource.kernel.must_include '/vmlinuz-2.6.32-573.7.1.el6.x86_64' + resource.initrd.must_equal '/initramfs-2.6.32-573.7.1.el6.x86_64.img' + resource.default.must_equal '0' + resource.timeout.must_equal '5' + end + + # Legacy Grub with a specified kernel + it 'parses data correctly with grub1 and a specified kernel' do + resource = MockLoader.new(:centos6).load_resource( + 'grub_conf', + '/etc/grub.conf', + 'CentOS 6 (2.6.32-573.el6.x86_64)' + ) + + resource.kernel.must_include '/vmlinuz-2.6.32-573.el6.x86_64' + resource.initrd.must_equal '/initramfs-2.6.32-573.el6.x86_64.img' + resource.default.must_equal '0' + resource.timeout.must_equal '5' + end end