Skip to content

Commit ece3f7f

Browse files
committed
feat: implement _fill_missing_selinux_accesses
1 parent cca9c88 commit ece3f7f

File tree

2 files changed

+207
-18
lines changed

2 files changed

+207
-18
lines changed

db.py

Lines changed: 172 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import os
88
import stat
99
from collections.abc import Iterable
10-
from .helpers import construct_selinux_context, selinux_check_access
10+
from .helpers import (
11+
construct_selinux_context,
12+
selinux_check_access,
13+
selinux_label_lookup,
14+
)
1115

1216
if os.name == 'posix':
1317
import pwd
@@ -143,6 +147,7 @@ class DatabaseWriter(DatabaseCommon):
143147
def __init__(self, path: str):
144148
self.con = sqlite3.connect(path, isolation_level=None)
145149
self.cur = self.con.cursor()
150+
self._prepare_accesses()
146151

147152
def insert_access(
148153
self, case_id: int, subject_cid: int, path_rowid: int
@@ -169,24 +174,14 @@ def insert_result(
169174
'''INSERT INTO results VALUES(?, ?, null, ?)
170175
ON CONFLICT (access_id, operation_id)
171176
DO UPDATE SET medusa_result = ?''',
172-
(
173-
access_id,
174-
perm_id,
175-
result_med,
176-
result_med
177-
),
177+
(access_id, perm_id, result_med, result_med),
178178
)
179179
elif result_med is None:
180180
self.cur.execute(
181181
'''INSERT INTO results VALUES(?, ?, ?, null)
182182
ON CONFLICT (access_id, operation_id)
183183
DO UPDATE SET reference_result = ?''',
184-
(
185-
access_id,
186-
perm_id,
187-
result_ref,
188-
result_ref
189-
),
184+
(access_id, perm_id, result_ref, result_ref),
190185
)
191186
else:
192187
raise Exception('One of the results has to be None.')
@@ -198,6 +193,35 @@ def insert_or_select_access(
198193
return self.insert_access(case_id, subject_cid, path_rowid)
199194
return rowid
200195

196+
def _insert_selinux_access(
197+
self,
198+
):
199+
is_dir = stat.S_ISDIR(_type)
200+
_class = 'dir' if is_dir else 'file'
201+
202+
results = [
203+
selinux_check_access(subject_context, context, _class, perm)
204+
for perm in perms
205+
]
206+
if verbose:
207+
for perm, result in zip(perms, results):
208+
print(
209+
f'{subject_context}=>{context} {path} ({_class}:{perm})={result}'
210+
)
211+
self.cur.execute(
212+
'INSERT INTO accesses VALUES(?, ?, ?)',
213+
(
214+
case_id,
215+
subject_cid,
216+
rowid,
217+
),
218+
)
219+
220+
access_id = self.cur.lastrowid
221+
222+
for perm_id, result in zip(perms_id, results):
223+
self.insert_result(access_id, perm_id, result, None)
224+
201225
def insert_selinux_accesses(
202226
self,
203227
case_name: str,
@@ -218,7 +242,6 @@ def insert_selinux_accesses(
218242
subject.
219243
:param verbose: Turns on verbose output.
220244
"""
221-
self._prepare_accesses()
222245
self.cur.execute(
223246
'INSERT INTO cases VALUES(?) ON CONFLICT DO NOTHING',
224247
(case_name,),
@@ -275,6 +298,138 @@ def insert_selinux_accesses(
275298
for perm_id, result in zip(perms_id, results):
276299
self.insert_result(access_id, perm_id, result, None)
277300

301+
def fill_missing_selinux_accesses(
302+
self,
303+
case_name: str,
304+
subject_context: str,
305+
object_types: Iterable[str],
306+
verbose: bool = False,
307+
):
308+
"""Fill missing accesses for SELinux in the database.
309+
310+
Fill a table with SELinux accesses from `subject_context` to all files
311+
with types from `selinux_types`.
312+
313+
:param case_name: Name of the service that is examined. This will be
314+
used as a unique value in the database.
315+
:param subject_context: SELinux context of the subject.
316+
:param object_types:SELinux types that will be searched in the database
317+
and found files will be examined for read and write permissions from the
318+
subject.
319+
:param verbose: Turns on verbose output.
320+
"""
321+
perms = ('read', 'write')
322+
perms_id = self.get_operations_id(perms)
323+
324+
res = self.cur.execute(
325+
"""WITH RECURSIVE child AS
326+
(SELECT accesses.rowid AS access_rowid,
327+
accesses.node_rowid,
328+
accesses.subject_cid,
329+
contexts.name AS subject_context,
330+
fs.rowid,
331+
fs.parent,
332+
fs.name,
333+
type,
334+
fs.selinux_user,
335+
fs.selinux_role,
336+
fs.selinux_type,
337+
fs.selinux_category,
338+
fs.selinux_sensitivity,
339+
operations.rowid AS operation_id,
340+
operation,
341+
results.reference_result,
342+
results.medusa_result
343+
FROM accesses
344+
JOIN contexts ON subject_cid = contexts.rowid
345+
JOIN fs ON node_rowid = fs.rowid
346+
LEFT JOIN results ON accesses.ROWID = results.access_id
347+
LEFT JOIN operations ON results.operation_id = operations.rowid
348+
WHERE case_id = 1
349+
AND reference_result IS NULL
350+
UNION ALL SELECT access_rowid,
351+
node_rowid,
352+
subject_cid,
353+
subject_context,
354+
fs.rowid,
355+
fs.parent,
356+
fs.name || '/' || child.name,
357+
child.type,
358+
child.selinux_user,
359+
child.selinux_role,
360+
child.selinux_type,
361+
child.selinux_category,
362+
child.selinux_sensitivity,
363+
operation_id,
364+
operation,
365+
reference_result,
366+
medusa_result
367+
FROM fs,
368+
child
369+
WHERE child.parent = fs.rowid )
370+
SELECT access_rowid,
371+
subject_cid,
372+
subject_context,
373+
node_rowid,
374+
name AS path,
375+
TYPE,
376+
selinux_user || ':' || selinux_role || ':' || selinux_type || ':' || selinux_sensitivity || COALESCE(':' || selinux_category, '') AS selinux_context,
377+
operation_id,
378+
operation,
379+
reference_result,
380+
medusa_result
381+
FROM child
382+
WHERE rowid = 1
383+
"""
384+
)
385+
accesses = res.fetchall()
386+
for (
387+
access_id,
388+
subject_cid,
389+
subject_context,
390+
node_rowid,
391+
path,
392+
_type,
393+
selinux_context,
394+
operation_id,
395+
operation,
396+
reference_result,
397+
medusa_result,
398+
) in accesses:
399+
if selinux_context is None:
400+
selinux_context = selinux_label_lookup(path, _type)
401+
402+
is_dir = stat.S_ISDIR(_type)
403+
_class = 'dir' if is_dir else 'file'
404+
405+
if operation_id is None:
406+
# Computer access permissions for all operations
407+
results = [
408+
selinux_check_access(
409+
subject_context, selinux_context, _class, perm
410+
)
411+
for perm in perms
412+
]
413+
if verbose:
414+
for perm, result in zip(perms, results):
415+
print(
416+
f'{subject_context}=>{selinux_context} {path} ({_class}:{perm})={result}'
417+
)
418+
419+
for perm_id, result in zip(perms_id, results):
420+
self.insert_result(access_id, perm_id, result, None)
421+
continue
422+
# Updating just one operation
423+
result = selinux_check_access(
424+
subject_context, selinux_context, _class, operation
425+
)
426+
if verbose:
427+
print(
428+
f'{subject_context}=>{selinux_context} {path} ({_class}:{operation})={result}'
429+
)
430+
431+
self.insert_result(access_id, operation_id, result, None)
432+
278433
def _prepare_accesses(self):
279434
self.cur.execute(
280435
"""CREATE TABLE IF NOT EXISTS accesses (
@@ -341,9 +496,9 @@ def _prepare_accesses(self):
341496
fs.rowid,
342497
fs.parent,
343498
fs.name || '/' || child.name,
344-
fs.selinux_user,
345-
fs.selinux_role,
346-
fs.selinux_type,
499+
child.selinux_user,
500+
child.selinux_role,
501+
child.selinux_type,
347502
operation,
348503
reference_result,
349504
medusa_result

helpers.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
"""Helper functions."""
22
try:
3-
from selinux import selinux_check_access as check_access
3+
from selinux import (
4+
selinux_check_access as check_access,
5+
selabel_lookup,
6+
selabel_open,
7+
SELABEL_CTX_FILE,
8+
selinux_opt,
9+
selabel_close,
10+
context_free,
11+
getfilecon
12+
)
413
except ImportError:
514
pass
15+
import sys
616

717

818
def construct_selinux_context(
@@ -25,4 +35,28 @@ def selinux_check_access(scon, tcon, tclass, perm):
2535
ret = check_access(scon, tcon, tclass, perm)
2636
except PermissionError:
2737
return 0
38+
except TypeError:
39+
print('error:', scon, tcon, tclass, perm)
40+
sys.exit(-1)
2841
return code.get(ret, 0)
42+
43+
44+
def selinux_label_lookup(path: str, mode: int) -> str:
45+
"""
46+
:param path: Path of the file which to look up.
47+
:param mode: mode of `path as returned by lstat.
48+
`"""
49+
handle = selabel_open(SELABEL_CTX_FILE, None, 0)
50+
# context is a list
51+
try:
52+
context = selabel_lookup(handle, path, mode)
53+
except FileNotFoundError:
54+
# This is weird. Happens for paths such as `/proc/meminfo`. Let's try
55+
# again, this time getting the information right from the filesystem
56+
#
57+
# `context` is a list [return value, actual context]
58+
context = getfilecon(path)
59+
selabel_close(handle)
60+
return context[1]
61+
selabel_close(handle)
62+
return context[1]

0 commit comments

Comments
 (0)