Add CLIP OS recommendations about CONFIG_IO_URING and CONFIG_X86_IOPL_IOPERM
[kconfig-hardened-check.git] / kconfig_hardened_check / __init__.py
index 548774526fc88ff78ccb19f8b89037a6f3684cce..986750919d1f0b6738088e4370f0e4067abd9ef8 100644 (file)
@@ -64,17 +64,6 @@ from .__about__ import __version__
 # pylint: disable=line-too-long,bad-whitespace,too-many-branches
 # pylint: disable=too-many-statements,global-statement
 
-# Report modes:
-#   * verbose mode for
-#     - reporting about unknown kernel options in the config
-#     - verbose printing of ComplexOptCheck items
-#   * json mode for printing the results in JSON format
-report_modes = ['verbose', 'json']
-
-supported_archs = ['X86_64', 'X86_32', 'ARM64', 'ARM']
-
-kernel_version = None
-
 
 class OptCheck:
     def __init__(self, reason, decision, name, expected):
@@ -109,16 +98,17 @@ class OptCheck:
 class VerCheck:
     def __init__(self, ver_expected):
         self.ver_expected = ver_expected
+        self.ver = None
         self.result = None
 
     def check(self):
-        if kernel_version[0] > self.ver_expected[0]:
+        if self.ver[0] > self.ver_expected[0]:
             self.result = 'OK: version >= ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
             return True
-        if kernel_version[0] < self.ver_expected[0]:
+        if self.ver[0] < self.ver_expected[0]:
             self.result = 'FAIL: version < ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
             return False
-        if kernel_version[1] >= self.ver_expected[1]:
+        if self.ver[1] >= self.ver_expected[1]:
             self.result = 'OK: version >= ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
             return True
         self.result = 'FAIL: version < ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
@@ -188,9 +178,9 @@ class ComplexOptCheck:
 
 class OR(ComplexOptCheck):
     # self.opts[0] is the option that this OR-check is about.
-    # Use case:
+    # Use cases:
     #     OR(<X_is_hardened>, <X_is_disabled>)
-    #     OR(<X_is_hardened>, <X_is_hardened_old>)
+    #     OR(<X_is_hardened>, <old_X_is_hardened>)
 
     def check(self):
         if not self.opts:
@@ -210,8 +200,10 @@ class OR(ComplexOptCheck):
 
 class AND(ComplexOptCheck):
     # self.opts[0] is the option that this AND-check is about.
-    # Use case: AND(<suboption>, <main_option>)
-    # Suboption is not checked if checking of the main_option is failed.
+    # Use cases:
+    #     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):
         for i, opt in reversed(list(enumerate(self.opts))):
@@ -221,7 +213,7 @@ class AND(ComplexOptCheck):
                 return ret
             if not ret:
                 if hasattr(opt, 'expected'):
-                    self.result = 'FAIL: CONFIG_{} is needed'.format(opt.name)
+                    self.result = 'FAIL: CONFIG_{} not "{}"'.format(opt.name, opt.expected)
                 else:
                     self.result = opt.result
                 return False
@@ -229,14 +221,14 @@ class AND(ComplexOptCheck):
         sys.exit('[!] ERROR: invalid AND check')
 
 
-def detect_arch(fname):
+def detect_arch(fname, archs):
     with open(fname, 'r') as f:
         arch_pattern = re.compile("CONFIG_[a-zA-Z0-9_]*=y")
         arch = None
         for line in f.readlines():
             if arch_pattern.match(line):
                 option, _ = line[7:].split('=', 1)
-                if option in supported_archs:
+                if option in archs:
                     if not arch:
                         arch = option
                     else:
@@ -363,6 +355,7 @@ def construct_checklist(l, arch):
     l += [OptCheck('self_protection', 'clipos', 'SECURITY_DMESG_RESTRICT', 'y')]
     l += [OptCheck('self_protection', 'clipos', 'DEBUG_VIRTUAL', 'y')]
     l += [OptCheck('self_protection', 'clipos', 'STATIC_USERMODEHELPER', 'y')] # needs userspace support
