Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions setools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

# Python classes for policy representation
from .policyrep import SELinuxPolicy, BoundsRuletype, ConstraintRuletype, DefaultRuletype, \
DefaultRangeValue, DefaultValue, FSUseRuletype, HandleUnknown, IbpkeyconRange, MLSRuletype, \
NodeconIPVersion, PolicyTarget, PortconProtocol, RBACRuletype, TERuletype
DefaultRangeValue, DefaultValue, FileContexts, FileContextsFiletype, FSUseRuletype, \
HandleUnknown, IbpkeyconRange, MLSRuletype, NodeconIPVersion, PolicyTarget, PortconProtocol, \
RBACRuletype, TERuletype

# Policy representation classes for type checking purposes. Few can be instantiated
# outside of this library.
Expand Down
12 changes: 12 additions & 0 deletions setools/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ class InvalidDefaultRange(InvalidSymbol):
#
# Other exceptions
#
class InvalidContext(ValueError, PolicyrepException):

"""Exception for invalid contexts."""
pass


class NoCommon(AttributeError, PolicyrepException):

"""
Expand All @@ -281,6 +287,12 @@ class NoDefaults(InvalidSymbol):
pass


class NoFileContextsMatch(ValueError):

"""Exception when a path does not match any file context entries."""
pass


class RuleNotConditional(AttributeError, PolicyrepException):

"""
Expand Down
49 changes: 47 additions & 2 deletions setools/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from .. import (BoolQuery, BoundsQuery, BoundsRuletype, CategoryQuery, CommonQuery,
ConstraintQuery, ConstraintRuletype, DefaultQuery, DefaultRuletype,
DevicetreeconQuery, DomainTransitionAnalysis, FSUseQuery, FSUseRuletype,
DevicetreeconQuery, DomainTransitionAnalysis, FileContexts,
FileContextsFiletype, FSUseQuery, FSUseRuletype,
GenfsconQuery, IbendportconQuery, IbpkeyconQuery, IbpkeyconRange,
InfoFlowAnalysis, InitialSIDQuery, IomemconQuery, IomemconRange,
IoportconQuery, IoportconRange, MLSRuleQuery, MLSRuletype, NetifconQuery,
Expand Down Expand Up @@ -52,6 +53,13 @@ def __missing__(self, key: str | None) -> SELinuxPolicy:
return self[key]


class FileContextsCache(dict):
"""Simple cache for loaded file_contexts"""
def __missing__(self, key: str | None) -> FileContexts:
self[key] = FileContexts(None, key)
return self[key]


class SEToolsMCPServer:
"""
MCP server encapsulating all setools policy analysis tools.
Expand All @@ -64,6 +72,7 @@ def __init__(self, default_policy: str | None = None) -> None:
self.log: logging.Logger = logging.getLogger(__name__)
self.default_policy: str | None = default_policy
self._policy_cache: PolicyCache = PolicyCache()
self._fc_cache: FileContextsCache = FileContextsCache()

try:
# Init the policy cache. Load the default policy as the None key and its path.
Expand All @@ -79,7 +88,8 @@ def __init__(self, default_policy: str | None = None) -> None:
instructions=(
"SELinux policy analysis tools built on the setools library. "
"Supports querying TE/RBAC/MLS rules, enumerating policy components, "
"domain transition analysis, information flow analysis, and policy diffing."
"domain transition analysis, information flow analysis, policy diffing,"
"and file_context lookup."
),
)

Expand Down Expand Up @@ -112,6 +122,13 @@ def _collect_results(query: PolicyQuery, *, max_results: int = 32768) -> str:
returned_count,
truncated)

def _load_file_contexts(self, fc_path: str | None = None) -> FileContexts:
"""
Return a (cached) FileContexts for file_contexts at path *fc_path*.
If *fc_path* is None, uses the system default file_contexts.
"""
return self._fc_cache[fc_path]

def _load_policy(self, policy: str | None = None) -> SELinuxPolicy:
"""
Return a (cached) SELinuxPolicy for policy at path *policy*.
Expand Down Expand Up @@ -1223,3 +1240,31 @@ def _cap(items: Any, limit: int) -> tuple[list[Any], bool]:
}

return self._serialize_results(differences, count, any_truncated)

def setools_lookup_file_context(
self,
path: Annotated[str, "The file path to look up in the file_contexts."],
filetype: Annotated[
str | None,
"File type to match. Valid values: any, file, dir, chr_file, blk_file, "
"sock_file, fifo_file, lnk_file. If omitted, defaults to 'any'.",
] = None,
fc_path: Annotated[
str | None,
"Path to a file_contexts file. If omitted, uses the system default.",
] = None,
) -> str:
"""
Look up the SELinux file context for a given path.

Returns the security context that would be assigned to the specified
path according to the file_contexts configuration.

Use this instead of a naive text search, as it properly evaluates the
file context rules, including regex precedence and file type specifiers.
"""
ft = FileContextsFiletype[filetype] if filetype else FileContextsFiletype.any

