Merge branch 'master' into typing
[kconfig-hardened-check.git] / kernel_hardening_checker / __init__.py
index 597a964eea63a3a2a01df794e6e65b25504a0905..f4b7525af266163f12b85be854d5ed843165b7de 100644 (file)
@@ -14,6 +14,7 @@ import gzip
 import sys
 from argparse import ArgumentParser
 from collections import OrderedDict
+from typing import List, Tuple
 import re
 import json
 from .__about__ import __version__
@@ -23,13 +24,13 @@ from .engine import populate_with_data, perform_checks, override_expected_value
 
 def _open(file: str, *args, **kwargs):
     open_method = open
-    if file.endswith(".gz"):
+    if file.endswith('.gz'):
         open_method = gzip.open
 
     return open_method(file, *args, **kwargs)
 
 
-def detect_arch(fname, archs):
+def detect_arch(fname: str, archs: List[str]) -> Tuple:
     with _open(fname, 'rt', encoding='utf-8') as f:
         arch_pattern = re.compile(r"CONFIG_[a-zA-Z0-9_]+=y$")
         arch = None
@@ -46,23 +47,24 @@ def detect_arch(fname, archs):
         return arch, 'OK'
 
 
-def detect_kernel_version(fname):
+def detect_kernel_version(fname: str) -> Tuple:
     with _open(fname, 'rt', encoding='utf-8') as f:
         ver_pattern = re.compile(r"^# Linux/.+ Kernel Configuration$|^Linux version .+")
         for line in f.readlines():
             if ver_pattern.match(line):
                 line = line.strip()
                 parts = line.split()
-                ver_str = parts[2]
+                ver_str = parts[2].split('-', 1)[0]
                 ver_numbers = ver_str.split('.')
-                if len(ver_numbers) < 3 or not ver_numbers[0].isdigit() or not ver_numbers[1].isdigit():
-                    msg = f'failed to parse the version "{ver_str}"'
-                    return None, msg
-                return (int(ver_numbers[0]), int(ver_numbers[1])), None
+                if len(ver_numbers) >= 3:
+                    if all(map(lambda x: x.isdigit(), ver_numbers)):
+                        return tuple(map(int, ver_numbers)), None
+                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):
     gcc_version = None
     clang_version = None
     with _open(fname, 'rt', encoding='utf-8') as f:
@@ -74,9 +76,9 @@ def detect_compiler(fname):
     if gcc_version is None or clang_version is None:
         return None, 'no CONFIG_GCC_VERSION or CONFIG_CLANG_VERSION'
     if gcc_version == '0' and clang_version != '0':
-        return 'CLANG ' + clang_version, 'OK'
+        return f'CLANG {clang_version}', 'OK'
     if gcc_version != '0' and clang_version == '0':
-        return 'GCC ' + gcc_version, 'OK'
+        return f'GCC {gcc_version}', 'OK'
     sys.exit(f'[!] ERROR: invalid GCC_VERSION and CLANG_VERSION: {gcc_version} {clang_version}')
 
 
@@ -84,16 +86,16 @@ def print_unknown_options(checklist, parsed_options, opt_type):
     known_options = []
 
     for o1 in checklist:
-        if o1.type != 'complex':
+        if o1.opt_type != 'complex':
             known_options.append(o1.name)
             continue
         for o2 in o1.opts:
-            if o2.type != 'complex':
+            if o2.opt_type != 'complex':
                 if hasattr(o2, 'name'):
                     known_options.append(o2.name)
                 continue
             for o3 in o2.opts:
-                assert(o3.type != 'complex'), \
+                assert(o3.opt_type != 'complex'), \
                        f'unexpected ComplexOptCheck inside {o2.name}'
                 if hasattr(o3, 'name'):
                     known_options.append(o3.name)
@@ -103,7 +105,7 @@ def print_unknown_options(checklist, parsed_options, opt_type):
             print(f'[?] No check for {opt_type} option {option} ({value})')
 
 
-def print_checklist(mode, checklist, with_results):
+def print_checklist(mode: str, checklist, with_results: bool):
     if mode == 'json':
         output = []
         for opt in checklist:
@@ -116,9 +118,9 @@ def print_checklist(mode, checklist, with_results):
     if with_results:
         sep_line_len += 30
     print('=' * sep_line_len)
-    print(f'{"option name":^40}|{"type":^7}|{"desired val":^12}|{"decision":^10}|{"reason":^18}', end='')
+    print(f'{"option_name":^40}|{"type":^7}|{"desired_val":^12}|{"decision":^10}|{"reason":^18}', end='')
     if with_results:
-        print('| check result', end='')
+        print('| check_result', end='')
     print()
     print('=' * sep_line_len)
 
@@ -150,7 +152,7 @@ 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):
+def parse_kconfig_file(_mode, parsed_options, fname: str):
     with _open(fname, 'rt', encoding='utf-8') 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$")
@@ -218,8 +220,8 @@ def parse_sysctl_file(mode, parsed_options, fname):
         sys.exit(f'[!] ERROR: {fname} doesn\'t look like a sysctl output file, please try `sudo sysctl -a > {fname}`')
 
     # let's check the presence of a sysctl option available for root
-    if 'net.core.bpf_jit_harden' not in parsed_options and mode != 'json':
-        print(f'[!] WARNING: sysctl option "net.core.bpf_jit_harden" available for root is not found in {fname}, please try `sudo sysctl -a > {fname}`')
+    if 'kernel.cad_pid' not in parsed_options and mode != 'json':
+        print(f'[!] WARNING: sysctl option "kernel.cad_pid" available for root is not found in {fname}, please try `sudo sysctl -a > {fname}`')
 
 
 def main():
@@ -232,7 +234,7 @@ def main():
     supported_archs = ['X86_64', 'X86_32', 'ARM64', 'ARM']
     parser = ArgumentParser(prog='kernel-hardening-checker',
                             description='A tool for checking the security hardening options of the Linux kernel')
-    parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
+    parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
     parser.add_argument('-m', '--mode', choices=report_modes,
                         help='choose the report mode')
     parser.add_argument('-c', '--config',
@@ -285,7 +287,7 @@ def main():
                 print('[!] Hint: provide the kernel version file through --kernel-version option')
             sys.exit(f'[!] ERROR: {msg}')
         if mode != 'json':
-            print(f'[+] Detected kernel version: {kernel_version[0]}.{kernel_version[1]}')
+            print(f'[+] Detected kernel version: {kernel_version}')
 
         compiler, msg = detect_compiler(args.config)
         if mode != 'json':
@@ -331,7 +333,8 @@ def main():
             override_expected_value(config_checklist, 'CONFIG_ARCH_MMAP_RND_BITS', mmap_rnd_bits_max)
         else:
             # remove the CONFIG_ARCH_MMAP_RND_BITS check to avoid false results
-            print('[-] Can\'t check CONFIG_ARCH_MMAP_RND_BITS without CONFIG_ARCH_MMAP_RND_BITS_MAX')
+            if mode != 'json':
+                print('[-] Can\'t check CONFIG_ARCH_MMAP_RND_BITS without CONFIG_ARCH_MMAP_RND_BITS_MAX')
             config_checklist[:] = [o for o in config_checklist if o.name != 'CONFIG_ARCH_MMAP_RND_BITS']
 
         # now everything is ready, perform the checks