Merge branch 'open_check'
[kconfig-hardened-check.git] / kernel_hardening_checker / __init__.py
index ca163b7db5a077d0895faf4baa6f161f5b16e9bd..5893fab542ddd3e16e1b22a55f86e5e21e1ab258 100644 (file)
@@ -8,28 +8,35 @@ Author: Alexander Popov <alex.popov@linux.com>
 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 os
 import gzip
 import sys
 from argparse import ArgumentParser
-from collections import OrderedDict
-from typing import List, Tuple, OrderedDict, TextIO
+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 StrOrNone, TupleOrNone, ChecklistObjType, print_unknown_options, 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) -> TextIO:
-    if file.endswith('.gz'):
-        return gzip.open(file, *args, **kwargs)
-    return open(file, *args, **kwargs)
+# kernel-hardening-checker version
+__version__ = '0.6.6'
+
+
+def _open(file: str) -> TextIO:
+    try:
+        if file.endswith('.gz'):
+            return gzip.open(file, 'rt', encoding='utf-8')
+        return open(file, 'rt', encoding='utf-8')
+    except FileNotFoundError:
+        sys.exit(f'[!] ERROR: unable to open {file}, are you sure it exists?')
 
 
 def detect_arch(fname: str, archs: List[str]) -> Tuple[StrOrNone, str]:
-    with _open(fname, 'rt', encoding='utf-8') as f:
+    with _open(fname) as f:
         arch_pattern = re.compile(r"CONFIG_[a-zA-Z0-9_]+=y$")
         arch = None
         for line in f.readlines():
@@ -46,7 +53,7 @@ def detect_arch(fname: str, archs: List[str]) -> Tuple[StrOrNone, str]:
 
 
 def detect_kernel_version(fname: str) -> Tuple[TupleOrNone, str]:
-    with _open(fname, 'rt', encoding='utf-8') as f:
+    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):
@@ -55,7 +62,7 @@ def detect_kernel_version(fname: str) -> Tuple[TupleOrNone, str]:
                 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)):
+                    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
@@ -65,7 +72,7 @@ def detect_kernel_version(fname: str) -> Tuple[TupleOrNone, str]:
 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]
@@ -100,14 +107,21 @@ def print_checklist(mode: StrOrNone, checklist: List[ChecklistObjType], with_res
     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':
@@ -116,9 +130,7 @@ def print_checklist(mode: StrOrNone, checklist: List[ChecklistObjType], with_res
 
     # 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)'
@@ -127,8 +139,8 @@ def print_checklist(mode: StrOrNone, checklist: List[ChecklistObjType], with_res
         print(f'[+] Config check is finished: \'OK\' - {ok_count}{ok_suppressed} / \'FAIL\' - {fail_count}{fail_suppressed}')
 
 
-def parse_kconfig_file(_mode: StrOrNone, parsed_options: OrderedDict[str, str], fname: str) -> None:
-    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$")
 
@@ -156,9 +168,15 @@ def parse_kconfig_file(_mode: StrOrNone, parsed_options: OrderedDict[str, str],
                 parsed_options[option] = value
 
 
-def parse_cmdline_file(mode: StrOrNone, parsed_options: OrderedDict[str, str], fname: str) -> None:
+def parse_cmdline_file(mode: StrOrNone, parsed_options: Dict[str, str], fname: str) -> None:
+    if not os.path.isfile(fname):
+        sys.exit(f'[!] ERROR: unable to open {fname}, are you sure it exists?')
+
     with open(fname, 'r', encoding='utf-8') as f:
         line = f.readline()
+        if not line:
+            sys.exit(f'[!] ERROR: empty "{fname}"')
+
         opts = line.split()
 
         line = f.readline()
@@ -178,7 +196,10 @@ def parse_cmdline_file(mode: StrOrNone, parsed_options: OrderedDict[str, str], f
             parsed_options[name] = value
 
 
-def parse_sysctl_file(mode: StrOrNone, parsed_options: OrderedDict[str, str], fname: str) -> None:
+def parse_sysctl_file(mode: StrOrNone, parsed_options: Dict[str, str], fname: str) -> None:
+    if not os.path.isfile(fname):
+        sys.exit(f'[!] ERROR: unable to open {fname}, are you sure it exists?')
+
     with open(fname, 'r', encoding='utf-8') as f:
         sysctl_pattern = re.compile(r"[a-zA-Z0-9/\._-]+ =.*$")
         for line in f.readlines():
@@ -285,7 +306,7 @@ def main() -> None:
             add_sysctl_checks(config_checklist, arch)
 
         # populate the checklist with the parsed Kconfig data
-        parsed_kconfig_options = OrderedDict() # type: OrderedDict[str, str]
+        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')
 
@@ -294,13 +315,13 @@ def main() -> None:
 
         if args.cmdline:
             # populate the checklist with the parsed cmdline data
-            parsed_cmdline_options = OrderedDict() # type: OrderedDict[str, str]
+            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() # type: OrderedDict[str, str]
+            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')
 
@@ -345,7 +366,7 @@ def main() -> None:
         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')