Add SECURITY_LOADPIN_ENFORCE check
[kconfig-hardened-check.git] / kconfig-hardened-check.py
index 4f01f5793468a1d4dd440117b9552d3baef9c171..e2d84838ddd66ec876aa84cd9ce2ccf6d0c08cf7 100755 (executable)
@@ -20,6 +20,7 @@
 #    page_poison=1 (if enabled)
 #    init_on_alloc=1
 #    init_on_free=1
+#    loadpin.enforce=1
 #
 #    Mitigations of CPU vulnerabilities:
 #       Аrch-independent:
@@ -53,8 +54,14 @@ from collections import OrderedDict
 import re
 import json
 
-debug_mode = False  # set it to True to print the unknown options from the config
-json_mode = False   # if True, print results in JSON format
+# debug_mode enables:
+#    - reporting about unknown kernel options in the config,
+#    - showing all checks from all supported platforms,
+#    - verbose printing of ComplexOptChecks (OR, AND).
+debug_mode = False
+
+# json_mode is for printing results in JSON format
+json_mode = False
 
 supported_archs = [ 'X86_64', 'X86_32', 'ARM64', 'ARM' ]
 config_checklist = []
@@ -320,6 +327,7 @@ def construct_checklist(checklist, arch):
     checklist.append(OptCheck('SLAB_MERGE_DEFAULT',                    'is not set', 'clipos', 'self_protection')) # slab_nomerge
     checklist.append(AND(OptCheck('GCC_PLUGIN_RANDSTRUCT_PERFORMANCE', 'is not set', 'clipos', 'self_protection'), \
                          randstruct_is_set))
