Fix 'decision' priority order ('lockdown' vs 'clipos' vs 'grapheneos')
[kconfig-hardened-check.git] / kconfig_hardened_check / __init__.py
index cb794505bd7a597411d3356472925c0125215967..5db3879e4f51d503972fe9d9e487963625fcaed1 100644 (file)
@@ -178,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:
@@ -200,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))):
@@ -211,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
@@ -353,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')]
@@ -429,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')]
@@ -447,18 +449,16 @@ 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')]
     l += [OptCheck('cut_attack_surface', 'maintainer', 'FB', 'is not set')]
     l += [OptCheck('cut_attack_surface', 'maintainer', 'VT', 'is not set')]
 
-    # '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
+    # 'cut_attack_surface', 'grapheneos'
+    l += [OptCheck('cut_attack_surface', 'grapheneos', 'AIO', 'is not set')]
 
     # 'cut_attack_surface', 'clipos'
     l += [OptCheck('cut_attack_surface', 'clipos', 'STAGING', 'is not set')]
@@ -471,13 +471,18 @@ 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'):
         l += [OptCheck('cut_attack_surface', 'clipos', 'X86_INTEL_TSX_MODE_OFF', 'y')] # tsx=off
 
-    # 'cut_attack_surface', 'grapheneos'
-    l += [OptCheck('cut_attack_surface', 'grapheneos', 'AIO', 'is not set')]
+    # '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', '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
 
     # 'cut_attack_surface', 'my'
     l += [OptCheck('cut_attack_surface', 'my', 'MMIOTRACE', 'is not set')] # refers to LOCKDOWN (permissive)
@@ -490,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'):
@@ -539,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':
@@ -547,10 +562,16 @@ 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, kernel_version):
@@ -602,7 +623,7 @@ def main():
     #     - 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']
+    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')
@@ -610,7 +631,7 @@ def main():
     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()
@@ -651,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':