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
23import subprocess
24from check_common import read_json_file, traverse_file_in_each_type
25
26BASELINE_SUFFIX = ".baseline"
27
28
29class PolicyDb(object):
30    def __init__(self, attributes_map, allow_map, class_map):
31        self.attributes_map = attributes_map
32        self.allow_map = allow_map
33        self.class_map = class_map
34
35
36def simplify_string(string):
37    return string.replace('(', '').replace(')', '').replace('\n', '').strip()
38
39
40def deal_with_allow(cil_file, allow_map, attributes_map):
41    with open(cil_file, 'r', encoding='utf-8') as cil_read:
42        for line in cil_read:
43            line = line.strip()
44            if not line.startswith('(allow ') and not line.startswith('(auditallow '):
45                continue
46            sub_string = simplify_string(line)
47            elem_list = sub_string.split(' ')
48            # (allow A B (dir (getattr)))
49            if len(elem_list) < 5:
50                continue
51            split_attribute(elem_list, allow_map, attributes_map)
52
53
54def split_attribute(elem_list, allow_map, attributes_map):
55    scontext = elem_list[1]
56    tcontext = elem_list[2]
57    tclass = elem_list[3]
58    perm = elem_list[4:]
59    if scontext not in attributes_map:
60        # allow type self
61        if tcontext == 'self':
62            allow_map[scontext][(scontext, tclass)] += perm
63        # allow type attribute
64        elif tcontext in attributes_map:
65            for tcon in attributes_map[tcontext]:
66                allow_map[scontext][(tcon, tclass)] += perm
67        # allow type type
68        else:
69            allow_map[scontext][(tcontext, tclass)] += perm
70        return
71
72    for scon in attributes_map[scontext]:
73        # allow attribute self
74        if tcontext == 'self':
75            allow_map[scon][(scon, tclass)] += perm
76        # allow attribute attribute
77        elif tcontext in attributes_map:
78            for tcon in attributes_map[tcontext]:
79                allow_map[scon][(tcon, tclass)] += perm
80        # allow attribute type
81        else:
82            allow_map[scon][(tcontext, tclass)] += perm
83
84
85def deal_with_typeattributeset(cil_file, attributes_map):
86    with open(cil_file, 'r', encoding='utf-8') as cil_read:
87        for line in cil_read:
88            if not line.startswith('(typeattributeset '):
89                continue
90            sub_string = simplify_string(line)
91            elem_list = sub_string.split(' ')
92            if len(elem_list) < 3:
93                continue
94            attributes_map[elem_list[1]] += elem_list[2:]
95
96
97def deal_with_class(cil_file, class_map):
98    with open(cil_file, 'r', encoding='utf-8') as cil_read:
99        common_map = defaultdict(list)
100        for line in cil_read:
101            if not line.startswith('(common '):
102                continue
103            sub_string = simplify_string(line)
104            elem_list = sub_string.split(' ')
105            if len(elem_list) < 3:
106                continue
107            common_map[elem_list[1]] += elem_list[2:]
108
109    with open(cil_file, 'r', encoding='utf-8') as cil_read:
110        for line in cil_read:
111            if not line.startswith('(class '):
112                continue
113            sub_string = simplify_string(line)
114            elem_list = sub_string.split(' ')
115            if len(elem_list) > 2:
116                class_map[elem_list[1]] += elem_list[2:]
117
118    with open(cil_file, 'r', encoding='utf-8') as cil_read:
119        for line in cil_read:
120            if not line.startswith('(classcommon '):
121                continue
122            sub_string = simplify_string(line)
123            elem_list = sub_string.split(' ')
124            if len(elem_list) < 3:
125                continue
126            class_map[elem_list[1]] += common_map[elem_list[2]]
127
128
129def generate_database(cil_file):
130    attributes_map = defaultdict(list)
131    class_map = defaultdict(list)
132    allow_map = defaultdict(lambda: defaultdict(list))
133    deal_with_typeattributeset(cil_file, attributes_map)
134    deal_with_allow(cil_file, allow_map, attributes_map)
135    deal_with_class(cil_file, class_map)
136    return PolicyDb(attributes_map, allow_map, class_map)
137
138
139def build_conf(output_conf, file_list, with_developer=False):
140    m4_args = []
141    if with_developer:
142        m4_args += ["-D", "build_with_developer=enable"]
143    build_conf_cmd = ["m4", "-s", "--fatal-warnings"] + m4_args + file_list
144    with open(output_conf, 'w', encoding="utf-8") as fd:
145        ret = subprocess.run(build_conf_cmd, shell=False, stdout=fd).returncode
146        if ret != 0:
147            raise Exception(ret)
148
149
150def get_baseline_file(args):
151    script_path = os.path.dirname(os.path.realpath(__file__))
152    baseline_file_list = [os.path.join(script_path, "config/glb_def.txt")]
153    baseline_file_list += traverse_file_in_each_type(args.policy_dir_list, BASELINE_SUFFIX)
154    return baseline_file_list
155
156
157def generate_baseline_database(args, domain, attributes_map, with_developer):
158    baseline_file_list = get_baseline_file(args)
159    output_path = os.path.dirname(os.path.realpath(args.developer_cil_file if with_developer else args.cil_file))
160    output_baseline = os.path.join(output_path, domain + BASELINE_SUFFIX)
161    build_conf(output_baseline, baseline_file_list, with_developer)
162    baseline_map = defaultdict(lambda: defaultdict(list))
163    deal_with_allow(output_baseline, baseline_map, attributes_map)
164    return baseline_map[domain]
165
166
167def check_baseline(args, domain, policy_db, with_developer):
168    baseline_map = generate_baseline_database(args, domain, policy_db.attributes_map, with_developer)
169    none_baseline_list = set()
170    domain_policy = policy_db.allow_map[domain]
171    baseline_diff = domain_policy.keys() ^ baseline_map.keys()
172    for diff in baseline_diff:
173        expect_perm = ''.join(['(', ' '.join(set(baseline_map.get(diff, ''))), ')))'])
174        expect = ' '.join(['expect rule: (allow', domain, ' ('.join(diff), expect_perm])
175        actual_perm = ''.join(['(', ' '.join(set(domain_policy.get(diff, ''))), ')))'])
176        actual = ' '.join(['actual rule: (allow', domain, ' ('.join(diff), actual_perm])
177        none_baseline_list.add('; '.join([expect, actual]))
178
179    for contexts in domain_policy.keys():
180        if set(baseline_map.get(contexts, '')) != set(domain_policy.get(contexts, '')):
181            expect_perm = ''.join(['(', ' '.join(set(baseline_map.get(contexts, ''))), ')))'])
182            expect = ' '.join(['expect rule: (allow', domain, ' ('.join(contexts), expect_perm])
183            actual_perm = ''.join(['(', ' '.join(set(domain_policy.get(contexts, ''))), ')))'])
184            actual = ' '.join(['actual rule: (allow', domain, ' ('.join(contexts), actual_perm])
185            none_baseline_list.add('; '.join([expect, actual]))
186
187    if len(none_baseline_list):
188        print('\tcheck \'{}\' baseline in {} mode failed'.format(domain, "developer" if with_developer else "user"))
189        for violation in none_baseline_list:
190            print('\t\t{}'.format(violation))
191        print('\tThere are two solutions:\n',
192            '\t1. Add the above actual rule to baseline file \'{}\' under \'{}\'{}\n'.format(
193                domain + BASELINE_SUFFIX, args.policy_dir_list, " and add developer_only" if with_developer else ""),
194            '\t2. Change the policy to satisfy expect rule\n')
195        return True
196    return False
197
198
199def parse_args():
200    parser = argparse.ArgumentParser()
201    parser.add_argument(
202        '--cil_file', help='the cil file path', required=True)
203    parser.add_argument(
204        '--developer_cil_file', help='the developer cil file path', required=True)
205    parser.add_argument(
206        '--config', help='the config file path', required=True)
207    parser.add_argument(
208        '--policy-dir-list', help='the policy dir list', required=True)
209    return parser.parse_args()
210
211
212if __name__ == '__main__':
213    input_args = parse_args()
214    script_dir = os.path.dirname(os.path.realpath(__file__))
215
216    user_policy_db = generate_database(input_args.cil_file)
217    developer_policy_db = generate_database(input_args.developer_cil_file)
218    baselines = read_json_file(os.path.join(script_dir, input_args.config)).get('baseline')
219    check_result = False
220    for label_name in baselines:
221        check_result |= check_baseline(input_args, label_name, user_policy_db, False)
222        check_result |= check_baseline(input_args, label_name, developer_policy_db, True)
223    if check_result:
224        raise Exception(-1)
225