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