+    checklist.append(OptCheck('CONFIG_RANDOM_TRUST_BOOTLOADER',        'is not set', 'clipos', 'self_protection'))
     if debug_mode or arch == 'X86_64' or arch == 'X86_32':
         checklist.append(OptCheck('RANDOM_TRUST_CPU',                      'is not set', 'clipos', 'self_protection'))
         checklist.append(AND(OptCheck('INTEL_IOMMU_SVM',                   'y', 'clipos', 'self_protection'), \
@@ -340,7 +348,10 @@ def construct_checklist(checklist, arch):
     if debug_mode or arch == 'ARM':
         checklist.append(OptCheck('SECURITY',                               'y', 'kspp', 'security_policy')) # and choose your favourite LSM
     checklist.append(OptCheck('SECURITY_YAMA',                          'y', 'kspp', 'security_policy'))
-    checklist.append(OptCheck('SECURITY_LOADPIN',                       'y', 'my', 'security_policy')) # needs userspace support
+    loadpin_is_set = OptCheck('SECURITY_LOADPIN',                       'y', 'my', 'security_policy') # needs userspace support
+    checklist.append(loadpin_is_set)
+    checklist.append(AND(OptCheck('SECURITY_LOADPIN_ENFORCE',           'y', 'my', 'security_policy'), \
+                         loadpin_is_set))
     checklist.append(OptCheck('SECURITY_LOCKDOWN_LSM',                  'y', 'my', 'security_policy'))
     checklist.append(OptCheck('SECURITY_LOCKDOWN_LSM_EARLY',            'y', 'my', 'security_policy'))
     checklist.append(OptCheck('LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY', 'y', 'my', 'security_policy'))
@@ -403,6 +414,7 @@ def construct_checklist(checklist, arch):
     checklist.append(OptCheck('BPF_SYSCALL',          'is not set', 'lockdown', 'cut_attack_surface')) # refers to LOCK_DOWN_KERNEL
     checklist.append(OptCheck('MMIOTRACE_TEST',       'is not set', 'lockdown', 'cut_attack_surface')) # refers to LOCK_DOWN_KERNEL
 
+    checklist.append(OptCheck('STAGING',                  'is not set', 'clipos', 'cut_attack_surface'))
     checklist.append(OptCheck('KSM',                      'is not set', 'clipos', 'cut_attack_surface')) # to prevent FLUSH+RELOAD attack
 #   checklist.append(OptCheck('IKCONFIG',                 'is not set', 'clipos', 'cut_attack_surface')) # no, this info is needed for this check :)
     checklist.append(OptCheck('KALLSYMS',                 'is not set', 'clipos', 'cut_attack_surface'))
@@ -433,6 +445,13 @@ def construct_checklist(checklist, arch):
 #   checklist.append(OptCheck('LKDTM',    'm', 'my', 'feature_test'))
 
 
+def print_opt(opt, with_results):
+    print('CONFIG_{:<38}|{:^13}|{:^10}|{:^20}'.format(opt.name, opt.expected, opt.decision, opt.reason), end='')
+    if with_results:
+        print('|   {}'.format(opt.result), end='')
+    print()
+
+
 def print_checklist(checklist, with_results):
     if json_mode:
         opts = []
@@ -444,21 +463,37 @@ def print_checklist(checklist, with_results):
         print(json.dumps(opts))
         return
 
-    # header
-    print('{:^45}|{:^13}|{:^10}|{:^20}'.format('option name', 'desired val', 'decision', 'reason'), end='')
+    # table header
     sep_line_len = 91
     if with_results:
-        print('|   {}'.format('check result'), end='')
         sep_line_len += 30
+    print('=' * sep_line_len)
+    print('{:^45}|{:^13}|{:^10}|{:^20}'.format('option name', 'desired val', 'decision', 'reason'), end='')
+    if with_results:
+        print('|   {}'.format('check result'), end='')
     print()
-
     print('=' * sep_line_len)
 
+    # table contents
     for opt in checklist:
-        print('CONFIG_{:<38}|{:^13}|{:^10}|{:^20}'.format(opt.name, opt.expected, opt.decision, opt.reason), end='')
-        if with_results:
-            print('|   {}'.format(opt.result), end='')
-        print()
+        if debug_mode and hasattr(opt, 'opts'):
+            print('    {:87}'.format('<<< ' + opt.__class__.__name__ + ' >>>'), end='')
+            if with_results:
+                print('|   {}'.format(opt.result), end='')
+            print()
+            for o in opt.opts:
+                if hasattr(o, 'ver_expected'):
+                    ver_req = 'kernel version >= ' + str(o.ver_expected[0]) + '.' + str(o.ver_expected[1])
+                    print('{:<91}'.format(ver_req), end='')
+                    if with_results:
+                        print('|   {}'.format(o.result), end='')
+                    print()
+                else:
+                    print_opt(o, with_results)
+        else:
+            print_opt(opt, with_results)
+        if debug_mode:
+            print('-' * sep_line_len)
     print()
 
 
@@ -484,7 +519,11 @@ def check_config_file(checklist, fname):
         opt_is_off = re.compile("# CONFIG_[a-zA-Z0-9_]* is not set")
 
         if not json_mode:
-            print('[+] Checking "{}" against hardening preferences...'.format(fname))
+            if not debug_mode:
+                which = arch
+            else:
+                which = 'ALL (debug)'
+            print('[+] Checking "{}" against {} hardening preferences...'.format(fname, which))
         for line in f.readlines():
             line = line.strip()
             option = None
@@ -509,7 +548,7 @@ def check_config_file(checklist, fname):
             known_options = [opt.name for opt in checklist]
             for option, value in parsed_options.items():
                 if option not in known_options:
-                    print("DEBUG: dunno about option {} ({})".format(option, value))
+                    print('DEBUG: dunno about option {} ({})'.format(option, value))
 
         print_checklist(checklist, True)
 
@@ -521,13 +560,14 @@ if __name__ == '__main__':
     parser.add_argument('-c', '--config',
                         help='check the config_file against these preferences')
     parser.add_argument('--debug', action='store_true',
-                        help='enable internal debug mode')
+                        help='enable internal debug mode (not for production use)')
     parser.add_argument('--json', action='store_true',
                         help='print results in JSON format')
     args = parser.parse_args()
 
     if args.debug:
         debug_mode = True
+        print('[!] WARNING: debug mode is enabled')
     if args.json:
         json_mode = True
     if debug_mode and json_mode:
@@ -560,7 +600,11 @@ if __name__ == '__main__':
         arch = args.print
         construct_checklist(config_checklist, arch)
         if not json_mode:
-            print('[+] Printing kernel hardening preferences for {}...'.format(arch))
+            if not debug_mode:
+                which = arch
+            else:
+                which = 'ALL architectures (debug)'
+            print('[+] Printing kernel hardening preferences for {}...'.format(which))
         print_checklist(config_checklist, False)
         sys.exit(0)