+    l += [OptCheck('self_protection', 'clipos', 'EFI_DISABLE_PCI_DMA', 'y')]
     l += [OptCheck('self_protection', 'clipos', 'SLAB_MERGE_DEFAULT', 'is not set')] # slab_nomerge
     l += [OptCheck('self_protection', 'clipos', 'RANDOM_TRUST_BOOTLOADER', 'is not set')]
     l += [OptCheck('self_protection', 'clipos', 'RANDOM_TRUST_CPU', 'is not set')]
@@ -439,7 +432,6 @@ def construct_checklist(l, arch):
         l += [OptCheck('cut_attack_surface', 'kspp', 'LEGACY_VSYSCALL_NONE', 'y')] # 'vsyscall=none'
 
     # 'cut_attack_surface', 'grsecurity'
-    l += [OptCheck('cut_attack_surface', 'grsecurity', 'X86_PTDUMP', 'is not set')]
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'ZSMALLOC_STAT', 'is not set')]
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'PAGE_OWNER', 'is not set')]
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'DEBUG_KMEMLEAK', 'is not set')]
@@ -457,6 +449,8 @@ def construct_checklist(l, arch):
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'DEVPORT', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'DEBUG_FS', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'grsecurity', 'NOTIFIER_ERROR_INJECTION','is not set')]
+    l += [AND(OptCheck('cut_attack_surface', 'grsecurity', 'X86_PTDUMP', 'is not set'),
+              OptCheck('cut_attack_surface', 'my', 'PTDUMP_DEBUGFS', 'is not set'))]
 
     # 'cut_attack_surface', 'maintainer'
     l += [OptCheck('cut_attack_surface', 'maintainer', 'DRM_LEGACY', 'is not set')]
@@ -465,7 +459,6 @@ def construct_checklist(l, arch):
 
     # 'cut_attack_surface', 'lockdown'
     l += [OptCheck('cut_attack_surface', 'lockdown', 'ACPI_TABLE_UPGRADE', 'is not set')] # refers to LOCKDOWN
-    l += [OptCheck('cut_attack_surface', 'lockdown', 'X86_IOPL_IOPERM', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'lockdown', 'EFI_TEST', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'lockdown', 'BPF_SYSCALL', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'lockdown', 'MMIOTRACE_TEST', 'is not set')] # refers to LOCKDOWN
@@ -481,6 +474,8 @@ def construct_checklist(l, arch):
     l += [OptCheck('cut_attack_surface', 'clipos', 'USER_NS', 'is not set')] # user.max_user_namespaces=0
     l += [OptCheck('cut_attack_surface', 'clipos', 'X86_MSR', 'is not set')] # refers to LOCKDOWN
     l += [OptCheck('cut_attack_surface', 'clipos', 'X86_CPUID', 'is not set')]
+    l += [OptCheck('cut_attack_surface', 'clipos', 'IO_URING', 'is not set')]
+    l += [OptCheck('cut_attack_surface', 'clipos', 'X86_IOPL_IOPERM', 'is not set')] # refers to LOCKDOWN
     l += [AND(OptCheck('cut_attack_surface', 'clipos', 'LDISC_AUTOLOAD', 'is not set'),
               PresenceCheck('LDISC_AUTOLOAD'))]
     if arch in ('X86_64', 'X86_32'):
@@ -500,7 +495,10 @@ def construct_checklist(l, arch):
     l += [OptCheck('cut_attack_surface', 'my', 'INPUT_EVBUG', 'is not set')] # Can be used as a keylogger
 
     # 'userspace_hardening'
-    l += [OptCheck('userspace_hardening', 'defconfig', 'INTEGRITY', 'y')]
+    if arch in ('X86_64', 'ARM64', 'X86_32'):
+        l += [OptCheck('userspace_hardening', 'defconfig', 'INTEGRITY', 'y')]
+    if arch == 'ARM':
+        l += [OptCheck('userspace_hardening', 'my', 'INTEGRITY', 'y')]
     if arch in ('ARM', 'X86_32'):
         l += [OptCheck('userspace_hardening', 'defconfig', 'VMSPLIT_3G', 'y')]
     if arch in ('X86_64', 'ARM64'):