fc = self._load_file_contexts(fc_path)
result = fc.lookup(path, ft)
return self._serialize_results(result, 1, False)
22 changes: 22 additions & 0 deletions setools/policyrep.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ class DefaultValue(PolicyEnum):
class Devicetreecon(Ocontext):
path: str = ...

class FileContexts:
policy: "SELinuxPolicy | None" = ...
path: str = ...
def __init__(self, policy: "SELinuxPolicy | None", fc_path: str | None = None) -> None: ...
def lookup(self, path: str, filetype: FileContextsFiletype = FileContextsFiletype.any) -> "Context | str": ...

class FileContextsFiletype(PolicyEnum):
any = ...
dir = ...
chr_file = ...
blk_file = ...
sock_file = ...
fifo_file = ...
lnk_file = ...
file = ...

class FSUse(Ocontext):
fs: str = ...
ruletype: "FSUseRuletype" = ...
Expand Down Expand Up @@ -342,9 +358,13 @@ class Range(PolicyObject):
high: Level = ...
low: Level = ...
def __contains__(self, other) -> bool: ...
def __xor__(self, other) -> bool: ...
def dom(self, other) -> bool: ...
def domby(self, other) -> bool: ...

class Role(PolicySymbol):
dominated_roles: frozenset["Role"] = ...
def __contains__(self, other) -> bool: ...
def expand(self) -> Iterable["Role"]: ...
def types(self) -> Iterable["Type"]: ...

Expand Down Expand Up @@ -438,6 +458,7 @@ class SELinuxPolicy:
def lookup_category(self, name: "Category" | str, deref: bool = True) -> "Category": ...
def lookup_class(self, name: "ObjClass" | str) -> "ObjClass": ...
def lookup_common(self, name: "Common" | str) -> "Common": ...
def lookup_context(self, ctx: "Context" | str) -> "Context": ...
def lookup_initialsid(self, name: "InitialSID" | str) -> "InitialSID": ...
def lookup_level(self, name: "Level" | str) -> "Level": ...
def lookup_range(self, name: "Range" | str) -> "Range": ...
Expand Down Expand Up @@ -509,6 +530,7 @@ class User(PolicySymbol):
mls_level: "Level" = ...
mls_range: "Range" = ...
roles: frozenset["Role"] = ...
def __contains__(self, other) -> bool: ...

class Validatetrans(BaseConstraint):
perms: NoReturn = ...
36 changes: 21 additions & 15 deletions setools/policyrep.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, uintptr_t
from libc.stdio cimport FILE, fopen, fclose, snprintf
from libc.stdlib cimport calloc, free
from libc.string cimport memcpy, memset, strerror
from posix.stat cimport S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFREG, S_IFLNK, S_IFSOCK

import dataclasses
import logging
Expand All @@ -25,13 +24,7 @@ from typing import TypeVar, Union
cimport sepol
cimport selinux

from .exception import InvalidPolicy, MLSDisabled, InvalidBoolean, InvalidCategory, InvalidClass, \
InvalidCommon, InvalidInitialSid, InvalidLevel, InvalidLevelDecl, InvalidRange, InvalidRole, \
InvalidSensitivity, InvalidType, InvalidUser, InvalidRuleType, InvalidBoundsType, \
InvalidConstraintType, InvalidDefaultType, InvalidFSUseType, InvalidMLSRuleType, \
InvalidRBACRuleType, InvalidTERuleType, SymbolUseError, RuleUseError, ConstraintUseError, \
NoStatement, InvalidDefaultValue, InvalidDefaultRange, NoCommon, NoDefaults, \
RuleNotConditional, TERuleNoFilename, LowLevelPolicyError
from .exception import *

cdef extern from "<stdio.h>":
int vasprintf(char **strp, const char *fmt, va_list ap)
Expand All @@ -44,15 +37,27 @@ cdef extern from "<stdarg.h>":

cdef extern from "<sys/socket.h>":
ctypedef unsigned int socklen_t
cdef int AF_INET
cdef int AF_INET6
enum:
AF_INET
AF_INET6

cdef extern from "<netinet/in.h>":
cdef int INET6_ADDRSTRLEN
cdef int IPPROTO_DCCP
cdef int IPPROTO_SCTP
cdef int IPPROTO_TCP
cdef int IPPROTO_UDP
enum:
INET6_ADDRSTRLEN
IPPROTO_DCCP
IPPROTO_SCTP
IPPROTO_TCP
IPPROTO_UDP

cdef extern from "<sys/stat.h>":
enum:
S_IFBLK
S_IFCHR
S_IFDIR
S_IFIFO
S_IFREG
S_IFLNK
S_IFSOCK

