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