From: Alexander Popov Date: Mon, 10 Jun 2024 14:10:47 +0000 (+0300) Subject: Add the comment about 'if arch' for the 'cut_attack_surface' checks X-Git-Url: https://jxself.org/git/?a=commitdiff_plain;h=HEAD;hp=9b14224a320d8e91fa127e0da5abbfa0c852ac5f;p=kconfig-hardened-check.git Add the comment about 'if arch' for the 'cut_attack_surface' checks Refers to #135. --- diff --git a/.github/workflows/engine_unit-test.yml b/.github/workflows/engine_unit-test.yml index 8357264..86120b0 100644 --- a/.github/workflows/engine_unit-test.yml +++ b/.github/workflows/engine_unit-test.yml @@ -3,12 +3,12 @@ name: engine unit-test on: push: branches: [ master ] - pull_request: - branches: [ master ] jobs: engine_unit-test: + if: github.repository == 'a13xp0p0v/kernel-hardening-checker' + runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/engine_unit-test_no-coverage.yml b/.github/workflows/engine_unit-test_no-coverage.yml new file mode 100644 index 0000000..7f92b48 --- /dev/null +++ b/.github/workflows/engine_unit-test_no-coverage.yml @@ -0,0 +1,32 @@ +name: engine unit-test no coverage + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + engine_unit-test_no-coverage: + + runs-on: ubuntu-latest + + strategy: + max-parallel: 1 + fail-fast: false + matrix: + python-version: ['3.12'] + + steps: + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get the source code + uses: actions/checkout@v4 + + - name: Run unit-tests + run: | + python -m unittest -v -b diff --git a/.github/workflows/functional_test.sh b/.github/workflows/functional_test.sh index 106320c..d894c94 100644 --- a/.github/workflows/functional_test.sh +++ b/.github/workflows/functional_test.sh @@ -70,13 +70,17 @@ coverage run -a --branch bin/kernel-hardening-checker -s $SYSCTL_EXAMPLE -m json coverage run -a --branch bin/kernel-hardening-checker -s $SYSCTL_EXAMPLE -m show_ok coverage run -a --branch bin/kernel-hardening-checker -s $SYSCTL_EXAMPLE -m show_fail +echo ">>>>> test -v (kernel version detection) <<<<<" +cp kernel_hardening_checker/config_files/distros/fedora_34.config ./test.config +coverage run -a --branch bin/kernel-hardening-checker -c ./test.config -v /proc/version + echo "Collect coverage for error handling" echo ">>>>> -c and -p together <<<<<" -coverage run -a --branch bin/kernel-hardening-checker -p X86_64 -c kernel_hardening_checker/config_files/distros/fedora_34.config && exit 1 +coverage run -a --branch bin/kernel-hardening-checker -p X86_64 -c ./test.config && exit 1 echo ">>>>> -c and -g together <<<<<" -coverage run -a --branch bin/kernel-hardening-checker -g X86_64 -c kernel_hardening_checker/config_files/distros/fedora_34.config && exit 1 +coverage run -a --branch bin/kernel-hardening-checker -g X86_64 -c ./test.config && exit 1 echo ">>>>> -l without -c <<<<<" coverage run -a --branch bin/kernel-hardening-checker -l /proc/cmdline && exit 1 @@ -97,16 +101,31 @@ coverage run -a --branch bin/kernel-hardening-checker -p X86_64 -m show_fail && echo ">>>>> wrong mode for -g <<<<<" coverage run -a --branch bin/kernel-hardening-checker -g X86_64 -m show_ok && exit 1 -cp kernel_hardening_checker/config_files/distros/fedora_34.config ./test.config +echo ">>>>> no kconfig file <<<<<" +coverage run -a --branch bin/kernel-hardening-checker -c ./nosuchfile && exit 1 + +echo ">>>>> no cmdline file <<<<<" +coverage run -a --branch bin/kernel-hardening-checker -c ./test.config -l ./nosuchfile && exit 1 + +echo ">>>>> empty cmdline file <<<<<" +touch ./empty_file +coverage run -a --branch bin/kernel-hardening-checker -c ./test.config -l ./empty_file && exit 1 + +echo ">>>>> no sysctl file <<<<<" +coverage run -a --branch bin/kernel-hardening-checker -s ./nosuchfile && exit 1 echo ">>>>> no kernel version <<<<<" sed '3d' test.config > error.config coverage run -a --branch bin/kernel-hardening-checker -c error.config && exit 1 -echo ">>>>> strange kernel version string <<<<<" +echo ">>>>> strange kernel version in kconfig <<<<<" sed '3 s/5./version 5./' test.config > error.config coverage run -a --branch bin/kernel-hardening-checker -c error.config && exit 1 +echo ">>>>> strange kernel version via -v <<<<<" +sed '3d' test.config > error.config +coverage run -a --branch bin/kernel-hardening-checker -c error.config -v /proc/cmdline && exit 1 + echo ">>>>> no arch <<<<<" sed '305d' test.config > error.config coverage run -a --branch bin/kernel-hardening-checker -c error.config && exit 1 @@ -147,7 +166,6 @@ echo 'some strange line' >> error_sysctls coverage run -a --branch bin/kernel-hardening-checker -c test.config -s error_sysctls && exit 1 echo ">>>>> invalid sysctl file <<<<<" -touch empty_file coverage run -a --branch bin/kernel-hardening-checker -c test.config -s empty_file && exit 1 echo "The end of the functional tests" diff --git a/.github/workflows/functional_test.yml b/.github/workflows/functional_test.yml index b9590ee..e197bac 100644 --- a/.github/workflows/functional_test.yml +++ b/.github/workflows/functional_test.yml @@ -3,12 +3,12 @@ name: functional test on: push: branches: [ master ] - pull_request: - branches: [ master ] jobs: functional_test: + if: github.repository == 'a13xp0p0v/kernel-hardening-checker' + runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/functional_test_no-coverage.yml b/.github/workflows/functional_test_no-coverage.yml new file mode 100644 index 0000000..3d02c5a --- /dev/null +++ b/.github/workflows/functional_test_no-coverage.yml @@ -0,0 +1,57 @@ +name: functional test no coverage + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + functional_test_no-coverage: + + runs-on: ubuntu-latest + + strategy: + max-parallel: 1 + fail-fast: false + matrix: + # Current ubuntu-latest (Ubuntu 22.04) provides the following versions of Python: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + + steps: + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + run: | + python -m pip install --upgrade pip + echo "Install the package via pip..." + pip --verbose install git+https://github.com/a13xp0p0v/kernel-hardening-checker + echo "Run the installed tool..." + kernel-hardening-checker + + - name: Check all configs with the installed tool + run: | + echo "Check all configs with the installed tool..." + sysctl -a > /tmp/sysctls + CONFIG_DIR=`find /opt/hostedtoolcache/Python/ -name config_files` + KCONFIGS=`find $CONFIG_DIR -type f | grep -e "\.config" -e "\.gz"` + COUNT=0 + for C in $KCONFIGS + do + COUNT=$(expr $COUNT + 1) + echo -e "\n>>>>> checking kconfig number $COUNT <<<<<" + kernel-hardening-checker -c $C -l /proc/cmdline -s /tmp/sysctls + done + echo -e "\nHave checked $COUNT kconfigs" + + - name: Get source code + uses: actions/checkout@v4 + + - name: Run the functional tests + run: | + pip install coverage + sh .github/workflows/functional_test.sh diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml new file mode 100644 index 0000000..e23d7e1 --- /dev/null +++ b/.github/workflows/static_analysis.yml @@ -0,0 +1,39 @@ +name: static analysis + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + static_analysis: + + runs-on: ubuntu-latest + + strategy: + max-parallel: 1 + fail-fast: false + matrix: + python-version: ['3.12'] + + steps: + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get the source code + uses: actions/checkout@v4 + + - name: Check static typing with mypy + run: | + pip install mypy + mypy kernel_hardening_checker/ --show-error-context --pretty --no-incremental --check-untyped-defs --disallow-untyped-defs --strict-equality + + - name: Check code with pylint + run: | + pip install pylint + pip install setuptools + pylint --recursive=y kernel_hardening_checker setup.py diff --git a/.woodpecker/functional_test.yml b/.woodpecker/functional_test.yml index 17272f5..6eab6a5 100644 --- a/.woodpecker/functional_test.yml +++ b/.woodpecker/functional_test.yml @@ -21,6 +21,23 @@ steps: - COUNT=0 - for C in $KCONFIGS; do COUNT=$(expr $COUNT + 1); echo ">>>>> checking kconfig number $COUNT <<<<<"; kernel-hardening-checker -c $C -l /proc/cmdline -s /tmp/sysctls; done - echo "Have checked $COUNT kconfigs" + static-typing-checking: + image: python:3 + pull: true + commands: + - echo "Install the mypy tool..." + - python --version + - pip install --no-cache-dir mypy + - mypy kernel_hardening_checker/ --show-error-context --pretty --no-incremental --check-untyped-defs --disallow-untyped-defs --strict-equality + pylint-checking: + image: python:3 + pull: true + commands: + - echo "Install the pylint tool..." + - python --version + - pip install --no-cache-dir pylint + - pip install --no-cache-dir setuptools + - pylint --recursive=y kernel_hardening_checker setup.py functional-test-with-coverage: image: python:3 pull: true diff --git a/kernel_hardening_checker/__about__.py b/kernel_hardening_checker/__about__.py deleted file mode 100644 index 09abf88..0000000 --- a/kernel_hardening_checker/__about__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Version -""" - -__version__ = '0.6.6' diff --git a/kernel_hardening_checker/__init__.py b/kernel_hardening_checker/__init__.py index 89c24f7..043dae8 100644 --- a/kernel_hardening_checker/__init__.py +++ b/kernel_hardening_checker/__init__.py @@ -8,29 +8,35 @@ Author: Alexander Popov This module performs input/output. """ -# pylint: disable=missing-function-docstring,line-too-long,invalid-name,too-many-branches,too-many-statements +# pylint: disable=missing-function-docstring,line-too-long,too-many-branches,too-many-statements +import os import gzip import sys from argparse import ArgumentParser -from collections import OrderedDict +from typing import List, Tuple, Dict, TextIO import re import json -from .__about__ import __version__ from .checks import add_kconfig_checks, add_cmdline_checks, normalize_cmdline_options, add_sysctl_checks -from .engine import populate_with_data, perform_checks, override_expected_value +from .engine import StrOrNone, TupleOrNone, ChecklistObjType +from .engine import print_unknown_options, populate_with_data, perform_checks, override_expected_value -def _open(file: str, *args, **kwargs): - open_method = open - if file.endswith('.gz'): - open_method = gzip.open +# kernel-hardening-checker version +__version__ = '0.6.6' - return open_method(file, *args, **kwargs) +def _open(file: str) -> TextIO: + try: + if file.endswith('.gz'): + return gzip.open(file, 'rt', encoding='utf-8') + return open(file, 'rt', encoding='utf-8') + except FileNotFoundError: + sys.exit(f'[!] ERROR: unable to open {file}, are you sure it exists?') -def detect_arch(fname, archs): - with _open(fname, 'rt', encoding='utf-8') as f: + +def detect_arch(fname: str, archs: List[str]) -> Tuple[StrOrNone, str]: + with _open(fname) as f: arch_pattern = re.compile(r"CONFIG_[a-zA-Z0-9_]+=y$") arch = None for line in f.readlines(): @@ -46,8 +52,8 @@ def detect_arch(fname, archs): return arch, 'OK' -def detect_kernel_version(fname): - with _open(fname, 'rt', encoding='utf-8') as f: +def detect_kernel_version(fname: str) -> Tuple[TupleOrNone, str]: + with _open(fname) as f: ver_pattern = re.compile(r"^# Linux/.+ Kernel Configuration$|^Linux version .+") for line in f.readlines(): if ver_pattern.match(line): @@ -56,17 +62,17 @@ def detect_kernel_version(fname): ver_str = parts[2].split('-', 1)[0] ver_numbers = ver_str.split('.') if len(ver_numbers) >= 3: - if all(map(lambda x: x.isdigit(), ver_numbers)): - return tuple(map(int, ver_numbers)), None + if all(map(lambda x: x.isdecimal(), ver_numbers)): + return tuple(map(int, ver_numbers)), 'OK' msg = f'failed to parse the version "{parts[2]}"' return None, msg return None, 'no kernel version detected' -def detect_compiler(fname): +def detect_compiler(fname: str) -> Tuple[StrOrNone, str]: gcc_version = None clang_version = None - with _open(fname, 'rt', encoding='utf-8') as f: + with _open(fname) as f: for line in f.readlines(): if line.startswith('CONFIG_GCC_VERSION='): gcc_version = line[19:-1] @@ -81,30 +87,7 @@ def detect_compiler(fname): sys.exit(f'[!] ERROR: invalid GCC_VERSION and CLANG_VERSION: {gcc_version} {clang_version}') -def print_unknown_options(checklist, parsed_options, opt_type): - known_options = [] - - for o1 in checklist: - if o1.opt_type != 'complex': - known_options.append(o1.name) - continue - for o2 in o1.opts: - if o2.opt_type != 'complex': - if hasattr(o2, 'name'): - known_options.append(o2.name) - continue - for o3 in o2.opts: - assert(o3.opt_type != 'complex'), \ - f'unexpected ComplexOptCheck inside {o2.name}' - if hasattr(o3, 'name'): - known_options.append(o3.name) - - for option, value in parsed_options.items(): - if option not in known_options: - print(f'[?] No check for {opt_type} option {option} ({value})') - - -def print_checklist(mode, checklist, with_results): +def print_checklist(mode: StrOrNone, checklist: List[ChecklistObjType], with_results: bool) -> None: if mode == 'json': output = [] for opt in checklist: @@ -124,13 +107,20 @@ def print_checklist(mode, checklist, with_results): print('=' * sep_line_len) # table contents + ok_count = 0 + fail_count = 0 for opt in checklist: if with_results: - if mode == 'show_ok': - if not opt.result.startswith('OK'): + assert(opt.result), f'unexpected empty result of {opt.name} check' + if opt.result.startswith('OK'): + ok_count += 1 + if mode == 'show_fail': continue - if mode == 'show_fail': - if not opt.result.startswith('FAIL'): + else: + assert(opt.result.startswith('FAIL')), \ + f'unexpected result "{opt.result}" of {opt.name} check' + fail_count += 1 + if mode == 'show_ok': continue opt.table_print(mode, with_results) print() @@ -140,9 +130,7 @@ def print_checklist(mode, checklist, with_results): # final score if with_results: - fail_count = len(list(filter(lambda opt: opt.result.startswith('FAIL'), checklist))) fail_suppressed = '' - ok_count = len(list(filter(lambda opt: opt.result.startswith('OK'), checklist))) ok_suppressed = '' if mode == 'show_ok': fail_suppressed = ' (suppressed in output)' @@ -151,8 +139,8 @@ def print_checklist(mode, checklist, with_results): print(f'[+] Config check is finished: \'OK\' - {ok_count}{ok_suppressed} / \'FAIL\' - {fail_count}{fail_suppressed}') -def parse_kconfig_file(_mode, parsed_options, fname): - with _open(fname, 'rt', encoding='utf-8') as f: +def parse_kconfig_file(_mode: StrOrNone, parsed_options: Dict[str, str], fname: str) -> None: + with _open(fname) as f: opt_is_on = re.compile(r"CONFIG_[a-zA-Z0-9_]+=.+$") opt_is_off = re.compile(r"# CONFIG_[a-zA-Z0-9_]+ is not set$") @@ -176,12 +164,19 @@ def parse_kconfig_file(_mode, parsed_options, fname): sys.exit(f'[!] ERROR: Kconfig option "{line}" is found multiple times') if option: + assert(value), f'unexpected empty value for {option}' parsed_options[option] = value -def parse_cmdline_file(mode, parsed_options, fname): +def parse_cmdline_file(mode: StrOrNone, parsed_options: Dict[str, str], fname: str) -> None: + if not os.path.isfile(fname): + sys.exit(f'[!] ERROR: unable to open {fname}, are you sure it exists?') + with open(fname, 'r', encoding='utf-8') as f: line = f.readline() + if not line: + sys.exit(f'[!] ERROR: empty "{fname}"') + opts = line.split() line = f.readline() @@ -197,10 +192,14 @@ def parse_cmdline_file(mode, parsed_options, fname): if name in parsed_options and mode != 'json': print(f'[!] WARNING: cmdline option "{name}" is found multiple times') value = normalize_cmdline_options(name, value) + assert(value is not None), f'unexpected None value for {name}' parsed_options[name] = value -def parse_sysctl_file(mode, parsed_options, fname): +def parse_sysctl_file(mode: StrOrNone, parsed_options: Dict[str, str], fname: str) -> None: + if not os.path.isfile(fname): + sys.exit(f'[!] ERROR: unable to open {fname}, are you sure it exists?') + with open(fname, 'r', encoding='utf-8') as f: sysctl_pattern = re.compile(r"[a-zA-Z0-9/\._-]+ =.*$") for line in f.readlines(): @@ -219,11 +218,11 @@ def parse_sysctl_file(mode, parsed_options, fname): sys.exit(f'[!] ERROR: {fname} doesn\'t look like a sysctl output file, please try `sudo sysctl -a > {fname}`') # let's check the presence of a sysctl option available for root - if 'net.core.bpf_jit_harden' not in parsed_options and mode != 'json': - print(f'[!] WARNING: sysctl option "net.core.bpf_jit_harden" available for root is not found in {fname}, please try `sudo sysctl -a > {fname}`') + if 'kernel.cad_pid' not in parsed_options and mode != 'json': + print(f'[!] WARNING: sysctl option "kernel.cad_pid" available for root is not found in {fname}, please try `sudo sysctl -a > {fname}`') -def main(): +def main() -> None: # Report modes: # * verbose mode for # - reporting about unknown kernel options in the Kconfig @@ -256,7 +255,7 @@ def main(): if mode != 'json': print(f'[+] Special report mode: {mode}') - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] if args.config: if args.print: @@ -307,7 +306,7 @@ def main(): add_sysctl_checks(config_checklist, arch) # populate the checklist with the parsed Kconfig data - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} # type: Dict[str, str] parse_kconfig_file(mode, parsed_kconfig_options, args.config) populate_with_data(config_checklist, parsed_kconfig_options, 'kconfig') @@ -316,13 +315,13 @@ def main(): if args.cmdline: # populate the checklist with the parsed cmdline data - parsed_cmdline_options = OrderedDict() + parsed_cmdline_options = {} # type: Dict[str, str] parse_cmdline_file(mode, parsed_cmdline_options, args.cmdline) populate_with_data(config_checklist, parsed_cmdline_options, 'cmdline') if args.sysctl: # populate the checklist with the parsed sysctl data - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} # type: Dict[str, str] parse_sysctl_file(mode, parsed_sysctl_options, args.sysctl) populate_with_data(config_checklist, parsed_sysctl_options, 'sysctl') @@ -367,7 +366,7 @@ def main(): add_sysctl_checks(config_checklist, None) # populate the checklist with the parsed sysctl data - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} parse_sysctl_file(mode, parsed_sysctl_options, args.sysctl) populate_with_data(config_checklist, parsed_sysctl_options, 'sysctl') @@ -389,6 +388,7 @@ def main(): if mode and mode not in ('verbose', 'json'): sys.exit(f'[!] ERROR: wrong mode "{mode}" for --print') arch = args.print + assert(arch), 'unexpected empty arch from ArgumentParser' add_kconfig_checks(config_checklist, arch) add_cmdline_checks(config_checklist, arch) add_sysctl_checks(config_checklist, arch) @@ -398,10 +398,15 @@ def main(): sys.exit(0) if args.generate: - assert(args.config is None and args.cmdline is None and args.sysctl is None and args.print is None), 'unexpected args' + assert(args.config is None and + args.cmdline is None and + args.sysctl is None and + args.print is None), \ + 'unexpected args' if mode: sys.exit(f'[!] ERROR: wrong mode "{mode}" for --generate') arch = args.generate + assert(arch), 'unexpected empty arch from ArgumentParser' add_kconfig_checks(config_checklist, arch) print(f'CONFIG_{arch}=y') # the Kconfig fragment should describe the microarchitecture for opt in config_checklist: diff --git a/kernel_hardening_checker/checks.py b/kernel_hardening_checker/checks.py index 47036d6..f9b86d9 100644 --- a/kernel_hardening_checker/checks.py +++ b/kernel_hardening_checker/checks.py @@ -8,13 +8,14 @@ Author: Alexander Popov This module contains knowledge for checks. """ -# pylint: disable=missing-function-docstring,line-too-long,invalid-name +# pylint: disable=missing-function-docstring,line-too-long # pylint: disable=too-many-branches,too-many-statements,too-many-locals -from .engine import KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND +from typing import List +from .engine import StrOrNone, ChecklistObjType, KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND -def add_kconfig_checks(l, arch): +def add_kconfig_checks(l: List[ChecklistObjType], arch: str) -> None: assert(arch), 'empty arch' # Calling the KconfigCheck class constructor: @@ -64,11 +65,9 @@ def add_kconfig_checks(l, arch): if arch in ('X86_64', 'ARM64', 'ARM'): l += [vmap_stack_is_set] if arch in ('X86_64', 'X86_32'): - l += [KconfigCheck('self_protection', 'defconfig', 'SPECULATION_MITIGATIONS', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'DEBUG_WX', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'WERROR', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'X86_MCE', 'y')] - l += [KconfigCheck('self_protection', 'defconfig', 'RETPOLINE', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'SYN_COOKIES', 'y')] # another reason? microcode_is_set = KconfigCheck('self_protection', 'defconfig', 'MICROCODE', 'y') l += [microcode_is_set] # is needed for mitigating CPU bugs @@ -88,16 +87,26 @@ def add_kconfig_checks(l, arch): cpu_sup_intel_not_set)] l += [OR(KconfigCheck('self_protection', 'defconfig', 'X86_MCE_AMD', 'y'), cpu_sup_amd_not_set)] + l += [OR(KconfigCheck('self_protection', 'defconfig', 'CPU_MITIGATIONS', 'y'), + KconfigCheck('self_protection', 'defconfig', 'SPECULATION_MITIGATIONS', 'y'))] + l += [OR(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_RETPOLINE', 'y'), + KconfigCheck('self_protection', 'defconfig', 'RETPOLINE', 'y'))] + l += [OR(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_RFDS', 'y'), + cpu_sup_intel_not_set)] + l += [OR(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_SPECTRE_BHI', 'y'), + cpu_sup_intel_not_set)] if arch in ('ARM64', 'ARM'): l += [KconfigCheck('self_protection', 'defconfig', 'HW_RANDOM_TPM', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'IOMMU_DEFAULT_DMA_STRICT', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'IOMMU_DEFAULT_PASSTHROUGH', 'is not set')] # true if IOMMU_DEFAULT_DMA_STRICT is set l += [KconfigCheck('self_protection', 'defconfig', 'STACKPROTECTOR_PER_TASK', 'y')] if arch == 'X86_64': - l += [KconfigCheck('self_protection', 'defconfig', 'PAGE_TABLE_ISOLATION', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'RANDOMIZE_MEMORY', 'y')] l += [KconfigCheck('self_protection', 'defconfig', 'X86_KERNEL_IBT', 'y')] - l += [OR(KconfigCheck('self_protection', 'defconfig', 'CPU_SRSO', 'y'), + l += [OR(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_PAGE_TABLE_ISOLATION', 'y'), + KconfigCheck('self_protection', 'defconfig', 'PAGE_TABLE_ISOLATION', 'y'))] + l += [OR(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_SRSO', 'y'), + KconfigCheck('self_protection', 'defconfig', 'CPU_SRSO', 'y'), cpu_sup_amd_not_set)] l += [AND(KconfigCheck('self_protection', 'defconfig', 'INTEL_IOMMU', 'y'), iommu_support_is_set)] @@ -126,6 +135,8 @@ def add_kconfig_checks(l, arch): l += [KconfigCheck('self_protection', 'defconfig', 'DEBUG_ALIGN_RODATA', 'y')] # 'self_protection', 'kspp' + l += [KconfigCheck('self_protection', 'kspp', 'PAGE_TABLE_CHECK', 'y')] + l += [KconfigCheck('self_protection', 'kspp', 'PAGE_TABLE_CHECK_ENFORCED', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'BUG_ON_DATA_CORRUPTION', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'SLAB_FREELIST_HARDENED', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'SLAB_FREELIST_RANDOM', 'y')] @@ -233,7 +244,8 @@ def add_kconfig_checks(l, arch): l += [KconfigCheck('self_protection', 'kspp', 'DEFAULT_MMAP_MIN_ADDR', '32768')] l += [KconfigCheck('self_protection', 'kspp', 'SYN_COOKIES', 'y')] # another reason? if arch == 'X86_64': - l += [KconfigCheck('self_protection', 'kspp', 'SLS', 'y')] # vs CVE-2021-26341 in Straight-Line-Speculation + l += [OR(KconfigCheck('self_protection', 'kspp', 'MITIGATION_SLS', 'y'), + KconfigCheck('self_protection', 'kspp', 'SLS', 'y'))] # vs CVE-2021-26341 in Straight-Line-Speculation l += [AND(KconfigCheck('self_protection', 'kspp', 'INTEL_IOMMU_SVM', 'y'), iommu_support_is_set)] l += [AND(KconfigCheck('self_protection', 'kspp', 'AMD_IOMMU_V2', 'y'), @@ -241,11 +253,13 @@ def add_kconfig_checks(l, arch): if arch == 'ARM64': l += [KconfigCheck('self_protection', 'kspp', 'ARM64_SW_TTBR0_PAN', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'SHADOW_CALL_STACK', 'y')] + l += [KconfigCheck('self_protection', 'kspp', 'UNWIND_PATCH_PAC_INTO_SCS', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'KASAN_HW_TAGS', 'y')] # see also: kasan=on, kasan.stacktrace=off, kasan.fault=panic if arch == 'X86_32': - l += [KconfigCheck('self_protection', 'kspp', 'PAGE_TABLE_ISOLATION', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'HIGHMEM64G', 'y')] l += [KconfigCheck('self_protection', 'kspp', 'X86_PAE', 'y')] + l += [OR(KconfigCheck('self_protection', 'kspp', 'MITIGATION_PAGE_TABLE_ISOLATION', 'y'), + KconfigCheck('self_protection', 'kspp', 'PAGE_TABLE_ISOLATION', 'y'))] l += [AND(KconfigCheck('self_protection', 'kspp', 'INTEL_IOMMU', 'y'), iommu_support_is_set)] @@ -273,6 +287,8 @@ def add_kconfig_checks(l, arch): KconfigCheck('security_policy', 'a13xp0p0v', 'SECURITY_SMACK', 'y'), KconfigCheck('security_policy', 'a13xp0p0v', 'SECURITY_TOMOYO', 'y'))] # one of major LSMs implementing MAC + # N.B. We don't use 'if arch' for the 'cut_attack_surface' checks that require 'is not set'. + # It makes the maintainance easier. These kernel options should be disabled anyway. # 'cut_attack_surface', 'defconfig' l += [KconfigCheck('cut_attack_surface', 'defconfig', 'SECCOMP', 'y')] l += [KconfigCheck('cut_attack_surface', 'defconfig', 'SECCOMP_FILTER', 'y')] @@ -422,7 +438,7 @@ def add_kconfig_checks(l, arch): l += [KconfigCheck('harden_userspace', 'a13xp0p0v', 'X86_USER_SHADOW_STACK', 'y')] -def add_cmdline_checks(l, arch): +def add_cmdline_checks(l: List[ChecklistObjType], arch: str) -> None: assert(arch), 'empty arch' # Calling the CmdlineCheck class constructor: @@ -464,6 +480,10 @@ def add_cmdline_checks(l, arch): l += [OR(CmdlineCheck('self_protection', 'defconfig', 'spectre_v2_user', 'is not off'), AND(CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), CmdlineCheck('self_protection', 'defconfig', 'spectre_v2_user', 'is not set')))] + l += [OR(CmdlineCheck('self_protection', 'defconfig', 'spectre_bhi', 'is not off'), + AND(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_SPECTRE_BHI', 'y'), + CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), + CmdlineCheck('self_protection', 'defconfig', 'spectre_bhi', 'is not set')))] l += [OR(CmdlineCheck('self_protection', 'defconfig', 'spec_store_bypass_disable', 'is not off'), AND(CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), CmdlineCheck('self_protection', 'defconfig', 'spec_store_bypass_disable', 'is not set')))] @@ -491,6 +511,10 @@ def add_cmdline_checks(l, arch): l += [OR(CmdlineCheck('self_protection', 'defconfig', 'gather_data_sampling', 'is not off'), AND(CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), CmdlineCheck('self_protection', 'defconfig', 'gather_data_sampling', 'is not set')))] + l += [OR(CmdlineCheck('self_protection', 'defconfig', 'reg_file_data_sampling', 'is not off'), + AND(KconfigCheck('self_protection', 'defconfig', 'MITIGATION_RFDS', 'y'), + CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), + CmdlineCheck('self_protection', 'defconfig', 'reg_file_data_sampling', 'is not set')))] if arch == 'ARM64': l += [OR(CmdlineCheck('self_protection', 'defconfig', 'kpti', 'is not off'), AND(CmdlineCheck('self_protection', 'kspp', 'mitigations', 'auto,nosmt'), @@ -610,6 +634,7 @@ no_kstrtobool_options = [ 'pti', # See pti_check_boottime_disable() in arch/x86/mm/pti.c 'spectre_v2', # See spectre_v2_parse_cmdline() in arch/x86/kernel/cpu/bugs.c 'spectre_v2_user', # See spectre_v2_parse_user_cmdline() in arch/x86/kernel/cpu/bugs.c + 'spectre_bhi', # See spectre_bhi_parse_cmdline() in arch/x86/kernel/cpu/bugs.c 'spec_store_bypass_disable', # See ssb_parse_cmdline() in arch/x86/kernel/cpu/bugs.c 'l1tf', # See l1tf_cmdline() in arch/x86/kernel/cpu/bugs.c 'mds', # See mds_cmdline() in arch/x86/kernel/cpu/bugs.c @@ -621,6 +646,7 @@ no_kstrtobool_options = [ 'ssbd', # See parse_spectre_v4_param() in arch/arm64/kernel/proton-pack.c 'spec_rstack_overflow', # See srso_parse_cmdline() in arch/x86/kernel/cpu/bugs.c 'gather_data_sampling', # See gds_parse_cmdline() in arch/x86/kernel/cpu/bugs.c + 'reg_file_data_sampling', # See rfds_parse_cmdline() in arch/x86/kernel/cpu/bugs.c 'slub_debug', # See setup_slub_debug() in mm/slub.c 'iommu', # See iommu_setup() in arch/x86/kernel/pci-dma.c 'vsyscall', # See vsyscall_setup() in arch/x86/entry/vsyscall/vsyscall_64.c @@ -630,7 +656,7 @@ no_kstrtobool_options = [ ] -def normalize_cmdline_options(option, value): +def normalize_cmdline_options(option: str, value: str) -> str: # Don't normalize the cmdline option values if # the Linux kernel doesn't use kstrtobool() for them if option in no_kstrtobool_options: @@ -646,7 +672,7 @@ def normalize_cmdline_options(option, value): return value -# TODO: draft of security hardening sysctls: +# Ideas of security hardening sysctls: # what about bpf_jit_enable? # vm.mmap_min_addr has a good value # nosmt sysfs control file @@ -657,27 +683,40 @@ def normalize_cmdline_options(option, value): # kernel.warn_limit (think about a proper value) # net.ipv4.tcp_syncookies=1 (?) -def add_sysctl_checks(l, _arch): +def add_sysctl_checks(l: List[ChecklistObjType], _arch: StrOrNone) -> None: # This function may be called with arch=None # Calling the SysctlCheck class constructor: # SysctlCheck(reason, decision, name, expected) - l += [SysctlCheck('self_protection', 'kspp', 'net.core.bpf_jit_harden', '2')] + # Use an omnipresent kconfig symbol to see if we have a kconfig file for checking + have_kconfig = KconfigCheck('-', '-', 'LOCALVERSION', 'is present') + + l += [OR(SysctlCheck('self_protection', 'kspp', 'net.core.bpf_jit_harden', '2'), + AND(KconfigCheck('-', '-', 'BPF_JIT', 'is not set'), + have_kconfig))] l += [SysctlCheck('cut_attack_surface', 'kspp', 'kernel.dmesg_restrict', '1')] l += [SysctlCheck('cut_attack_surface', 'kspp', 'kernel.perf_event_paranoid', '3')] # with a custom patch, see https://lwn.net/Articles/696216/ - l += [SysctlCheck('cut_attack_surface', 'kspp', 'kernel.kexec_load_disabled', '1')] l += [SysctlCheck('cut_attack_surface', 'kspp', 'user.max_user_namespaces', '0')] # may break the upower daemon in Ubuntu l += [SysctlCheck('cut_attack_surface', 'kspp', 'dev.tty.ldisc_autoload', '0')] - l += [SysctlCheck('cut_attack_surface', 'kspp', 'kernel.unprivileged_bpf_disabled', '1')] l += [SysctlCheck('cut_attack_surface', 'kspp', 'kernel.kptr_restrict', '2')] l += [SysctlCheck('cut_attack_surface', 'kspp', 'dev.tty.legacy_tiocsti', '0')] - l += [SysctlCheck('cut_attack_surface', 'kspp', 'vm.unprivileged_userfaultfd', '0')] + l += [OR(SysctlCheck('cut_attack_surface', 'kspp', 'kernel.kexec_load_disabled', '1'), + AND(KconfigCheck('-', '-', 'KEXEC_CORE', 'is not set'), + have_kconfig))] + l += [OR(SysctlCheck('cut_attack_surface', 'kspp', 'kernel.unprivileged_bpf_disabled', '1'), + AND(KconfigCheck('cut_attack_surface', 'lockdown', 'BPF_SYSCALL', 'is not set'), + have_kconfig))] + l += [OR(SysctlCheck('cut_attack_surface', 'kspp', 'vm.unprivileged_userfaultfd', '0'), + AND(KconfigCheck('cut_attack_surface', 'grsec', 'USERFAULTFD', 'is not set'), + have_kconfig))] # At first, it disabled unprivileged userfaultfd, # and since v5.11 it enables unprivileged userfaultfd for user-mode only. - l += [SysctlCheck('cut_attack_surface', 'clipos', 'kernel.modules_disabled', '1')] # radical, but may be useful in some cases + l += [OR(SysctlCheck('cut_attack_surface', 'clipos', 'kernel.modules_disabled', '1'), + AND(KconfigCheck('cut_attack_surface', 'kspp', 'MODULES', 'is not set'), + have_kconfig))] # radical, but may be useful in some cases l += [SysctlCheck('harden_userspace', 'kspp', 'fs.protected_symlinks', '1')] l += [SysctlCheck('harden_userspace', 'kspp', 'fs.protected_hardlinks', '1')] diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-cmdline-x86-64.txt b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-cmdline-x86-64.txt index e053b2d..f2a666c 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-cmdline-x86-64.txt +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-cmdline-x86-64.txt @@ -1 +1 @@ -hardened_usercopy=1 init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on page_alloc.shuffle=1 slab_nomerge pti=on nosmt slub_debug=ZF slub_debug=P page_poison=1 iommu.passthrough=0 iommu.strict=1 mitigations=auto,nosmt vsyscall=none vdso32=0 +hardened_usercopy=1 init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on page_alloc.shuffle=1 slab_nomerge pti=on nosmt slub_debug=ZF slub_debug=P page_poison=1 iommu.passthrough=0 iommu.strict=1 mitigations=auto,nosmt vsyscall=none vdso32=0 cfi=kcfi diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm.config b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm.config index c750260..e0818e3 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm.config +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm.config @@ -25,6 +25,7 @@ CONFIG_IO_STRICT_DEVMEM=y CONFIG_SYN_COOKIES=y # Perform additional validation of various commonly targeted structures. +CONFIG_LIST_HARDENED=y CONFIG_DEBUG_CREDENTIALS=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_DEBUG_LIST=y @@ -52,6 +53,7 @@ CONFIG_SECURITY_LANDLOCK=y # Make sure SELinux cannot be disabled trivially. # CONFIG_SECURITY_SELINUX_BOOTPARAM is not set # CONFIG_SECURITY_SELINUX_DEVELOP is not set +# CONFIG_SECURITY_SELINUX_DEBUG is not set # CONFIG_SECURITY_WRITABLE_HOOKS is not set # Enable "lockdown" LSM for bright line between the root user and kernel memory. @@ -67,11 +69,19 @@ CONFIG_HARDENED_USERCOPY=y # Randomize allocator freelists, harden metadata. CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y +CONFIG_RANDOM_KMALLOC_CACHES=y + +# Make cross-slab heap attacks not as trivial when object sizes are the same. (Same as slab_nomerge boot param.) +# CONFIG_SLAB_MERGE_DEFAULT is not set # Allow for randomization of high-order page allocation freelist. Must be enabled with # the "page_alloc.shuffle=1" command line below). CONFIG_SHUFFLE_PAGE_ALLOCATOR=y +# Sanity check userspace page table mappings (since v5.17) +CONFIG_PAGE_TABLE_CHECK=y +CONFIG_PAGE_TABLE_CHECK_ENFORCED=y + # Allow allocator validation checking to be enabled (see "slub_debug=P" below). CONFIG_SLUB_DEBUG=y @@ -118,6 +128,7 @@ CONFIG_UBSAN_LOCAL_BOUNDS=y # Enable sampling-based overflow detection (since v5.12). This is similar to KASAN coverage, but with almost zero runtime overhead. CONFIG_KFENCE=y +CONFIG_KFENCE_SAMPLE_INTERVAL=100 # Randomize kernel stack offset on syscall entry (since v5.13). CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y @@ -196,10 +207,14 @@ CONFIG_STATIC_USERMODEHELPER=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=-1 +# Limit sysrq to sync,unmount,reboot. For more details see the sysrq bit field table. +CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=176 + # Keep root from altering kernel memory via loadable modules. # CONFIG_MODULES is not set # But if CONFIG_MODULE=y is needed, at least they must be signed with a per-build key. +# See also kernel.modules_disabled sysctl below. CONFIG_STRICT_MODULE_RWX=y CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_FORCE=y @@ -207,6 +222,7 @@ CONFIG_MODULE_SIG_ALL=y CONFIG_MODULE_SIG_SHA512=y CONFIG_MODULE_SIG_HASH="sha512" CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" +# CONFIG_MODULE_FORCE_LOAD is not set # GCC plugins diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm64.config b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm64.config index c059256..a68f819 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm64.config +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-arm64.config @@ -25,6 +25,7 @@ CONFIG_IO_STRICT_DEVMEM=y CONFIG_SYN_COOKIES=y # Perform additional validation of various commonly targeted structures. +CONFIG_LIST_HARDENED=y CONFIG_DEBUG_CREDENTIALS=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_DEBUG_LIST=y @@ -52,6 +53,7 @@ CONFIG_SECURITY_LANDLOCK=y # Make sure SELinux cannot be disabled trivially. # CONFIG_SECURITY_SELINUX_BOOTPARAM is not set # CONFIG_SECURITY_SELINUX_DEVELOP is not set +# CONFIG_SECURITY_SELINUX_DEBUG is not set # CONFIG_SECURITY_WRITABLE_HOOKS is not set # Enable "lockdown" LSM for bright line between the root user and kernel memory. @@ -67,11 +69,19 @@ CONFIG_HARDENED_USERCOPY=y # Randomize allocator freelists, harden metadata. CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y +CONFIG_RANDOM_KMALLOC_CACHES=y + +# Make cross-slab heap attacks not as trivial when object sizes are the same. (Same as slab_nomerge boot param.) +# CONFIG_SLAB_MERGE_DEFAULT is not set # Allow for randomization of high-order page allocation freelist. Must be enabled with # the "page_alloc.shuffle=1" command line below). CONFIG_SHUFFLE_PAGE_ALLOCATOR=y +# Sanity check userspace page table mappings (since v5.17) +CONFIG_PAGE_TABLE_CHECK=y +CONFIG_PAGE_TABLE_CHECK_ENFORCED=y + # Allow allocator validation checking to be enabled (see "slub_debug=P" below). CONFIG_SLUB_DEBUG=y @@ -118,6 +128,7 @@ CONFIG_UBSAN_LOCAL_BOUNDS=y # Enable sampling-based overflow detection (since v5.12). This is similar to KASAN coverage, but with almost zero runtime overhead. CONFIG_KFENCE=y +CONFIG_KFENCE_SAMPLE_INTERVAL=100 # Randomize kernel stack offset on syscall entry (since v5.13). CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y @@ -196,10 +207,14 @@ CONFIG_STATIC_USERMODEHELPER=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=-1 +# Limit sysrq to sync,unmount,reboot. For more details see the sysrq bit field table. +CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=176 + # Keep root from altering kernel memory via loadable modules. # CONFIG_MODULES is not set # But if CONFIG_MODULE=y is needed, at least they must be signed with a per-build key. +# See also kernel.modules_disabled sysctl below. CONFIG_STRICT_MODULE_RWX=y CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_FORCE=y @@ -207,6 +222,7 @@ CONFIG_MODULE_SIG_ALL=y CONFIG_MODULE_SIG_SHA512=y CONFIG_MODULE_SIG_HASH="sha512" CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" +# CONFIG_MODULE_FORCE_LOAD is not set # GCC plugins @@ -250,8 +266,9 @@ CONFIG_ARM64_SW_TTBR0_PAN=y # Enable Kernel Page Table Isolation to remove an entire class of cache timing side-channels. CONFIG_UNMAP_KERNEL_AT_EL0=y -# Software Shadow Stack or PAC +# Enable Software Shadow Stack when hardware Pointer Authentication (PAC) isn't available. CONFIG_SHADOW_CALL_STACK=y +CONFIG_UNWIND_PATCH_PAC_INTO_SCS=y # Pointer authentication (ARMv8.3 and later). If hardware actually supports it, one can # turn off CONFIG_STACKPROTECTOR_STRONG with this enabled. diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-32.config b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-32.config index 9db30cb..a88dde5 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-32.config +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-32.config @@ -25,6 +25,7 @@ CONFIG_IO_STRICT_DEVMEM=y CONFIG_SYN_COOKIES=y # Perform additional validation of various commonly targeted structures. +CONFIG_LIST_HARDENED=y CONFIG_DEBUG_CREDENTIALS=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_DEBUG_LIST=y @@ -52,6 +53,7 @@ CONFIG_SECURITY_LANDLOCK=y # Make sure SELinux cannot be disabled trivially. # CONFIG_SECURITY_SELINUX_BOOTPARAM is not set # CONFIG_SECURITY_SELINUX_DEVELOP is not set +# CONFIG_SECURITY_SELINUX_DEBUG is not set # CONFIG_SECURITY_WRITABLE_HOOKS is not set # Enable "lockdown" LSM for bright line between the root user and kernel memory. @@ -67,11 +69,19 @@ CONFIG_HARDENED_USERCOPY=y # Randomize allocator freelists, harden metadata. CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y +CONFIG_RANDOM_KMALLOC_CACHES=y + +# Make cross-slab heap attacks not as trivial when object sizes are the same. (Same as slab_nomerge boot param.) +# CONFIG_SLAB_MERGE_DEFAULT is not set # Allow for randomization of high-order page allocation freelist. Must be enabled with # the "page_alloc.shuffle=1" command line below). CONFIG_SHUFFLE_PAGE_ALLOCATOR=y +# Sanity check userspace page table mappings (since v5.17) +CONFIG_PAGE_TABLE_CHECK=y +CONFIG_PAGE_TABLE_CHECK_ENFORCED=y + # Allow allocator validation checking to be enabled (see "slub_debug=P" below). CONFIG_SLUB_DEBUG=y @@ -118,6 +128,7 @@ CONFIG_UBSAN_LOCAL_BOUNDS=y # Enable sampling-based overflow detection (since v5.12). This is similar to KASAN coverage, but with almost zero runtime overhead. CONFIG_KFENCE=y +CONFIG_KFENCE_SAMPLE_INTERVAL=100 # Randomize kernel stack offset on syscall entry (since v5.13). CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y @@ -196,10 +207,14 @@ CONFIG_STATIC_USERMODEHELPER=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=-1 +# Limit sysrq to sync,unmount,reboot. For more details see the sysrq bit field table. +CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=176 + # Keep root from altering kernel memory via loadable modules. # CONFIG_MODULES is not set # But if CONFIG_MODULE=y is needed, at least they must be signed with a per-build key. +# See also kernel.modules_disabled sysctl below. CONFIG_STRICT_MODULE_RWX=y CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_FORCE=y @@ -207,6 +222,7 @@ CONFIG_MODULE_SIG_ALL=y CONFIG_MODULE_SIG_SHA512=y CONFIG_MODULE_SIG_HASH="sha512" CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" +# CONFIG_MODULE_FORCE_LOAD is not set # GCC plugins diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-64.config b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-64.config index f374cda..cd9afbd 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-64.config +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-kconfig-x86-64.config @@ -25,6 +25,7 @@ CONFIG_IO_STRICT_DEVMEM=y CONFIG_SYN_COOKIES=y # Perform additional validation of various commonly targeted structures. +CONFIG_LIST_HARDENED=y CONFIG_DEBUG_CREDENTIALS=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_DEBUG_LIST=y @@ -52,6 +53,7 @@ CONFIG_SECURITY_LANDLOCK=y # Make sure SELinux cannot be disabled trivially. # CONFIG_SECURITY_SELINUX_BOOTPARAM is not set # CONFIG_SECURITY_SELINUX_DEVELOP is not set +# CONFIG_SECURITY_SELINUX_DEBUG is not set # CONFIG_SECURITY_WRITABLE_HOOKS is not set # Enable "lockdown" LSM for bright line between the root user and kernel memory. @@ -67,11 +69,19 @@ CONFIG_HARDENED_USERCOPY=y # Randomize allocator freelists, harden metadata. CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y +CONFIG_RANDOM_KMALLOC_CACHES=y + +# Make cross-slab heap attacks not as trivial when object sizes are the same. (Same as slab_nomerge boot param.) +# CONFIG_SLAB_MERGE_DEFAULT is not set # Allow for randomization of high-order page allocation freelist. Must be enabled with # the "page_alloc.shuffle=1" command line below). CONFIG_SHUFFLE_PAGE_ALLOCATOR=y +# Sanity check userspace page table mappings (since v5.17) +CONFIG_PAGE_TABLE_CHECK=y +CONFIG_PAGE_TABLE_CHECK_ENFORCED=y + # Allow allocator validation checking to be enabled (see "slub_debug=P" below). CONFIG_SLUB_DEBUG=y @@ -118,6 +128,7 @@ CONFIG_UBSAN_LOCAL_BOUNDS=y # Enable sampling-based overflow detection (since v5.12). This is similar to KASAN coverage, but with almost zero runtime overhead. CONFIG_KFENCE=y +CONFIG_KFENCE_SAMPLE_INTERVAL=100 # Randomize kernel stack offset on syscall entry (since v5.13). CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y @@ -196,10 +207,14 @@ CONFIG_STATIC_USERMODEHELPER=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=-1 +# Limit sysrq to sync,unmount,reboot. For more details see the sysrq bit field table. +CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=176 + # Keep root from altering kernel memory via loadable modules. # CONFIG_MODULES is not set # But if CONFIG_MODULE=y is needed, at least they must be signed with a per-build key. +# See also kernel.modules_disabled sysctl below. CONFIG_STRICT_MODULE_RWX=y CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_FORCE=y @@ -207,6 +222,7 @@ CONFIG_MODULE_SIG_ALL=y CONFIG_MODULE_SIG_SHA512=y CONFIG_MODULE_SIG_HASH="sha512" CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" +# CONFIG_MODULE_FORCE_LOAD is not set # GCC plugins @@ -253,6 +269,12 @@ CONFIG_LEGACY_VSYSCALL_NONE=y # Enable Kernel Page Table Isolation to remove an entire class of cache timing side-channels. CONFIG_PAGE_TABLE_ISOLATION=y +# Enforce CET Indirect Branch Tracking in the kernel. (Since v5.18) +CONFIG_X86_KERNEL_IBT=y + +# Support userspace CET Shadow Stack +CONFIG_X86_USER_SHADOW_STACK=y + # Remove additional (32-bit) attack surface, unless you really need them. # CONFIG_COMPAT is not set # CONFIG_IA32_EMULATION is not set @@ -270,6 +292,6 @@ CONFIG_AMD_IOMMU_V2=y # Straight-Line-Speculation CONFIG_SLS=y -# Enable Control Flow Integrity (since v6.1) +# Enable Control Flow Integrity (since v6.1). CONFIG_CFI_CLANG=y # CONFIG_CFI_PERMISSIVE is not set diff --git a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-sysctl.txt b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-sysctl.txt index 9f99c6c..c45c201 100644 --- a/kernel_hardening_checker/config_files/kspp-recommendations/kspp-sysctl.txt +++ b/kernel_hardening_checker/config_files/kspp-recommendations/kspp-sysctl.txt @@ -1,6 +1,7 @@ kernel.printk = 3 4 1 7 kernel.kptr_restrict = 2 kernel.dmesg_restrict = 1 +kernel.disable_modules = 1 kernel.perf_event_paranoid = 3 kernel.kexec_load_disabled = 1 kernel.randomize_va_space = 2 @@ -9,6 +10,8 @@ user.max_user_namespaces = 0 dev.tty.ldisc_autoload = 0 dev.tty.legacy_tiocsti = 0 kernel.unprivileged_bpf_disabled = 1 +kernel.warn_limit = 1 +kernel.oops_limit = 1 net.core.bpf_jit_harden = 2 vm.unprivileged_userfaultfd = 0 fs.protected_symlinks = 1 diff --git a/kernel_hardening_checker/engine.py b/kernel_hardening_checker/engine.py index 56aa80f..ee56d63 100644 --- a/kernel_hardening_checker/engine.py +++ b/kernel_hardening_checker/engine.py @@ -9,15 +9,23 @@ This module is the engine of checks. """ # pylint: disable=missing-class-docstring,missing-function-docstring -# pylint: disable=line-too-long,invalid-name,too-many-branches +# pylint: disable=line-too-long,too-many-branches +from __future__ import annotations import sys +from typing import Union, Optional, List, Dict, Tuple +StrOrNone = Optional[str] +TupleOrNone = Optional[Tuple[int, ...]] +DictOrTuple = Union[Dict[str, str], Tuple[int, ...]] +StrOrBool = Union[str, bool] + GREEN_COLOR = '\x1b[32m' RED_COLOR = '\x1b[31m' COLOR_END = '\x1b[0m' -def colorize_result(input_text): + +def colorize_result(input_text: StrOrNone) -> StrOrNone: if input_text is None or not sys.stdout.isatty(): return input_text if input_text.startswith('OK'): @@ -29,20 +37,23 @@ def colorize_result(input_text): class OptCheck: - def __init__(self, reason, decision, name, expected): - assert(name and name == name.strip() and len(name.split()) == 1), \ + def __init__(self, reason: str, decision: str, name: str, expected: str) -> None: + assert(name and isinstance(name, str) and + name == name.strip() and len(name.split()) == 1), \ f'invalid name "{name}" for {self.__class__.__name__}' self.name = name - assert(decision and decision == decision.strip() and len(decision.split()) == 1), \ + assert(decision and isinstance(decision, str) and + decision == decision.strip() and len(decision.split()) == 1), \ f'invalid decision "{decision}" for "{name}" check' self.decision = decision - assert(reason and reason == reason.strip() and len(reason.split()) == 1), \ + assert(reason and isinstance(reason, str) and + reason == reason.strip() and len(reason.split()) == 1), \ f'invalid reason "{reason}" for "{name}" check' self.reason = reason - assert(expected and expected == expected.strip()), \ + assert(expected and isinstance(expected, str) and expected == expected.strip()), \ f'invalid expected value "{expected}" for "{name}" check (1)' val_len = len(expected.split()) if val_len == 3: @@ -56,19 +67,19 @@ class OptCheck: f'invalid expected value "{expected}" for "{name}" check (4)' self.expected = expected - self.state = None - self.result = None + self.state = None # type: str | None + self.result = None # type: str | None @property - def opt_type(self): + def opt_type(self) -> StrOrNone: return None - def set_state(self, data): + def set_state(self, data: StrOrNone) -> None: assert(data is None or isinstance(data, str)), \ f'invalid state "{data}" for "{self.name}" check' self.state = data - def check(self): + def check(self) -> None: # handle the 'is present' check if self.expected == 'is present': if self.state is None: @@ -100,67 +111,72 @@ class OptCheck: else: self.result = f'FAIL: "{self.state}"' - def table_print(self, _mode, with_results): + def table_print(self, _mode: StrOrNone, with_results: bool) -> None: print(f'{self.name:<40}|{self.opt_type:^7}|{self.expected:^12}|{self.decision:^10}|{self.reason:^18}', end='') if with_results: print(f'| {colorize_result(self.result)}', end='') - def json_dump(self, with_results): + def json_dump(self, with_results: bool) -> Dict[str, StrOrBool]: + assert(self.opt_type), f'unexpected empty opt_type in {self.name}' dump = { "option_name": self.name, "type": self.opt_type, "desired_val": self.expected, "decision": self.decision, "reason": self.reason, - } + } # type: Dict[str, StrOrBool] if with_results: + assert(self.result), f'unexpected empty result in {self.name}' dump["check_result"] = self.result dump["check_result_bool"] = self.result.startswith('OK') return dump class KconfigCheck(OptCheck): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, *args: str) -> None: + super().__init__(*args) self.name = f'CONFIG_{self.name}' @property - def opt_type(self): + def opt_type(self) -> str: return 'kconfig' class CmdlineCheck(OptCheck): @property - def opt_type(self): + def opt_type(self) -> str: return 'cmdline' class SysctlCheck(OptCheck): @property - def opt_type(self): + def opt_type(self) -> str: return 'sysctl' class VersionCheck: - def __init__(self, ver_expected): + def __init__(self, ver_expected: Tuple[int, int, int]) -> None: assert(ver_expected and isinstance(ver_expected, tuple) and len(ver_expected) == 3), \ f'invalid expected version "{ver_expected}" for VersionCheck (1)' assert(all(map(lambda x: isinstance(x, int), ver_expected))), \ f'invalid expected version "{ver_expected}" for VersionCheck (2)' self.ver_expected = ver_expected - self.ver = () - self.result = None + self.ver = (0, 0, 0) # type: Tuple[int, ...] + self.result = None # type: str | None @property - def opt_type(self): + def opt_type(self) -> str: return 'version' - def set_state(self, data): + def set_state(self, data: Tuple[int, ...]) -> None: assert(data and isinstance(data, tuple) and len(data) >= 3), \ - f'invalid version "{data}" for VersionCheck' + f'invalid version "{data}" for VersionCheck (1)' + assert(all(map(lambda x: isinstance(x, int), data))), \ + f'invalid version "{data}" for VersionCheck (2)' self.ver = data[:3] - def check(self): + def check(self) -> None: + assert(self.ver[0] >= 2), 'not initialized kernel version' if self.ver[0] > self.ver_expected[0]: self.result = f'OK: version >= {self.ver_expected}' return @@ -180,7 +196,7 @@ class VersionCheck: return self.result = f'FAIL: version < {self.ver_expected}' - def table_print(self, _mode, with_results): + def table_print(self, _mode: StrOrNone, with_results: bool) -> None: ver_req = f'kernel version >= {self.ver_expected}' print(f'{ver_req:<91}', end='') if with_results: @@ -188,29 +204,31 @@ class VersionCheck: class ComplexOptCheck: - def __init__(self, *opts): + def __init__(self, *opts: AnyOptCheckType) -> None: self.opts = opts assert(self.opts), \ f'empty {self.__class__.__name__} check' assert(len(self.opts) != 1), \ f'useless {self.__class__.__name__} check: {opts}' - assert(isinstance(opts[0], (KconfigCheck, CmdlineCheck, SysctlCheck))), \ + assert(isinstance(self.opts[0], SimpleNamedOptCheckTypes)), \ f'invalid {self.__class__.__name__} check: {opts}' - self.result = None + self.result = None # type: str | None @property - def opt_type(self): + def opt_type(self) -> str: return 'complex' @property - def name(self): + def name(self) -> str: + assert hasattr(self.opts[0], 'name') # true for SimpleNamedOptCheckTypes return self.opts[0].name @property - def expected(self): + def expected(self) -> str: + assert hasattr(self.opts[0], 'expected') # true for SimpleNamedOptCheckTypes return self.opts[0].expected - def table_print(self, mode, with_results): + def table_print(self, mode: StrOrNone, with_results: bool) -> None: if mode == 'verbose': class_name = f'<<< {self.__class__.__name__} >>>' print(f' {class_name:87}', end='') @@ -225,10 +243,12 @@ class ComplexOptCheck: if with_results: print(f'| {colorize_result(self.result)}', end='') - def json_dump(self, with_results): + def json_dump(self, with_results: bool) -> Dict[str, StrOrBool]: + assert hasattr(self.opts[0], 'json_dump') # true for SimpleNamedOptCheckTypes dump = self.opts[0].json_dump(False) if with_results: # Add the 'check_result' and 'check_result_bool' keys to the dictionary + assert(self.result), f'unexpected empty result in {self.name}' dump["check_result"] = self.result dump["check_result_bool"] = self.result.startswith('OK') return dump @@ -239,25 +259,29 @@ class OR(ComplexOptCheck): # Use cases: # OR(, ) # OR(, ) - def check(self): + def check(self) -> None: for i, opt in enumerate(self.opts): opt.check() + assert(opt.result), 'unexpected empty result of the OR sub-check' if opt.result.startswith('OK'): self.result = opt.result - # Add more info for additional checks: if i != 0: - if opt.result == 'OK': - self.result = f'OK: {opt.name} is "{opt.expected}"' - elif opt.result == 'OK: is not found': - self.result = f'OK: {opt.name} is not found' - elif opt.result == 'OK: is present': - self.result = f'OK: {opt.name} is present' - elif opt.result.startswith('OK: is not off'): - self.result = f'OK: {opt.name} is not off' - else: - # VersionCheck provides enough info + # Add more info for additional checks: + if isinstance(opt, VersionCheck): assert(opt.result.startswith('OK: version')), \ - f'unexpected OK description "{opt.result}"' + f'unexpected VersionCheck result {opt.result}' + # VersionCheck provides enough info, nothing to add + else: + if opt.result == 'OK': + self.result = f'OK: {opt.name} is "{opt.expected}"' + elif opt.result == 'OK: is not found': + self.result = f'OK: {opt.name} is not found' + elif opt.result == 'OK: is present': + self.result = f'OK: {opt.name} is present' + else: + assert(opt.result.startswith('OK: is not off')), \ + f'unexpected OK description "{opt.result}"' + self.result = f'OK: {opt.name} is not off' return self.result = self.opts[0].result @@ -268,9 +292,10 @@ class AND(ComplexOptCheck): # AND(, ) # Suboption is not checked if checking of the main_option is failed. # AND(, ) - def check(self): + def check(self) -> None: for i, opt in reversed(list(enumerate(self.opts))): opt.check() + assert(opt.result), 'unexpected empty result of the AND sub-check' if i == 0: self.result = opt.result return @@ -278,72 +303,125 @@ class AND(ComplexOptCheck): # This FAIL is caused by additional checks, # and not by the main option that this AND-check is about. # Describe the reason of the FAIL. - if opt.result.startswith('FAIL: \"') or opt.result == 'FAIL: is not found': - self.result = f'FAIL: {opt.name} is not "{opt.expected}"' - elif opt.result == 'FAIL: is not present': - self.result = f'FAIL: {opt.name} is not present' - elif opt.result in ('FAIL: is off', 'FAIL: is off, "0"'): - self.result = f'FAIL: {opt.name} is off' - elif opt.result == 'FAIL: is off, not found': - self.result = f'FAIL: {opt.name} is off, not found' - else: - # VersionCheck provides enough info - self.result = opt.result + if isinstance(opt, VersionCheck): assert(opt.result.startswith('FAIL: version')), \ - f'unexpected FAIL description "{opt.result}"' + f'unexpected VersionCheck result {opt.result}' + self.result = opt.result # VersionCheck provides enough info + else: + if opt.result.startswith('FAIL: \"') or opt.result == 'FAIL: is not found': + self.result = f'FAIL: {opt.name} is not "{opt.expected}"' + elif opt.result == 'FAIL: is not present': + self.result = f'FAIL: {opt.name} is not present' + elif opt.result in ('FAIL: is off', 'FAIL: is off, "0"'): + self.result = f'FAIL: {opt.name} is off' + else: + assert(opt.result == 'FAIL: is off, not found'), \ + f'unexpected FAIL description "{opt.result}"' + self.result = f'FAIL: {opt.name} is off, not found' return +# All classes are declared, let's define typing: +# 1) basic simple check objects SIMPLE_OPTION_TYPES = ('kconfig', 'cmdline', 'sysctl', 'version') +SimpleOptCheckType = Union[KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck] +SimpleOptCheckTypes = (KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck) +SimpleNamedOptCheckType = Union[KconfigCheck, CmdlineCheck, SysctlCheck] +SimpleNamedOptCheckTypes = (KconfigCheck, CmdlineCheck, SysctlCheck) + +# 2) complex objects that may contain complex and simple objects +ComplexOptCheckType = Union[OR, AND] +ComplexOptCheckTypes = (OR, AND) + +# 3) objects that can be added to the checklist +ChecklistObjType = Union[KconfigCheck, CmdlineCheck, SysctlCheck, OR, AND] +# 4) all existing objects +AnyOptCheckType = Union[KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND] -def populate_simple_opt_with_data(opt, data, data_type): - assert(opt.opt_type != 'complex'), \ - f'unexpected ComplexOptCheck "{opt.name}"' - assert(opt.opt_type in SIMPLE_OPTION_TYPES), \ - f'invalid opt_type "{opt.opt_type}"' - assert(data_type in SIMPLE_OPTION_TYPES), \ - f'invalid data_type "{data_type}"' - assert(data), \ - 'empty data' + +def populate_simple_opt_with_data(opt: SimpleOptCheckType, data: DictOrTuple, data_type: str) -> None: + assert(opt.opt_type != 'complex'), f'unexpected opt_type "{opt.opt_type}" for {opt}' + assert(opt.opt_type in SIMPLE_OPTION_TYPES), f'invalid opt_type "{opt.opt_type}"' + assert(data_type in SIMPLE_OPTION_TYPES), f'invalid data_type "{data_type}"' + assert(data), 'empty data' if data_type != opt.opt_type: return if data_type in ('kconfig', 'cmdline', 'sysctl'): + assert(isinstance(data, dict)), \ + f'unexpected data with data_type {data_type}' + assert(isinstance(opt, SimpleNamedOptCheckTypes)), \ + f'unexpected VersionCheck with opt_type "{opt.opt_type}"' opt.set_state(data.get(opt.name, None)) else: - assert(data_type == 'version'), \ + assert(isinstance(data, tuple)), \ + f'unexpected verion data with data_type {data_type}' + assert(isinstance(opt, VersionCheck) and data_type == 'version'), \ f'unexpected data_type "{data_type}"' opt.set_state(data) -def populate_opt_with_data(opt, data, data_type): +def populate_opt_with_data(opt: AnyOptCheckType, data: DictOrTuple, data_type: str) -> None: assert(opt.opt_type != 'version'), 'a single VersionCheck is useless' if opt.opt_type != 'complex': + assert(isinstance(opt, SimpleOptCheckTypes)), \ + f'unexpected object {opt} with opt_type "{opt.opt_type}"' populate_simple_opt_with_data(opt, data, data_type) else: + assert(isinstance(opt, ComplexOptCheckTypes)), \ + f'unexpected object {opt} with opt_type "{opt.opt_type}"' for o in opt.opts: if o.opt_type != 'complex': + assert(isinstance(o, SimpleOptCheckTypes)), \ + f'unexpected object {o} with opt_type "{o.opt_type}"' populate_simple_opt_with_data(o, data, data_type) else: # Recursion for nested ComplexOptCheck objects populate_opt_with_data(o, data, data_type) -def populate_with_data(checklist, data, data_type): +def populate_with_data(checklist: List[ChecklistObjType], data: DictOrTuple, data_type: str) -> None: for opt in checklist: populate_opt_with_data(opt, data, data_type) -def override_expected_value(checklist, name, new_val): +def override_expected_value(checklist: List[ChecklistObjType], name: str, new_val: str) -> None: for opt in checklist: if opt.name == name: - assert(opt.opt_type in ('kconfig', 'cmdline', 'sysctl')), \ - f'overriding an expected value for "{opt.opt_type}" checks is not supported yet' + assert(isinstance(opt, SimpleNamedOptCheckTypes)), \ + f'overriding an expected value for {opt}" is not supported yet' opt.expected = new_val -def perform_checks(checklist): +def perform_checks(checklist: List[ChecklistObjType]) -> None: for opt in checklist: opt.check() + + +def print_unknown_options(checklist: List[ChecklistObjType], parsed_options: Dict[str, str], opt_type: str) -> None: + known_options = [] + + for o1 in checklist: + if isinstance(o1, SimpleOptCheckTypes): + assert(o1.opt_type != 'complex'), f'{o1} with complex opt_type' + assert(not isinstance(o1, VersionCheck)), 'single VersionCheck in checklist' + known_options.append(o1.name) + continue + for o2 in o1.opts: + if isinstance(o2, SimpleOptCheckTypes): + assert(o2.opt_type != 'complex'), f'{o2} with complex opt_type' + if hasattr(o2, 'name'): + known_options.append(o2.name) + continue + for o3 in o2.opts: + assert(isinstance(o3, SimpleOptCheckTypes)), \ + f'unexpected ComplexOptCheck inside {o2.name}' + assert(o3.opt_type != 'complex'), f'{o3} with complex opt_type' + if hasattr(o3, 'name'): + known_options.append(o3.name) + + for option, value in parsed_options.items(): + if option not in known_options: + print(f'[?] No check for {opt_type} option {option} ({value})') diff --git a/kernel_hardening_checker/test_engine.py b/kernel_hardening_checker/test_engine.py index 2901449..05e640c 100644 --- a/kernel_hardening_checker/test_engine.py +++ b/kernel_hardening_checker/test_engine.py @@ -13,10 +13,14 @@ This module performs unit-testing of the kernel-hardening-checker engine. import unittest import io import sys -from collections import OrderedDict import json import inspect -from .engine import KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND, populate_with_data, perform_checks, override_expected_value +from typing import Union, Optional, List, Dict, Tuple +from .engine import StrOrBool, ChecklistObjType, KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND +from .engine import populate_with_data, perform_checks, override_expected_value + + +ResultType = List[Union[Dict[str, StrOrBool], str]] class TestEngine(unittest.TestCase): @@ -24,31 +28,31 @@ class TestEngine(unittest.TestCase): Example test scenario: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [KconfigCheck('reason_1', 'decision_1', 'KCONFIG_NAME', 'expected_1')] config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'cmdline_name', 'expected_2')] config_checklist += [SysctlCheck('reason_3', 'decision_3', 'sysctl_name', 'expected_3')] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_KCONFIG_NAME'] = 'UNexpected_1' # 3. prepare the parsed cmdline options - parsed_cmdline_options = OrderedDict() + parsed_cmdline_options = {} parsed_cmdline_options['cmdline_name'] = 'expected_2' # 4. prepare the parsed sysctl options - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} parsed_sysctl_options['sysctl_name'] = 'expected_3' # 5. prepare the kernel version - kernel_version = (42, 43) + kernel_version = (42, 43, 44) # 6. run the engine self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, kernel_version) # 7. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual(... """ @@ -56,7 +60,11 @@ class TestEngine(unittest.TestCase): maxDiff = None @staticmethod - def run_engine(checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, kernel_version): + def run_engine(checklist: List[ChecklistObjType], + parsed_kconfig_options: Optional[Dict[str, str]], + parsed_cmdline_options: Optional[Dict[str, str]], + parsed_sysctl_options: Optional[Dict[str, str]], + kernel_version: Optional[Tuple[int, int, int]]) -> None: # populate the checklist with data if parsed_kconfig_options: populate_with_data(checklist, parsed_kconfig_options, 'kconfig') @@ -86,7 +94,7 @@ class TestEngine(unittest.TestCase): print() @staticmethod - def get_engine_result(checklist, result, result_type): + def get_engine_result(checklist: List[ChecklistObjType], result: ResultType, result_type: str) -> None: assert(result_type in ('json', 'stdout', 'stdout_verbose')), \ f'invalid result type "{result_type}"' @@ -106,9 +114,9 @@ class TestEngine(unittest.TestCase): sys.stdout = stdout_backup result.append(captured_output.getvalue()) - def test_simple_kconfig(self): + def test_simple_kconfig(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')] config_checklist += [KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2')] config_checklist += [KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')] @@ -121,7 +129,7 @@ class TestEngine(unittest.TestCase): config_checklist += [KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is not off')] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1' parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2' parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5' @@ -133,7 +141,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, parsed_kconfig_options, None, None, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -149,9 +157,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}] ) - def test_simple_cmdline(self): + def test_simple_cmdline(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [CmdlineCheck('reason_1', 'decision_1', 'name_1', 'expected_1')] config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')] config_checklist += [CmdlineCheck('reason_3', 'decision_3', 'name_3', 'expected_3')] @@ -164,7 +172,7 @@ class TestEngine(unittest.TestCase): config_checklist += [CmdlineCheck('reason_10', 'decision_10', 'name_10', 'is not off')] # 2. prepare the parsed cmdline options - parsed_cmdline_options = OrderedDict() + parsed_cmdline_options = {} parsed_cmdline_options['name_1'] = 'expected_1' parsed_cmdline_options['name_2'] = 'UNexpected_2' parsed_cmdline_options['name_5'] = '' @@ -176,7 +184,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, None, parsed_cmdline_options, None, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -192,9 +200,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'name_10', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}] ) - def test_simple_sysctl(self): + def test_simple_sysctl(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [SysctlCheck('reason_1', 'decision_1', 'name_1', 'expected_1')] config_checklist += [SysctlCheck('reason_2', 'decision_2', 'name_2', 'expected_2')] config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')] @@ -207,7 +215,7 @@ class TestEngine(unittest.TestCase): config_checklist += [SysctlCheck('reason_10', 'decision_10', 'name_10', 'is not off')] # 2. prepare the parsed sysctl options - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} parsed_sysctl_options['name_1'] = 'expected_1' parsed_sysctl_options['name_2'] = 'UNexpected_2' parsed_sysctl_options['name_5'] = '' @@ -219,7 +227,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, None, None, parsed_sysctl_options, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -235,9 +243,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'name_10', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}] ) - def test_complex_or(self): + def test_complex_or(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'), KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))] config_checklist += [OR(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'), @@ -252,7 +260,7 @@ class TestEngine(unittest.TestCase): KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1' parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2' parsed_kconfig_options['CONFIG_NAME_3'] = 'UNexpected_3' @@ -266,7 +274,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, parsed_kconfig_options, None, None, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -278,9 +286,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'OK: CONFIG_NAME_12 is not off', 'check_result_bool': True}] ) - def test_complex_and(self): + def test_complex_and(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'), KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))] config_checklist += [AND(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'), @@ -295,7 +303,7 @@ class TestEngine(unittest.TestCase): KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1' parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2' parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3' @@ -311,7 +319,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, parsed_kconfig_options, None, None, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -323,9 +331,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'FAIL: CONFIG_NAME_12 is off, not found', 'check_result_bool': False}] ) - def test_complex_nested(self): + def test_complex_nested(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'), OR(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'), KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')))] @@ -340,7 +348,7 @@ class TestEngine(unittest.TestCase): KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'expected_12')))] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1' parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2' parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3' @@ -358,7 +366,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, parsed_kconfig_options, None, None, None) # 4. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -368,9 +376,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'expected_10', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: "UNexpected_10"', 'check_result_bool': False}] ) - def test_version(self): + def test_version(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'), VersionCheck((41, 101, 0)))] config_checklist += [AND(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'), @@ -385,7 +393,7 @@ class TestEngine(unittest.TestCase): VersionCheck((42, 43, 45)))] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2' parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4' parsed_kconfig_options['CONFIG_NAME_6'] = 'expected_6' @@ -397,7 +405,7 @@ class TestEngine(unittest.TestCase): self.run_engine(config_checklist, parsed_kconfig_options, None, None, kernel_version) # 5. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, @@ -409,9 +417,9 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_6', 'type': 'kconfig', 'desired_val': 'expected_6', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: version < (42, 43, 45)', 'check_result_bool': False}] ) - def test_stdout(self): + def test_stdout(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'), CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2'), SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3'))] @@ -420,23 +428,23 @@ class TestEngine(unittest.TestCase): SysctlCheck('reason_6', 'decision_6', 'name_6', 'expected_6'))] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'UNexpected_1' # 3. prepare the parsed cmdline options - parsed_cmdline_options = OrderedDict() + parsed_cmdline_options = {} parsed_cmdline_options['name_2'] = 'expected_2' parsed_cmdline_options['name_5'] = 'UNexpected_5' # 4. prepare the parsed sysctl options - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} parsed_sysctl_options['name_6'] = 'expected_6' # 5. run the engine self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None) # 6. check that the results are correct - json_result = [] + json_result = [] # type: ResultType self.get_engine_result(config_checklist, json_result, 'json') self.assertEqual( json_result, @@ -444,7 +452,7 @@ class TestEngine(unittest.TestCase): {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: name_5 is not "expected_5"', 'check_result_bool': False}] ) - stdout_result = [] + stdout_result = [] # type: ResultType self.get_engine_result(config_checklist, stdout_result, 'stdout') self.assertEqual( stdout_result, @@ -474,30 +482,30 @@ name_6 |sysctl | expected_6 |decision_6| re ' ] ) - def test_value_overriding(self): + def test_value_overriding(self) -> None: # 1. prepare the checklist - config_checklist = [] + config_checklist = [] # type: List[ChecklistObjType] config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')] config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')] config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')] # 2. prepare the parsed kconfig options - parsed_kconfig_options = OrderedDict() + parsed_kconfig_options = {} parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1_new' # 3. prepare the parsed cmdline options - parsed_cmdline_options = OrderedDict() + parsed_cmdline_options = {} parsed_cmdline_options['name_2'] = 'expected_2_new' # 4. prepare the parsed sysctl options - parsed_sysctl_options = OrderedDict() + parsed_sysctl_options = {} parsed_sysctl_options['name_3'] = 'expected_3_new' # 5. run the engine self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None) # 6. check that the results are correct - result = [] + result = [] # type: ResultType self.get_engine_result(config_checklist, result, 'json') self.assertEqual( result, diff --git a/setup.cfg b/setup.cfg index 953b045..bfe5105 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] name = kernel-hardening-checker +version = attr: kernel_hardening_checker.__version__ author = Alexander Popov author_email = alex.popov@linux.com home_page = https://github.com/a13xp0p0v/kernel-hardening-checker diff --git a/setup.py b/setup.py index 853fcae..127bfae 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -from setuptools import setup +""" +This tool is for checking the security hardening options of the Linux kernel. + +Author: Alexander Popov -about = {} -with open('kernel_hardening_checker/__about__.py') as f: - exec(f.read(), about) +This module performs installing of the kernel-hardening-checker package. +""" -print('v: "{}"'.format(about['__version__'])) +from setuptools import setup # See the options in setup.cfg -setup(version = about['__version__']) +setup()