@@ -549,6 +547,13 @@ def print_checklist(mode, checklist, with_results):
 
     # table contents
     for opt in checklist:
+        if with_results:
+            if mode == 'show_ok':
+                if not opt.result.startswith('OK'):
+                    continue
+            if mode == 'show_fail':
+                if not opt.result.startswith('FAIL'):
+                    continue
         opt.table_print(mode, with_results)
         print()
         if mode == 'verbose':
@@ -557,19 +562,27 @@ def print_checklist(mode, checklist, with_results):
 
     # final score
     if with_results:
-        error_count = len(list(filter(lambda opt: opt.result.startswith('FAIL'), checklist)))
+        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)'
+        if mode == 'show_fail':
+            ok_suppressed = ' (suppressed in output)'
         if mode != 'json':
-            print('[+] Config check is finished: \'OK\' - {} / \'FAIL\' - {}'.format(ok_count, error_count))
+            print('[+] Config check is finished: \'OK\' - {}{} / \'FAIL\' - {}{}'.format(ok_count, ok_suppressed, fail_count, fail_suppressed))
 
 
-def perform_checks(checklist, parsed_options):
+def perform_checks(checklist, parsed_options, kernel_version):
     for opt in checklist:
         if hasattr(opt, 'opts'):
             # prepare ComplexOptCheck
             for o in opt.opts:
                 if hasattr(o, 'state'):
                     o.state = parsed_options.get(o.name, None)
+                if hasattr(o, 'ver'):
+                    o.ver = kernel_version
         else:
             # prepare simple check
             if not hasattr(opt, 'state'):
@@ -605,33 +618,37 @@ def parse_config_file(parsed_options, fname):
 
 
 def main():
-    global kernel_version
-
-    mode = None
-    config_checklist = []
-    parsed_options = OrderedDict()
-
+    # Report modes:
+    #   * verbose mode for
+    #     - reporting about unknown kernel options in the config
+    #     - verbose printing of ComplexOptCheck items
+    #   * json mode for printing the results in JSON format
+    report_modes = ['verbose', 'json', 'show_ok', 'show_fail']
+    supported_archs = ['X86_64', 'X86_32', 'ARM64', 'ARM']
     parser = ArgumentParser(prog='kconfig-hardened-check',
                             description='Checks the hardening options in the Linux kernel config')
     parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
     parser.add_argument('-p', '--print', choices=supported_archs,
                         help='print hardening preferences for selected architecture')
     parser.add_argument('-c', '--config',
-                        help='check the config_file against these preferences')
+                        help='check the kernel config file against these preferences')
     parser.add_argument('-m', '--mode', choices=report_modes,
                         help='choose the report mode')
     args = parser.parse_args()
 
+    mode = None
     if args.mode:
         mode = args.mode
         if mode != 'json':
             print("[+] Special report mode: {}".format(mode))
 
+    config_checklist = []
+
     if args.config:
         if mode != 'json':
             print('[+] Config file to check: {}'.format(args.config))
 
-        arch, msg = detect_arch(args.config)
+        arch, msg = detect_arch(args.config, supported_archs)
         if not arch:
             sys.exit('[!] ERROR: {}'.format(msg))
         if mode != 'json':
@@ -644,8 +661,9 @@ def main():
             print('[+] Detected kernel version: {}.{}'.format(kernel_version[0], kernel_version[1]))
 
         construct_checklist(config_checklist, arch)
+        parsed_options = OrderedDict()
         parse_config_file(parsed_options, args.config)
-        perform_checks(config_checklist, parsed_options)
+        perform_checks(config_checklist, parsed_options, kernel_version)
 
         if mode == 'verbose':
             print_unknown_options(config_checklist, parsed_options)
@@ -654,6 +672,8 @@ def main():
         sys.exit(0)
 
     if args.print:
+        if mode in ('show_ok', 'show_fail'):
+            sys.exit('[!] ERROR: please use "{}" mode for checking the kernel config'.format(mode))
         arch = args.print
         construct_checklist(config_checklist, arch)
         if mode != 'json':