on:
push:
branches: [ master ]
- pull_request:
- branches: [ master ]
jobs:
engine_unit-test:
+ if: github.repository == 'a13xp0p0v/kernel-hardening-checker'
+
runs-on: ubuntu-latest
strategy:
--- /dev/null
+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
on:
push:
branches: [ master ]
- pull_request:
- branches: [ master ]
jobs:
functional_test:
+ if: github.repository == 'a13xp0p0v/kernel-hardening-checker'
+
runs-on: ubuntu-latest
strategy:
--- /dev/null
+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
--- /dev/null
+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
- 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
+++ /dev/null
-"""
-Version
-"""
-
-__version__ = '0.6.6'
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():
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):
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]
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:
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':
# 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)'
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$")
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()
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():
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
if mode != 'json':
print(f'[+] Special report mode: {mode}')
- config_checklist = []
+ config_checklist = [] # type: List[ChecklistObjType]
if args.config:
if args.print:
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')
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')
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')
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)
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:
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:
efi_not_set = KconfigCheck('-', '-', 'EFI', 'is not set')
cc_is_gcc = KconfigCheck('-', '-', 'CC_IS_GCC', 'y') # exists since v4.18
cc_is_clang = KconfigCheck('-', '-', 'CC_IS_CLANG', 'y') # exists since v4.18
+ if arch in ('X86_64', 'X86_32'):
+ cpu_sup_amd_not_set = KconfigCheck('-', '-', 'CPU_SUP_AMD', 'is not set')
+ cpu_sup_intel_not_set = KconfigCheck('-', '-', 'CPU_SUP_INTEL', 'is not set')
modules_not_set = KconfigCheck('cut_attack_surface', 'kspp', 'MODULES', 'is not set') # radical, but may be useful in some cases
devmem_not_set = KconfigCheck('cut_attack_surface', 'kspp', 'DEVMEM', 'is not set') # refers to LOCKDOWN
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', 'X86_MCE_INTEL', 'y')]
- l += [KconfigCheck('self_protection', 'defconfig', 'X86_MCE_AMD', '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
l += [OR(KconfigCheck('self_protection', 'defconfig', 'MICROCODE_INTEL', 'y'),
+ cpu_sup_intel_not_set,
AND(microcode_is_set,
VersionCheck((6, 6, 0))))] # MICROCODE_INTEL was included in MICROCODE since v6.6
l += [OR(KconfigCheck('self_protection', 'defconfig', 'MICROCODE_AMD', 'y'),
+ cpu_sup_amd_not_set,
AND(microcode_is_set,
VersionCheck((6, 6, 0))))] # MICROCODE_AMD was included in MICROCODE since v6.6
l += [OR(KconfigCheck('self_protection', 'defconfig', 'X86_SMAP', 'y'),
VersionCheck((5, 19, 0)))] # X86_SMAP is enabled by default since v5.19
l += [OR(KconfigCheck('self_protection', 'defconfig', 'X86_UMIP', 'y'),
KconfigCheck('self_protection', 'defconfig', 'X86_INTEL_UMIP', 'y'))]
+ l += [OR(KconfigCheck('self_protection', 'defconfig', 'X86_MCE_INTEL', 'y'),
+ cpu_sup_intel_not_set)]
+ l += [OR(KconfigCheck('self_protection', 'defconfig', 'X86_MCE_AMD', 'y'),
+ cpu_sup_amd_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', 'PAGE_TABLE_ISOLATION', 'y')]
l += [KconfigCheck('self_protection', 'defconfig', 'RANDOMIZE_MEMORY', 'y')]
l += [KconfigCheck('self_protection', 'defconfig', 'X86_KERNEL_IBT', 'y')]
- l += [KconfigCheck('self_protection', 'defconfig', 'CPU_SRSO', 'y')]
+ l += [OR(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)]
l += [AND(KconfigCheck('self_protection', 'defconfig', 'AMD_IOMMU', 'y'),
l += [OR(KconfigCheck('cut_attack_surface', 'defconfig', 'STRICT_DEVMEM', 'y'),
devmem_not_set)] # refers to LOCKDOWN
if arch in ('X86_64', 'X86_32'):
- l += [KconfigCheck('cut_attack_surface', 'defconfig', 'X86_INTEL_TSX_MODE_OFF', 'y')] # tsx=off
+ l += [OR(KconfigCheck('cut_attack_surface', 'defconfig', 'X86_INTEL_TSX_MODE_OFF', 'y'),
+ cpu_sup_intel_not_set)] # tsx=off
# 'cut_attack_surface', 'kspp'
l += [KconfigCheck('cut_attack_surface', 'kspp', 'SECURITY_DMESG_RESTRICT', 'y')]
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:
# 'cut_attack_surface', 'defconfig'
if arch in ('X86_64', 'X86_32'):
+ tsx_not_set = CmdlineCheck('cut_attack_surface', 'defconfig', 'tsx', 'is not set')
l += [OR(CmdlineCheck('cut_attack_surface', 'defconfig', 'tsx', 'off'),
AND(KconfigCheck('cut_attack_surface', 'defconfig', 'X86_INTEL_TSX_MODE_OFF', 'y'),
- CmdlineCheck('cut_attack_surface', 'defconfig', 'tsx', 'is not set')))]
+ tsx_not_set),
+ AND(KconfigCheck('-', '-', 'CPU_SUP_INTEL', 'is not set'),
+ tsx_not_set))]
# 'cut_attack_surface', 'kspp'
l += [CmdlineCheck('cut_attack_surface', 'kspp', 'nosmt', 'is present')] # slow (high performance penalty)
]
-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:
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
# 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')]
"""
# 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'):
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:
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:
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
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:
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='')
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
# Use cases:
# OR(<X_is_hardened>, <X_is_disabled>)
# OR(<X_is_hardened>, <old_X_is_hardened>)
- 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
# AND(<suboption>, <main_option>)
# Suboption is not checked if checking of the main_option is failed.
# AND(<X_is_disabled>, <old_X_is_disabled>)
- 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
# 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})')
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):
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(...
"""
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')
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}"'
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')]
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'
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,
{'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')]
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'] = ''
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,
{'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')]
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'] = ''
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,
{'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'),
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'
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,
{'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'),
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'
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,
{'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')))]
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'
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,
{'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'),
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'
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,
{'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'))]
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,
{'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,
' ]
)
- 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,
[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
#!/usr/bin/env python3
-from setuptools import setup
+"""
+This tool is for checking the security hardening options of the Linux kernel.
+
+Author: Alexander Popov <alex.popov@linux.com>
-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()