cdef extern from "<arpa/inet.h>":
cdef const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)
Expand All @@ -66,6 +71,7 @@ include "bounds.pxi"
include "constraint.pxi"
include "context.pxi"
include "default.pxi"
include "filecontexts.pxi"
include "fscontext.pxi"
include "initsid.pxi"
include "mls.pxi"
Expand Down
38 changes: 38 additions & 0 deletions setools/policyrep/context.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,44 @@ cdef class Context(PolicyObject):

return c

@staticmethod
cdef inline Context factory_from_string(SELinuxPolicy policy, str ctx):
"""Factory function for creating Context objects from a string."""
cdef:
Context c = Context.__new__(Context)
list items = ctx.split(":", maxsplit=3)

try:
c.user = policy.lookup_user(items[0])
c.role = policy.lookup_role(items[1])
c.type_ = policy.lookup_type(items[2])

# object_r is a special case: it is implicitly associated with
# all users and types.
if c.role != "object_r":
if c.role not in c.user.roles:
raise InvalidContext(
f"{ctx} is invalid: Role {c.role} is not associated to user {c.user}.")
if c.type_ not in tuple(c.role.types()):
raise InvalidContext(
f"{ctx} is invalid: Type {c.type_} is not associated to role {c.role}.")

if policy.mls:
c._range = policy.lookup_range(items[3])
if not c._range <= c.user.mls_range:
raise InvalidContext(
f"{ctx} is invalid: Range {c._range} not in user {c.user}'s "
f"allowed range {c.user.mls_range}")

except IndexError as ex:
raise InvalidContext("f{ctx} is invalid: Context is incomplete.") from ex

except InvalidSymbol as ex:
raise InvalidContext(f"{ctx} is invalid: {ex}") from ex

c.policy = policy
return c

def __str__(self):
if self._range:
return f"{self.user}:{self.role}:{self.type_}:{self.range_}"
Expand Down
63 changes: 63 additions & 0 deletions setools/policyrep/filecontexts.pxi
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: LGPL-2.1-only

class FileContextsFiletype(PolicyEnum):

"""Enumeration of file types in FileContexts."""

any = 0
dir = 1
chr_file = 2
blk_file = 3
sock_file = 4
fifo_file = 5
lnk_file = 6
file = 7


cdef class FileContexts:
cdef:
selinux.selabel_handle *handle

readonly SELinuxPolicy policy
readonly str path

def __cinit__(self, policy: SELinuxPolicy | None, fc_path: str | None = None):
"""
Parameter:
policy Policy to use for context lookups. If None, lookup()
will return the raw context string.
fc_path Path to a file_contexts to open. If not specified, the
system's file_contexts will be used.
"""

cdef selinux.selinux_opt selabel_opt
if fc_path:
selabel_opt.type = selinux.SELABEL_OPT_PATH
selabel_opt.value = fc_path
else:
selabel_opt.type = selinux.SELABEL_OPT_UNUSED

self.handle = selinux.selabel_open(selinux.SELABEL_CTX_FILE, &selabel_opt, 1)
if self.handle == NULL:
if errno == ENOMEM:
PyErr_NoMemory()
else:
PyErr_SetFromErrnoWithFilename(OSError, fc_path)

self.path = fc_path
self.policy = policy

def __dealloc__(self):
if self.handle != NULL:
selinux.selabel_close(self.handle)

def lookup(self, path: str, filetype: FileContextsFiletype = FileContextsFiletype.any):
"""Look up a path in the file_contexts."""
cdef char *ctx
if selinux.selabel_lookup_raw(self.handle, &ctx, path, filetype.value) < 0:
if errno == ENOENT:
raise NoFileContextsMatch(f"\"{path}\" ({filetype}) does not match.")
else:
PyErr_SetFromErrno(OSError)

return self.policy.lookup_context(ctx) if self.policy else ctx
31 changes: 29 additions & 2 deletions setools/policyrep/mls.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,9 @@ cdef class Range(PolicyObject):
def __hash__(self):
return hash(str(self))

def __contains__(self, other):
return self.low <= other <= self.high

def __eq__(self, other):
try:
return self.low == other.low and self.high == other.high
Expand All @@ -450,8 +453,32 @@ cdef class Range(PolicyObject):
self_str = str(self).replace(" ", "")
return self_str == other_str

def __contains__(self, other):
return self.low <= other <= self.high
def __ge__(self, other):
return self.low <= other.low and self.high >= other.high

def __gt__(self, other):
return (self.low <= other.low and self.high > other.high) \
or (self.low < other.low and self.high >= other.high)

def __le__(self, other):
return other.low <= self.low <= other.high \
and other.low <= self.high <= other.high

def __lt__(self, other):
return (other.low <= self.low and other.high > self.high) \
or (other.low < self.low and other.high >= self.high)

def __xor__(self, other):
# Incomp operator
return not (self >= other or self <= other)

def dom(self, other):
"""Returns if self dominates other."""
return self >= other

def domby(self, other):
"""Returns if self is dominated by other."""
return self <= other

def statement(self):
raise NoStatement
Expand Down
Loading