1
# ----------------------------------------------------------------------
2
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of version 2 of the GNU General Public
6
# License as published by the Free Software Foundation.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# ----------------------------------------------------------------------
14
from __future__ import with_statement
17
from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp # , msg, error, debug
19
class Severity(object):
20
def __init__(self, dbname=None, default_rank=10):
21
"""Initialises the class object"""
22
self.PROF_DIR = '/etc/apparmor.d' # The profile directory
23
self.severity = dict()
24
self.severity['DATABASENAME'] = dbname
25
self.severity['CAPABILITIES'] = {}
26
self.severity['FILES'] = {}
27
self.severity['REGEXPS'] = {}
28
self.severity['DEFAULT_RANK'] = default_rank
29
# For variable expansions for the profile
30
self.severity['VARIABLES'] = dict()
34
with open_file_read(dbname) as database: # open(dbname, 'r')
35
for lineno, line in enumerate(database, start=1):
36
line = line.strip() # or only rstrip and lstrip?
37
if line == '' or line.startswith('#'):
39
if line.startswith('/'):
41
path, read, write, execute = line.split()
42
read, write, execute = int(read), int(write), int(execute)
44
raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
46
if read not in range(0, 11) or write not in range(0, 11) or execute not in range(0, 11):
47
raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
48
path = path.lstrip('/')
50
self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute}
52
ptr = self.severity['REGEXPS']
53
pieces = path.split('/')
54
for index, piece in enumerate(pieces):
56
path = '/'.join(pieces[index:])
57
regexp = convert_regexp(path)
58
ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}}
61
ptr[piece] = ptr.get(piece, {})
63
elif line.startswith('CAP_'):
65
resource, severity = line.split()
66
severity = int(severity)
68
error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line)
70
raise AppArmorException(error_message) # from None
72
if severity not in range(0, 11):
73
raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
74
self.severity['CAPABILITIES'][resource] = severity
76
raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
78
def handle_capability(self, resource):
79
"""Returns the severity of for the capability resource, default value if no match"""
80
if resource in self.severity['CAPABILITIES'].keys():
81
return self.severity['CAPABILITIES'][resource]
82
# raise ValueError("unexpected capability rank input: %s"%resource)
83
warn("unknown capability: %s" % resource)
84
return self.severity['DEFAULT_RANK']
86
def check_subtree(self, tree, mode, sev, segments):
87
"""Returns the max severity from the regex tree"""
88
if len(segments) == 0:
93
path = '/'.join([first] + rest)
94
# Check if we have a matching directory tree to descend into
95
if tree.get(first, False):
96
sev = self.check_subtree(tree[first], mode, sev, rest)
97
# If severity still not found, match against globs
99
# Match against all globs at this directory level
100
for chunk in tree.keys():
102
# Match rest of the path
103
if re.search("^" + chunk, path):
105
if "AA_RANK" in tree[chunk].keys():
107
if sev is None or tree[chunk]["AA_RANK"].get(m, -1) > sev:
108
sev = tree[chunk]["AA_RANK"].get(m, None)
111
def handle_file(self, resource, mode):
112
"""Returns the severity for the file, default value if no match found"""
113
resource = resource[1:] # remove initial / from path
114
pieces = resource.split('/') # break path into directory level chunks
116
# Check for an exact match in the db
117
if resource in self.severity['FILES'].keys():
118
# Find max value among the given modes
120
if sev is None or self.severity['FILES'][resource].get(m, -1) > sev:
121
sev = self.severity['FILES'][resource].get(m, None)
123
# Search regex tree for matching glob
124
sev = self.check_subtree(self.severity['REGEXPS'], mode, sev, pieces)
126
# Return default rank if severity cannot be found
127
return self.severity['DEFAULT_RANK']
131
def rank(self, resource, mode=None):
132
"""Returns the rank for the resource file/capability"""
133
if '@' in resource: # path contains variable
134
return self.handle_variable_rank(resource, mode)
135
elif resource[0] == '/': # file resource
136
return self.handle_file(resource, mode)
137
elif resource[0:4] == 'CAP_': # capability resource
138
return self.handle_capability(resource)
140
raise AppArmorException("Unexpected rank input: %s" % resource)
142
def handle_variable_rank(self, resource, mode):
143
"""Returns the max possible rank for file resources containing variables"""
144
regex_variable = re.compile('@{([^{.]*)}')
147
variable = regex_variable.search(resource).groups()[0]
148
variable = '@{%s}' % variable
149
#variables = regex_variable.findall(resource)
150
for replacement in self.severity['VARIABLES'][variable]:
151
resource_replaced = self.variable_replace(variable, replacement, resource)
152
rank_new = self.handle_variable_rank(resource_replaced, mode)
153
#rank_new = self.handle_variable_rank(resource.replace('@{'+variable+'}', replacement), mode)
154
if rank is None or rank_new > rank:
158
return self.handle_file(resource, mode)
160
def variable_replace(self, variable, replacement, resource):
161
"""Returns the expanded path for the passed variable"""
164
# Check for leading or trailing / that may need to be collapsed
165
if resource.find("/" + variable) != -1 and resource.find("//" + variable) == -1: # find that a single / exists before variable or not
167
if resource.find(variable + "/") != -1 and resource.find(variable + "//") == -1:
169
if replacement[0] == '/' and replacement[:2] != '//' and leading: # finds if the replacement has leading / or not
170
replacement = replacement[1:]
171
if replacement[-1] == '/' and replacement[-2:] != '//' and trailing:
172
replacement = replacement[:-1]
173
return resource.replace(variable, replacement)
175
def load_variables(self, prof_path):
176
"""Loads the variables for the given profile"""
177
regex_include = re.compile('^#?include\s*<(\S*)>')
178
if os.path.isfile(prof_path):
179
with open_file_read(prof_path) as f_in:
182
# If any includes, load variables from them first
183
match = regex_include.search(line)
185
new_path = match.groups()[0]
186
new_path = self.PROF_DIR + '/' + new_path
187
self.load_variables(new_path)
189
# Remove any comments
191
line = line.split('#')[0].rstrip()
192
# Expected format is @{Variable} = value1 value2 ..
193
if line.startswith('@') and '=' in line:
195
line = line.split('+=')
197
self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()]
199
raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path))
201
line = line.split('=')
202
if line[0] in self.severity['VARIABLES'].keys():
203
raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path))
204
self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()]
206
def unload_variables(self):
207
"""Clears all loaded variables"""
208
self.severity['VARIABLES'] = dict()