1#!/usr/bin/env python
2# coding: utf-8
3
4"""
5Copyright (c) 2023 Huawei Device Co., Ltd.
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10    http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17
18"""
19
20import argparse
21import os
22from collections import defaultdict
23from check_common import read_json_file, traverse_file_in_each_type
24
25WHITELIST_FILE_NAME = "perm_group_whitelist.json"
26
27
28class PolicyDb(object):
29    def __init__(self, attributes_map, allow_map, class_map):
30        self.attributes_map = attributes_map
31        self.allow_map = allow_map
32        self.class_map = class_map
33
34
35def simplify_string(string):
36    return string.replace('(', '').replace(')', '').replace('\n', '').strip()
37
38
39def deal_with_allow(cil_file, allow_map, attributes_map):
40    with open(cil_file, 'r', encoding='utf-8') as cil_read:
41        for line in cil_read:
42            if not line.startswith('(allow ') and not line.startswith('(auditallow '):
43                continue
44            sub_string = simplify_string(line)
45            elem_list = sub_string.split(' ')
46            # (allow A B (dir (getattr)))
47            if len(elem_list) < 5:
48                continue
49            split_attribute(elem_list, allow_map, attributes_map)
50
51
52def split_attribute(elem_list, allow_map, attributes_map):
53    scontext = elem_list[1]
54    tcontext = elem_list[2]
55    tclass = elem_list[3]
56    perm = elem_list[4:]
57    if scontext not in attributes_map:
58        # allow type self
59        if tcontext == 'self':
60            allow_map[(scontext, scontext)][tclass] += perm
61        # allow type attribute
62        elif tcontext in attributes_map:
63            for tcon in attributes_map[tcontext]:
64                allow_map[(scontext, tcon)][tclass] += perm
65        # allow type type
66        else:
67            allow_map[(scontext, tcontext)][tclass] += perm
68        return
69
70    for scon in attributes_map[scontext]:
71        # allow attribute self
72        if tcontext == 'self':
73            allow_map[(scon, scon)][tclass] += perm
74        # allow attribute attribute
75        elif tcontext in attributes_map:
76            for tcon in attributes_map[tcontext]:
77                allow_map[(scon, tcon)][tclass] += perm
78        # allow attribute type
79        else:
80            allow_map[(scon, tcontext)][tclass] += perm
81
82
83def deal_with_typeattributeset(cil_file, attributes_map):
84    with open(cil_file, 'r', encoding='utf-8') as cil_read:
85        for line in cil_read:
86            if not line.startswith('(typeattributeset '):
87                continue
88            sub_string = simplify_string(line)
89            elem_list = sub_string.split(' ')
90            if len(elem_list) < 3:
91                continue
92            attributes_map[elem_list[1]] += elem_list[2:]
93
94
95def deal_with_class(cil_file, class_map):
96    with open(cil_file, 'r', encoding='utf-8') as cil_read:
97        common_map = defaultdict(list)
98        for line in cil_read:
99            if not line.startswith('(common '):
100                continue
101            sub_string = simplify_string(line)
102            elem_list = sub_string.split(' ')
103            if len(elem_list) < 3:
104                continue
105            common_map[elem_list[1]] += elem_list[2:]
106
107    with open(cil_file, 'r', encoding='utf-8') as cil_read:
108        for line in cil_read:
109            if not line.startswith('(class '):
110                continue
111            sub_string = simplify_string(line)
112            elem_list = sub_string.split(' ')
113            if len(elem_list) > 2:
114                class_map[elem_list[1]] += elem_list[2:]
115
116    with open(cil_file, 'r', encoding='utf-8') as cil_read:
117        for line in cil_read:
118            if not line.startswith('(classcommon '):
119                continue
120            sub_string = simplify_string(line)
121            elem_list = sub_string.split(' ')
122            if len(elem_list) < 3:
123                continue
124            class_map[elem_list[1]] += common_map[elem_list[2]]
125
126
127def generate_database(cil_file):
128    attributes_map = defaultdict(list)
129    class_map = defaultdict(list)
130    allow_map = defaultdict(lambda: defaultdict(list))
131    deal_with_typeattributeset(cil_file, attributes_map)
132    deal_with_allow(cil_file, allow_map, attributes_map)
133    deal_with_class(cil_file, class_map)
134    return PolicyDb(attributes_map, allow_map, class_map)
135
136
137class CheckPermGroup(object):
138    def __init__(self, check_class_list, check_perms):
139        self.check_class_list = check_class_list
140        self.check_perms = check_perms
141
142
143def get_check_class_list(check_tclass, check_perms, class_map):
144    if check_tclass == '*':
145        check_class_list = []
146        for tclass in class_map.keys():
147            if set(check_perms) <= set(class_map[tclass]):
148                check_class_list.append(tclass)
149        return check_class_list
150    return [check_tclass]
151
152
153def get_perm_group_list(rule, class_map):
154    check_perm_group_list = []
155    perm_group_list = rule.get('perm_group')
156    for perm_group in perm_group_list:
157        check_tclass = perm_group.get('tclass')
158        check_perms = perm_group.get('perm').split(' ')
159        check_class_list = get_check_class_list(check_tclass, check_perms, class_map)
160        check_perm_group_list.append(CheckPermGroup(check_class_list, check_perms))
161    return check_perm_group_list
162
163
164def get_whitelist(args, check_name, with_developer):
165    whitelist_file_list = traverse_file_in_each_type(args.policy_dir_list, WHITELIST_FILE_NAME)
166    contexts_list = []
167    for path in whitelist_file_list:
168        white_list = read_json_file(path).get('whitelist')
169        for item in white_list:
170            if item.get('name') != check_name:
171                continue
172            contexts_list.extend(item.get('user'))
173            if with_developer:
174                contexts_list.extend(item.get('developer'))
175    return contexts_list
176
177
178def check_perm_group(args, rule, policy_db, with_developer):
179    check_name = rule.get('name')
180    check_perm_group_list = get_perm_group_list(rule, policy_db.class_map)
181    contexts_list = get_whitelist(args, check_name, with_developer)
182
183    non_exempt_violator_list = []
184    violator_list = []
185    for contexts in policy_db.allow_map.keys():
186        check_result = 0
187        for perm_group in check_perm_group_list:
188            check_success = False
189            for check_class in perm_group.check_class_list:
190                check_success |= (set(perm_group.check_perms) <= set(policy_db.allow_map[contexts][check_class]))
191            if check_success:
192                check_result += 1
193        if check_result != len(check_perm_group_list):
194            continue
195        violater = ' '.join(contexts)
196        # all violation list
197        violator_list.append(violater)
198        # if not in whitelist
199        if violater not in contexts_list:
200            non_exempt_violator_list.append(violater)
201
202    if len(non_exempt_violator_list):
203        print('\tcheck rule \'{}\' in {} mode failed, {}'.format(
204            check_name, "developer" if with_developer else "user", rule.get('description')))
205        print('\tviolation list (scontext tcontext):')
206        for violation in non_exempt_violator_list:
207            print('\t\t{}'.format(violation))
208        print('\tThere are two solutions:\n',
209              '\t1. Add the above list to whitelist file \'{}\' under \'{}\' in \'{}\' part of \'{}\'\n'.format(
210                    WHITELIST_FILE_NAME, args.policy_dir_list, "developer" if with_developer else "user", check_name),
211              '\t2. Change the policy to avoid violating rule \'{}\'\n'.format(check_name))
212        return True
213
214    diff_list = list(set(contexts_list) - set(violator_list))
215    if len(diff_list):
216        print('\tcheck rule \'{}\' failed in whitelist file \'{}\'\n'.format(check_name, WHITELIST_FILE_NAME),
217              '\tremove the following unnecessary whitelists in rule \'{}\' part \'{}\':'.format(
218                    check_name, 'developer' if with_developer else 'user'))
219        for diff in diff_list:
220            print('\t\t{}'.format(diff))
221        return True
222    return False
223
224
225def parse_args():
226    parser = argparse.ArgumentParser()
227    parser.add_argument(
228        '--cil_file', help='the cil file path', required=True)
229    parser.add_argument(
230        '--developer_cil_file', help='the developer cil file path', required=True)
231    parser.add_argument(
232        '--policy-dir-list', help='the whitelist path list', required=True)
233    parser.add_argument(
234        '--config', help='the config file path', required=True)
235    return parser.parse_args()
236
237
238if __name__ == '__main__':
239    input_args = parse_args()
240    script_path = os.path.dirname(os.path.realpath(__file__))
241
242    user_policy_db = generate_database(input_args.cil_file)
243    developer_policy_db = generate_database(input_args.developer_cil_file)
244    check_rules = read_json_file(os.path.join(script_path, input_args.config)).get('check_rules')
245    result = False
246    for check_rule in check_rules:
247        result |= check_perm_group(input_args, check_rule, user_policy_db, False)
248        result |= check_perm_group(input_args, check_rule, developer_policy_db, True)
249    if result:
250        raise Exception(-1)
251