From: Julien Voisin Date: Sun, 19 May 2024 19:17:19 +0000 (+0000) Subject: Merge branch 'master' into scs_pac X-Git-Tag: v0.6.10~57^2 X-Git-Url: https://jxself.org/git/?a=commitdiff_plain;h=f6075e933d9ec15d4b0bc216f764acbdfb51235d;hp=3ccd5268b84cd2e27c0e56b4903439d8adbdaa2c;p=kconfig-hardened-check.git Merge branch 'master' into scs_pac --- 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 161fc94..91742c3 100644 --- a/kernel_hardening_checker/__init__.py +++ b/kernel_hardening_checker/__init__.py @@ -8,29 +8,31 @@ 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 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: + if file.endswith('.gz'): + return gzip.open(file, 'rt', encoding='utf-8') + return open(file, 'rt', encoding='utf-8') -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 +48,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 +58,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 +83,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,14 +103,21 @@ 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'): + elif opt.result.startswith('FAIL'): + fail_count += 1 + if mode == 'show_ok': continue + else: + assert(False), f'unexpected result "{opt.result}" of {opt.name} check' opt.table_print(mode, with_results) print() if mode == 'verbose': @@ -140,9 +126,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 +135,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,10 +160,11 @@ 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: with open(fname, 'r', encoding='utf-8') as f: line = f.readline() opts = line.split() @@ -197,10 +182,11 @@ 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: with open(fname, 'r', encoding='utf-8') as f: sysctl_pattern = re.compile(r"[a-zA-Z0-9/\._-]+ =.*$") for line in f.readlines(): @@ -223,7 +209,7 @@ def parse_sysctl_file(mode, parsed_options, fname): 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 +242,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 +293,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 +302,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 +353,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 +375,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 +385,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 e0caab6..672ea7e 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: @@ -423,7 +424,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: @@ -631,7 +632,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: @@ -647,7 +648,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 @@ -658,7 +659,7 @@ 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: 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()