Add the X86_KERNEL_IBT check
[kconfig-hardened-check.git] / kconfig_hardened_check / engine.py
1 #!/usr/bin/python3
2
3 """
4 This tool helps me to check Linux kernel options against
5 my security hardening preferences for X86_64, ARM64, X86_32, and ARM.
6 Let the computers do their job!
7
8 Author: Alexander Popov <alex.popov@linux.com>
9
10 This module is the engine of checks.
11 """
12
13 # pylint: disable=missing-class-docstring,missing-function-docstring
14 # pylint: disable=line-too-long,invalid-name,too-many-branches
15
16
17 class OptCheck:
18     def __init__(self, reason, decision, name, expected):
19         assert(name and name == name.strip() and len(name.split()) == 1), \
20                f'invalid name "{name}" for {self.__class__.__name__}'
21         self.name = name
22
23         assert(decision and decision == decision.strip() and len(decision.split()) == 1), \
24                f'invalid decision "{decision}" for "{name}" check'
25         self.decision = decision
26
27         assert(reason and reason == reason.strip() and len(reason.split()) == 1), \
28                f'invalid reason "{reason}" for "{name}" check'
29         self.reason = reason
30
31         assert(expected and expected == expected.strip()), \
32                f'invalid expected value "{expected}" for "{name}" check (1)'
33         val_len = len(expected.split())
34         if val_len == 3:
35             assert(expected in ('is not set', 'is not off')), \
36                    f'invalid expected value "{expected}" for "{name}" check (2)'
37         elif val_len == 2:
38             assert(expected == 'is present'), \
39                    f'invalid expected value "{expected}" for "{name}" check (3)'
40         else:
41             assert(val_len == 1), \
42                    f'invalid expected value "{expected}" for "{name}" check (4)'
43         self.expected = expected
44
45         self.state = None
46         self.result = None
47
48     def check(self):
49         # handle the 'is present' check
50         if self.expected == 'is present':
51             if self.state is None:
52                 self.result = 'FAIL: is not present'
53             else:
54                 self.result = 'OK: is present'
55             return
56
57         # handle the 'is not off' option check
58         if self.expected == 'is not off':
59             if self.state == 'off':
60                 self.result = 'FAIL: is off'
61             elif self.state == '0':
62                 self.result = 'FAIL: is off, "0"'
63             elif self.state is None:
64                 self.result = 'FAIL: is off, not found'
65             else:
66                 self.result = f'OK: is not off, "{self.state}"'
67             return
68
69         # handle the option value check
70         if self.expected == self.state:
71             self.result = 'OK'
72         elif self.state is None:
73             if self.expected == 'is not set':
74                 self.result = 'OK: is not found'
75             else:
76                 self.result = 'FAIL: is not found'
77         else:
78             self.result = f'FAIL: "{self.state}"'
79
80     def table_print(self, _mode, with_results):
81         print(f'{self.name:<40}|{self.type:^7}|{self.expected:^12}|{self.decision:^10}|{self.reason:^18}', end='')
82         if with_results:
83             print(f'| {self.result}', end='')
84
85     def json_dump(self, with_results):
86         dump = [self.name, self.type, self.expected, self.decision, self.reason]
87         if with_results:
88             dump.append(self.result)
89         return dump
90
91
92 class KconfigCheck(OptCheck):
93     def __init__(self, *args, **kwargs):
94         super().__init__(*args, **kwargs)
95         self.name = 'CONFIG_' + self.name
96
97     @property
98     def type(self):
99         return 'kconfig'
100
101
102 class CmdlineCheck(OptCheck):
103     @property
104     def type(self):
105         return 'cmdline'
106
107
108 class VersionCheck:
109     def __init__(self, ver_expected):
110         assert(ver_expected and isinstance(ver_expected, tuple) and len(ver_expected) == 2), \
111                f'invalid version "{ver_expected}" for VersionCheck'
112         self.ver_expected = ver_expected
113         self.ver = ()
114         self.result = None
115
116     @property
117     def type(self):
118         return 'version'
119
120     def check(self):
121         if self.ver[0] > self.ver_expected[0]:
122             self.result = f'OK: version >= {self.ver_expected[0]}.{self.ver_expected[1]}'
123             return
124         if self.ver[0] < self.ver_expected[0]:
125             self.result = f'FAIL: version < {self.ver_expected[0]}.{self.ver_expected[1]}'
126             return
127         if self.ver[1] >= self.ver_expected[1]:
128             self.result = f'OK: version >= {self.ver_expected[0]}.{self.ver_expected[1]}'
129             return
130         self.result = f'FAIL: version < {self.ver_expected[0]}.{self.ver_expected[1]}'
131
132     def table_print(self, _mode, with_results):
133         ver_req = f'kernel version >= {self.ver_expected[0]}.{self.ver_expected[1]}'
134         print(f'{ver_req:<91}', end='')
135         if with_results:
136             print(f'| {self.result}', end='')
137
138
139 class ComplexOptCheck:
140     def __init__(self, *opts):
141         self.opts = opts
142         assert(self.opts), \
143                f'empty {self.__class__.__name__} check'
144         assert(len(self.opts) != 1), \
145                f'useless {self.__class__.__name__} check: {opts}'
146         assert(isinstance(opts[0], (KconfigCheck, CmdlineCheck))), \
147                f'invalid {self.__class__.__name__} check: {opts}'
148         self.result = None
149
150     @property
151     def type(self):
152         return 'complex'
153
154     @property
155     def name(self):
156         return self.opts[0].name
157
158     @property
159     def expected(self):
160         return self.opts[0].expected
161
162     def table_print(self, mode, with_results):
163         if mode == 'verbose':
164             print(f'    {"<<< " + self.__class__.__name__ + " >>>":87}', end='')
165             if with_results:
166                 print(f'| {self.result}', end='')
167             for o in self.opts:
168                 print()
169                 o.table_print(mode, with_results)
170         else:
171             o = self.opts[0]
172             o.table_print(mode, False)
173             if with_results:
174                 print(f'| {self.result}', end='')
175
176     def json_dump(self, with_results):
177         dump = self.opts[0].json_dump(False)
178         if with_results:
179             dump.append(self.result)
180         return dump
181
182
183 class OR(ComplexOptCheck):
184     # self.opts[0] is the option that this OR-check is about.
185     # Use cases:
186     #     OR(<X_is_hardened>, <X_is_disabled>)
187     #     OR(<X_is_hardened>, <old_X_is_hardened>)
188     def check(self):
189         for i, opt in enumerate(self.opts):
190             opt.check()
191             if opt.result.startswith('OK'):
192                 self.result = opt.result
193                 # Add more info for additional checks:
194                 if i != 0:
195                     if opt.result == 'OK':
196                         self.result = f'OK: {opt.name} is "{opt.expected}"'
197                     elif opt.result == 'OK: is not found':
198                         self.result = f'OK: {opt.name} is not found'
199                     elif opt.result == 'OK: is present':
200                         self.result = f'OK: {opt.name} is present'
201                     elif opt.result.startswith('OK: is not off'):
202                         self.result = f'OK: {opt.name} is not off'
203                     else:
204                         # VersionCheck provides enough info
205                         assert(opt.result.startswith('OK: version')), \
206                                f'unexpected OK description "{opt.result}"'
207                 return
208         self.result = self.opts[0].result
209
210
211 class AND(ComplexOptCheck):
212     # self.opts[0] is the option that this AND-check is about.
213     # Use cases:
214     #     AND(<suboption>, <main_option>)
215     #       Suboption is not checked if checking of the main_option is failed.
216     #     AND(<X_is_disabled>, <old_X_is_disabled>)
217     def check(self):
218         for i, opt in reversed(list(enumerate(self.opts))):
219             opt.check()
220             if i == 0:
221                 self.result = opt.result
222                 return
223             if not opt.result.startswith('OK'):
224                 # This FAIL is caused by additional checks,
225                 # and not by the main option that this AND-check is about.
226                 # Describe the reason of the FAIL.
227                 if opt.result.startswith('FAIL: \"') or opt.result == 'FAIL: is not found':
228                     self.result = f'FAIL: {opt.name} is not "{opt.expected}"'
229                 elif opt.result == 'FAIL: is not present':
230                     self.result = f'FAIL: {opt.name} is not present'
231                 elif opt.result in ('FAIL: is off', 'FAIL: is off, "0"'):
232                     self.result = f'FAIL: {opt.name} is off'
233                 elif opt.result == 'FAIL: is off, not found':
234                     self.result = f'FAIL: {opt.name} is off, not found'
235                 else:
236                     # VersionCheck provides enough info
237                     self.result = opt.result
238                     assert(opt.result.startswith('FAIL: version')), \
239                            f'unexpected FAIL description "{opt.result}"'
240                 return
241
242
243 SIMPLE_OPTION_TYPES = ('kconfig', 'version', 'cmdline')
244
245
246 def populate_simple_opt_with_data(opt, data, data_type):
247     assert(opt.type != 'complex'), \
248            f'unexpected ComplexOptCheck "{opt.name}"'
249     assert(opt.type in SIMPLE_OPTION_TYPES), \
250            f'invalid opt type "{opt.type}"'
251     assert(data_type in SIMPLE_OPTION_TYPES), \
252            f'invalid data type "{data_type}"'
253     assert(data), \
254            'empty data'
255
256     if data_type != opt.type:
257         return
258
259     if data_type in ('kconfig', 'cmdline'):
260         opt.state = data.get(opt.name, None)
261     else:
262         assert(data_type == 'version'), \
263                f'unexpected data type "{data_type}"'
264         opt.ver = data
265
266
267 def populate_opt_with_data(opt, data, data_type):
268     if opt.type == 'complex':
269         for o in opt.opts:
270             if o.type == 'complex':
271                 # Recursion for nested ComplexOptCheck objects
272                 populate_opt_with_data(o, data, data_type)
273             else:
274                 populate_simple_opt_with_data(o, data, data_type)
275     else:
276         assert(opt.type in ('kconfig', 'cmdline')), \
277                f'bad type "{opt.type}" for a simple check'
278         populate_simple_opt_with_data(opt, data, data_type)
279
280
281 def populate_with_data(checklist, data, data_type):
282     for opt in checklist:
283         populate_opt_with_data(opt, data, data_type)
284
285
286 def override_expected_value(checklist, name, new_val):
287     for opt in checklist:
288         if opt.name == name:
289             assert(opt.type in ('kconfig', 'cmdline')), \
290                    f'overriding an expected value for "{opt.type}" checks is not supported yet'
291             opt.expected = new_val
292
293
294 def perform_checks(checklist):
295     for opt in checklist:
296         opt.check()