From 28d0900918f413c210e05b104a1365cf1e5fb198 Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Wed, 1 Jul 2026 17:26:13 +0000 Subject: [PATCH 1/3] policyrep: Convert included preprocessor macros to anonymous enums. This tells Cython that they are not writable values. Avoids issues like this: 178762 | AF_INET6 = __Pyx_PyLong_As_int(o); if (unlikely((AF_INET6 == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 42, __pyx_L2_error) | ^ setools/policyrep.c:178765:22: error: lvalue required as left operand of assignment Signed-off-by: Chris PeBenito --- setools/policyrep.pyx | 27 +++- setools/policyrep/sepol.pxd | 306 +++++++++++++++++++----------------- 2 files changed, 184 insertions(+), 149 deletions(-) diff --git a/setools/policyrep.pyx b/setools/policyrep.pyx index b1be4d37..39cca8e3 100644 --- a/setools/policyrep.pyx +++ b/setools/policyrep.pyx @@ -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 @@ -44,15 +43,27 @@ cdef extern from "": cdef extern from "": ctypedef unsigned int socklen_t - cdef int AF_INET - cdef int AF_INET6 + enum: + AF_INET + AF_INET6 cdef extern from "": - 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 "": + enum: + S_IFBLK + S_IFCHR + S_IFDIR + S_IFIFO + S_IFREG + S_IFLNK + S_IFSOCK cdef extern from "": cdef const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) diff --git a/setools/policyrep/sepol.pxd b/setools/policyrep/sepol.pxd index 4db8fc3c..19191ba4 100644 --- a/setools/policyrep/sepol.pxd +++ b/setools/policyrep/sepol.pxd @@ -24,15 +24,17 @@ cdef extern from "": cdef extern from "": - cdef int SECURITY_FS_USE_XATTR - cdef int SECURITY_FS_USE_TRANS - cdef int SECURITY_FS_USE_TASK - cdef int SECURITY_FS_USE_GENFS - cdef int SECURITY_FS_USE_NONE + enum: + SECURITY_FS_USE_XATTR + SECURITY_FS_USE_TRANS + SECURITY_FS_USE_TASK + SECURITY_FS_USE_GENFS + SECURITY_FS_USE_NONE cdef extern from "": - cdef int SELINUX_MAGIC + enum: + SELINUX_MAGIC ctypedef char* sepol_security_context_t ctypedef uint32_t sepol_access_vector_t @@ -54,8 +56,9 @@ cdef extern from "": # # ebitmap_t # - cdef int MAPBIT - cdef int MAPSIZE + enum: + MAPBIT + MAPSIZE cdef struct ebitmap: ebitmap_node_t *node @@ -127,22 +130,23 @@ cdef extern from "": # # avtab_key_t # - cdef int AVTAB_ALLOWED - cdef int AVTAB_AUDITALLOW - cdef int AVTAB_AUDITDENY - cdef int AVTAB_NEVERALLOW - cdef int AVTAB_AV - cdef int AVTAB_TRANSITION - cdef int AVTAB_MEMBER - cdef int AVTAB_CHANGE - cdef int AVTAB_TYPE - cdef int AVTAB_XPERMS_ALLOWED - cdef int AVTAB_XPERMS_AUDITALLOW - cdef int AVTAB_XPERMS_DONTAUDIT - cdef int AVTAB_XPERMS_NEVERALLOW - cdef int AVTAB_XPERMS - cdef int AVTAB_ENABLED_OLD - cdef int AVTAB_ENABLED + enum: + AVTAB_ALLOWED + AVTAB_AUDITALLOW + AVTAB_AUDITDENY + AVTAB_NEVERALLOW + AVTAB_AV + AVTAB_TRANSITION + AVTAB_MEMBER + AVTAB_CHANGE + AVTAB_TYPE + AVTAB_XPERMS_ALLOWED + AVTAB_XPERMS_AUDITALLOW + AVTAB_XPERMS_DONTAUDIT + AVTAB_XPERMS_NEVERALLOW + AVTAB_XPERMS + AVTAB_ENABLED_OLD + AVTAB_ENABLED cdef struct avtab_key: uint16_t source_type @@ -155,9 +159,10 @@ cdef extern from "": # # avtab_extended_perms_t # - cdef int AVTAB_XPERMS_IOCTLFUNCTION - cdef int AVTAB_XPERMS_IOCTLDRIVER - cdef int AVTAB_XPERMS_NLMSG + enum: + AVTAB_XPERMS_IOCTLFUNCTION + AVTAB_XPERMS_IOCTLDRIVER + AVTAB_XPERMS_NLMSG cdef struct avtab_extended_perms: uint8_t specified @@ -271,10 +276,11 @@ cdef extern from "": # # sidtab_t # - cdef int SIDTAB_HASH_BITS - cdef int SIDTAB_HASH_BUCKETS - cdef int SIDTAB_HASH_MASK - cdef int SIDTAB_SIZE + enum: + SIDTAB_HASH_BITS + SIDTAB_HASH_BUCKETS + SIDTAB_HASH_MASK + SIDTAB_SIZE ctypedef struct sidtab_t: sidtab_ptr_t *htable @@ -292,8 +298,9 @@ cdef extern from "": #endif """ - cdef int COND_EXPR_MAXDEPTH - cdef int COND_MAX_BOOLS + enum: + COND_EXPR_MAXDEPTH + COND_MAX_BOOLS # # cond_av_list_t @@ -307,14 +314,15 @@ cdef extern from "": # # cond_expr_t # - cdef int COND_BOOL - cdef int COND_NOT - cdef int COND_OR - cdef int COND_AND - cdef int COND_XOR - cdef int COND_EQ - cdef int COND_NEQ - cdef int COND_LAST + enum: + COND_BOOL + COND_NOT + COND_OR + COND_AND + COND_XOR + COND_EQ + COND_NEQ + COND_LAST cdef struct cond_expr: uint32_t expr_type @@ -326,7 +334,8 @@ cdef extern from "": # # cond_node_t # - cdef int COND_NODE_FLAGS_TUNABLE + enum: + COND_NODE_FLAGS_TUNABLE cdef struct cond_node: int cur_state @@ -346,31 +355,33 @@ cdef extern from "": cdef extern from "": - cdef int CEXPR_NOT - cdef int CEXPR_AND - cdef int CEXPR_OR - cdef int CEXPR_ATTR - cdef int CEXPR_NAMES - cdef int CEXPR_USER - cdef int CEXPR_ROLE - cdef int CEXPR_TYPE - cdef int CEXPR_TARGET - cdef int CEXPR_XTARGET - cdef int CEXPR_L1L2 - cdef int CEXPR_L1H2 - cdef int CEXPR_H1L2 - cdef int CEXPR_H1H2 - cdef int CEXPR_L1H1 - cdef int CEXPR_L2H2 + enum: + CEXPR_NOT + CEXPR_AND + CEXPR_OR + CEXPR_ATTR + CEXPR_NAMES + CEXPR_USER + CEXPR_ROLE + CEXPR_TYPE + CEXPR_TARGET + CEXPR_XTARGET + CEXPR_L1L2 + CEXPR_L1H2 + CEXPR_H1L2 + CEXPR_H1H2 + CEXPR_L1H1 + CEXPR_L2H2 # # constraint_expr_t # - cdef int CEXPR_EQ - cdef int CEXPR_NEQ - cdef int CEXPR_DOM - cdef int CEXPR_DOMBY - cdef int CEXPR_INCOMP + enum: + CEXPR_EQ + CEXPR_NEQ + CEXPR_DOM + CEXPR_DOMBY + CEXPR_INCOMP cdef struct constraint_expr: uint32_t expr_type @@ -411,8 +422,9 @@ cdef extern from "": # # role_set_t # - cdef int ROLE_STAR - cdef int ROLE_COMP + enum: + ROLE_STAR + ROLE_COMP cdef struct role_set: ebitmap_t roles @@ -423,8 +435,9 @@ cdef extern from "": # # type_set_t # - cdef int TYPE_STAR - cdef int TYPE_COMP + enum: + TYPE_STAR + TYPE_COMP cdef struct type_set: ebitmap_t types @@ -436,10 +449,11 @@ cdef extern from "": # # av_extended_perms_t # - cdef int AVRULE_XPERMS_IOCTLFUNCTION - cdef int AVRULE_XPERMS_IOCTLDRIVER - cdef int AVRULE_XPERMS_NLMSG - cdef int EXTENDED_PERMS_LEN + enum: + AVRULE_XPERMS_IOCTLFUNCTION + AVRULE_XPERMS_IOCTLDRIVER + AVRULE_XPERMS_NLMSG + EXTENDED_PERMS_LEN cdef struct av_extended_perms: uint8_t specified @@ -453,22 +467,23 @@ cdef extern from "": # # avrule_t # - cdef int AVRULE_ALLOWED - cdef int AVRULE_AUDITALLOW - cdef int AVRULE_AUDITDENY - cdef int AVRULE_DONTAUDIT - cdef int AVRULE_NEVERALLOW - cdef int AVRULE_AV - cdef int AVRULE_TRANSITION - cdef int AVRULE_MEMBER - cdef int AVRULE_CHANGE - cdef int AVRULE_TYPE - cdef int AVRULE_XPERMS_ALLOWED - cdef int AVRULE_XPERMS_AUDITALLOW - cdef int AVRULE_XPERMS_DONTAUDIT - cdef int AVRULE_XPERMS_NEVERALLOW - cdef int AVRULE_XPERMS - cdef int RULE_SELF + enum: + AVRULE_ALLOWED + AVRULE_AUDITALLOW + AVRULE_AUDITDENY + AVRULE_DONTAUDIT + AVRULE_NEVERALLOW + AVRULE_AV + AVRULE_TRANSITION + AVRULE_MEMBER + AVRULE_CHANGE + AVRULE_TYPE + AVRULE_XPERMS_ALLOWED + AVRULE_XPERMS_AUDITALLOW + AVRULE_XPERMS_DONTAUDIT + AVRULE_XPERMS_NEVERALLOW + AVRULE_XPERMS + RULE_SELF cdef struct avrule: uint32_t specified @@ -505,15 +520,16 @@ cdef extern from "": # # class_datum_t # - cdef int DEFAULT_SOURCE - cdef int DEFAULT_TARGET - cdef int DEFAULT_SOURCE_LOW - cdef int DEFAULT_SOURCE_HIGH - cdef int DEFAULT_SOURCE_LOW_HIGH - cdef int DEFAULT_TARGET_LOW - cdef int DEFAULT_TARGET_HIGH - cdef int DEFAULT_TARGET_LOW_HIGH - cdef int DEFAULT_GLBLUB + enum: + DEFAULT_SOURCE + DEFAULT_TARGET + DEFAULT_SOURCE_LOW + DEFAULT_SOURCE_HIGH + DEFAULT_SOURCE_LOW_HIGH + DEFAULT_TARGET_LOW + DEFAULT_TARGET_HIGH + DEFAULT_TARGET_LOW_HIGH + DEFAULT_GLBLUB cdef struct class_datum: symtab_datum_t s @@ -532,7 +548,8 @@ cdef extern from "": # # cond_bool_datum_t # - cdef int COND_BOOL_FLAGS_TUNABLE + enum: + COND_BOOL_FLAGS_TUNABLE cdef struct cond_bool_datum: # Boolean data type symtab_datum_t s @@ -636,24 +653,25 @@ cdef extern from "": # # ocontext_t # - cdef int OCON_ISID - cdef int OCON_FS - cdef int OCON_PORT - cdef int OCON_NETIF - cdef int OCON_NODE - cdef int OCON_FSUSE - cdef int OCON_NODE6 - cdef int OCON_IBPKEY - cdef int OCON_IBENDPORT - - cdef int OCON_XEN_ISID - cdef int OCON_XEN_PIRQ - cdef int OCON_XEN_IOPORT - cdef int OCON_XEN_IOMEM - cdef int OCON_XEN_PCIDEVICE - cdef int OCON_XEN_DEVICETREE - - cdef int OCON_NUM + enum: + OCON_ISID + OCON_FS + OCON_PORT + OCON_NETIF + OCON_NODE + OCON_FSUSE + OCON_NODE6 + OCON_IBPKEY + OCON_IBENDPORT + OCON_NUM + + enum: + OCON_XEN_ISID + OCON_XEN_PIRQ + OCON_XEN_IOPORT + OCON_XEN_IOMEM + OCON_XEN_PCIDEVICE + OCON_XEN_DEVICETREE cdef struct ocontext: ocontext_u_union u @@ -705,8 +723,9 @@ cdef extern from "": # # role_datum_t # - cdef int ROLE_ROLE - cdef int ROLE_ATTRIB + enum: + ROLE_ROLE + ROLE_ATTRIB cdef struct role_datum: symtab_datum_t s @@ -746,13 +765,14 @@ cdef extern from "": # # type_datum_t # - cdef int TYPE_TYPE - cdef int TYPE_ATTRIB - cdef int TYPE_ALIAS - cdef int TYPE_FLAGS_PERMISSIVE - cdef int TYPE_FLAGS_EXPAND_ATTR_TRUE - cdef int TYPE_FLAGS_EXPAND_ATTR_FALSE - cdef int TYPE_FLAGS_EXPAND_ATTR + enum: + TYPE_TYPE + TYPE_ATTRIB + TYPE_ALIAS + TYPE_FLAGS_PERMISSIVE + TYPE_FLAGS_EXPAND_ATTR_TRUE + TYPE_FLAGS_EXPAND_ATTR_FALSE + TYPE_FLAGS_EXPAND_ATTR cdef struct type_datum: symtab_datum_t s @@ -782,18 +802,20 @@ cdef extern from "": # # Policy DB # - cdef int POLICYDB_VERSION_MAX - cdef int POLICYDB_VERSION_MIN - - cdef int SYM_COMMONS - cdef int SYM_CLASSES - cdef int SYM_ROLES - cdef int SYM_TYPES - cdef int SYM_USERS - cdef int SYM_BOOLS - cdef int SYM_LEVELS - cdef int SYM_CATS - cdef int SYM_NUM + enum: + POLICYDB_VERSION_MAX + POLICYDB_VERSION_MIN + + enum: + SYM_COMMONS + SYM_CLASSES + SYM_ROLES + SYM_TYPES + SYM_USERS + SYM_BOOLS + SYM_LEVELS + SYM_CATS + SYM_NUM cdef struct policydb: uint32_t policy_type @@ -846,11 +868,13 @@ cdef extern from "": ctypedef sepol_policydb sepol_policydb_t - cdef int SEPOL_DENY_UNKNOWN - cdef int SEPOL_REJECT_UNKNOWN - cdef int SEPOL_ALLOW_UNKNOWN - cdef int SEPOL_TARGET_SELINUX - cdef int SEPOL_TARGET_XEN + enum: + SEPOL_DENY_UNKNOWN + SEPOL_REJECT_UNKNOWN + SEPOL_ALLOW_UNKNOWN + SEPOL_TARGET_SELINUX + SEPOL_TARGET_XEN + cdef int sepol_policy_kern_vers_min() cdef int sepol_policy_kern_vers_max() From 7a70208413e5df19c1fb760d14b47be668eeb938 Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Wed, 1 Jul 2026 20:18:50 +0000 Subject: [PATCH 2/3] Add class to query file_contexts. Signed-off-by: Chris PeBenito --- setools/__init__.py | 5 +- setools/exception.py | 12 ++ setools/policyrep.pyi | 22 +++ setools/policyrep.pyx | 9 +- setools/policyrep/context.pxi | 38 +++++ setools/policyrep/filecontexts.pxi | 63 ++++++++ setools/policyrep/mls.pxi | 31 +++- setools/policyrep/selinux.pxd | 60 ++++++++ setools/policyrep/selinuxpolicy.pxi | 4 + tests/library/policyrep/file_contexts | 10 ++ tests/library/policyrep/test_filecontexts.py | 143 +++++++++++++++++++ tests/library/policyrep/test_mls.py | 135 +++++++++++++++++ 12 files changed, 521 insertions(+), 11 deletions(-) create mode 100644 setools/policyrep/filecontexts.pxi create mode 100644 tests/library/policyrep/file_contexts create mode 100644 tests/library/policyrep/test_filecontexts.py diff --git a/setools/__init__.py b/setools/__init__.py index 321440f7..5d330e48 100644 --- a/setools/__init__.py +++ b/setools/__init__.py @@ -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. diff --git a/setools/exception.py b/setools/exception.py index 434e169e..21f16977 100644 --- a/setools/exception.py +++ b/setools/exception.py @@ -267,6 +267,12 @@ class InvalidDefaultRange(InvalidSymbol): # # Other exceptions # +class InvalidContext(ValueError, PolicyrepException): + + """Exception for invalid contexts.""" + pass + + class NoCommon(AttributeError, PolicyrepException): """ @@ -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): """ diff --git a/setools/policyrep.pyi b/setools/policyrep.pyi index e44b7faa..6f66878e 100644 --- a/setools/policyrep.pyi +++ b/setools/policyrep.pyi @@ -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" = ... @@ -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"]: ... @@ -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": ... @@ -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 = ... diff --git a/setools/policyrep.pyx b/setools/policyrep.pyx index 39cca8e3..9e2d6762 100644 --- a/setools/policyrep.pyx +++ b/setools/policyrep.pyx @@ -24,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 "": int vasprintf(char **strp, const char *fmt, va_list ap) @@ -77,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" diff --git a/setools/policyrep/context.pxi b/setools/policyrep/context.pxi index 35d62a19..d4ffa6fc 100644 --- a/setools/policyrep/context.pxi +++ b/setools/policyrep/context.pxi @@ -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_}" diff --git a/setools/policyrep/filecontexts.pxi b/setools/policyrep/filecontexts.pxi new file mode 100644 index 00000000..51c4a76e --- /dev/null +++ b/setools/policyrep/filecontexts.pxi @@ -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 diff --git a/setools/policyrep/mls.pxi b/setools/policyrep/mls.pxi index 908fd7ae..e8ed056e 100644 --- a/setools/policyrep/mls.pxi +++ b/setools/policyrep/mls.pxi @@ -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 @@ -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 diff --git a/setools/policyrep/selinux.pxd b/setools/policyrep/selinux.pxd index c22e6ecf..0b0e2708 100644 --- a/setools/policyrep/selinux.pxd +++ b/setools/policyrep/selinux.pxd @@ -11,3 +11,63 @@ cdef extern from "": const char* selinux_current_policy_path() const char* selinux_binary_policy_path() char* selinux_boolean_sub(const char *boolean_name); + +cdef extern from "": + # + # selabel_handle + # + cdef struct selabel_handle: + pass + + # + # struct selinux_opt + # + enum: + SELABEL_OPT_UNUSED + SELABEL_OPT_VALIDATE + SELABEL_OPT_DIGEST + + cdef struct selinux_opt: + int type + const char *value + + # + # functions + # + enum: + SELABEL_CTX_FILE + SELABEL_CTX_MEDIA + SELABEL_CTX_X + SELABEL_CTX_DB + + enum: + SELABEL_OPT_PATH + SELABEL_OPT_BASEONLY + SELABEL_OPT_SUBSET + + enum: + SELABEL_X_PROP + SELABEL_X_SELN + SELABEL_X_EXT + SELABEL_X_EVENT + SELABEL_X_CLIENT + SELABEL_X_POLYPROP + SELABEL_X_POLYSELN + + enum: + SELABEL_DB_DATABASE + SELABEL_DB_SCHEMA + SELABEL_DB_TABLE + SELABEL_DB_COLUMN + SELABEL_DB_TUPLE + SELABEL_DB_PROCEDURE + SELABEL_DB_SEQUENCE + SELABEL_DB_BLOB + SELABEL_DB_VIEW + SELABEL_DB_LANGUAGE + SELABEL_DB_EXCEPTION + SELABEL_DB_DATATYPE + + selabel_handle* selabel_open(unsigned int backend, const selinux_opt * options, unsigned nopt) + int selabel_lookup_raw(selabel_handle * handle, char ** con, const char * key, int type) + void selabel_close(selabel_handle *hnd) diff --git a/setools/policyrep/selinuxpolicy.pxi b/setools/policyrep/selinuxpolicy.pxi index 2c7977ce..8a472dd6 100644 --- a/setools/policyrep/selinuxpolicy.pxi +++ b/setools/policyrep/selinuxpolicy.pxi @@ -367,6 +367,10 @@ cdef class SELinuxPolicy: raise InvalidCommon(f"{name} is not a valid common") + def lookup_context(self, ctx): + """Look up a context.""" + return Context.factory_from_string(self, ctx) + def lookup_level(self, level): """Look up a MLS level.""" return Level.factory_from_string(self, level) diff --git a/tests/library/policyrep/file_contexts b/tests/library/policyrep/file_contexts new file mode 100644 index 00000000..74672cc4 --- /dev/null +++ b/tests/library/policyrep/file_contexts @@ -0,0 +1,10 @@ +/usr/bin/test0 -- user0:object_r:type0:s4:c5 +/usr/lib/test1 -d user0:object_r:type1:s4:c5 +/usr/sbin/test2 -c user0:object_r:type2:s4:c5 +/usr/share/test3 -b user0:object_r:type3:s4:c5 +/usr/local/test4 -s user0:object_r:type4:s4:c5 +/usr/local/test5 -p user0:object_r:type5:s4:c5 +/usr/local/test6 -l user0:object_r:type6:s4:c5 +/var/run/test7 user0:object_r:type7:s4:c5 +/opt/test_regex(/.*)? user0:object_r:type8:s4:c5 +/tmp/no_match <> diff --git a/tests/library/policyrep/test_filecontexts.py b/tests/library/policyrep/test_filecontexts.py new file mode 100644 index 00000000..d809e319 --- /dev/null +++ b/tests/library/policyrep/test_filecontexts.py @@ -0,0 +1,143 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +import pytest +import setools + + +@pytest.mark.obj_args("tests/library/policyrep/selinuxpolicy.conf") +class TestFileContexts: + + """Tests for FileContexts class.""" + + def test_lookup_any(self, compiled_policy): + """FileContexts: lookup with default (any) filetype.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert "user0:object_r:type7:s4:c5" == str(ctx) + + def test_lookup_file(self, compiled_policy): + """FileContexts: lookup with regular file type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/bin/test0", setools.FileContextsFiletype.file) + assert "user0:object_r:type0:s4:c5" == str(ctx) + + def test_lookup_dir(self, compiled_policy): + """FileContexts: lookup with directory type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/lib/test1", setools.FileContextsFiletype.dir) + assert "user0:object_r:type1:s4:c5" == str(ctx) + + def test_lookup_chr_file(self, compiled_policy): + """FileContexts: lookup with character device type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/sbin/test2", setools.FileContextsFiletype.chr_file) + assert "user0:object_r:type2:s4:c5" == str(ctx) + + def test_lookup_blk_file(self, compiled_policy): + """FileContexts: lookup with block device type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/share/test3", setools.FileContextsFiletype.blk_file) + assert "user0:object_r:type3:s4:c5" == str(ctx) + + def test_lookup_sock_file(self, compiled_policy): + """FileContexts: lookup with socket type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/local/test4", setools.FileContextsFiletype.sock_file) + assert "user0:object_r:type4:s4:c5" == str(ctx) + + def test_lookup_fifo_file(self, compiled_policy): + """FileContexts: lookup with named pipe type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/local/test5", setools.FileContextsFiletype.fifo_file) + assert "user0:object_r:type5:s4:c5" == str(ctx) + + def test_lookup_lnk_file(self, compiled_policy): + """FileContexts: lookup with symbolic link type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/local/test6", setools.FileContextsFiletype.lnk_file) + assert "user0:object_r:type6:s4:c5" == str(ctx) + + def test_lookup_regex(self, compiled_policy): + """FileContexts: lookup with regex path.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/opt/test_regex/subdir/file") + assert "user0:object_r:type8:s4:c5" == str(ctx) + + def test_lookup_no_match(self, compiled_policy): + """FileContexts: lookup raises NoFileContextsMatch for unmatched path.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + with pytest.raises(setools.exception.NoFileContextsMatch): + fc.lookup("/nonexistent/path/that/does/not/match") + + def test_path_attribute(self, compiled_policy): + """FileContexts: path attribute is set correctly.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + assert fc.path == "tests/library/policyrep/file_contexts" + + def test_policy_attribute(self, compiled_policy): + """FileContexts: policy attribute is set correctly.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + assert fc.policy is compiled_policy + + def test_context_user(self, compiled_policy): + """FileContexts: returned context has correct user.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert isinstance(ctx, setools.Context) + assert "user0" == str(ctx.user) + + def test_context_role(self, compiled_policy): + """FileContexts: returned context has correct role.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert isinstance(ctx, setools.Context) + assert "object_r" == str(ctx.role) + + def test_context_type(self, compiled_policy): + """FileContexts: returned context has correct type.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert isinstance(ctx, setools.Context) + assert "type7" == str(ctx.type_) + + def test_context_range(self, compiled_policy): + """FileContexts: returned context has correct MLS range.""" + fc = setools.FileContexts(compiled_policy, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert isinstance(ctx, setools.Context) + assert "s4:c5" == str(ctx.range_) + + def test_open_nonexistent_file(self, compiled_policy): + """FileContexts: OSError raised for nonexistent file_contexts path.""" + with pytest.raises(OSError): + setools.FileContexts(compiled_policy, "/nonexistent/path/file_contexts") + + +class TestFileContextsNoPolicy: + + """Tests for FileContexts when no policy is provided.""" + + def test_attribute(self): + """FileContexts: policy attribute is None when no policy provided.""" + fc = setools.FileContexts(None, "tests/library/policyrep/file_contexts") + assert fc.policy is None + + def test_lookup(self): + """FileContexts: lookup returns raw string when policy is None.""" + fc = setools.FileContexts(None, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/var/run/test7") + assert ctx == "user0:object_r:type7:s4:c5" + assert isinstance(ctx, str) + + def test_lookup_file(self): + """FileContexts: lookup with filetype returns raw string when policy is None.""" + fc = setools.FileContexts(None, "tests/library/policyrep/file_contexts") + ctx = fc.lookup("/usr/bin/test0", setools.FileContextsFiletype.file) + assert ctx == "user0:object_r:type0:s4:c5" + assert isinstance(ctx, str) + + def test_lookup_no_match(self): + """FileContexts: lookup raises NoFileContextsMatch when policy is None.""" + fc = setools.FileContexts(None, "tests/library/policyrep/file_contexts") + with pytest.raises(setools.exception.NoFileContextsMatch): + fc.lookup("/nonexistent/path/that/does/not/match") diff --git a/tests/library/policyrep/test_mls.py b/tests/library/policyrep/test_mls.py index 5cedc8d9..970fd9ab 100644 --- a/tests/library/policyrep/test_mls.py +++ b/tests/library/policyrep/test_mls.py @@ -368,6 +368,141 @@ def test_contains(self, compiled_policy: setools.SELinuxPolicy) -> None: level5 = compiled_policy.lookup_level("s2:c0.c11") assert not (level5 in rangeobj) + def test_dom(self, compiled_policy: setools.SELinuxPolicy) -> None: + """Range dominate (ge).""" + # equal + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 >= range2 + + # dominates (wider low) + range1 = compiled_policy.lookup_range("s0-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 >= range2 + + # dominates (wider high) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 >= range2 + + # dominated by + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c11") + assert not (range1 >= range2) + + # incomp + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s1:c5-s2:c0.c11") + assert not (range1 >= range2) + + def test_domby(self, compiled_policy: setools.SELinuxPolicy) -> None: + """Range dominated-by (le).""" + # equal + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 <= range2 + + # dominated by (narrower) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c11") + assert range1 <= range2 + + # dominates (wider) + range1 = compiled_policy.lookup_range("s0-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 <= range2) + + # incomp + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s1:c5-s2:c0.c11") + assert not (range1 <= range2) + + def test_proper_dom(self, compiled_policy: setools.SELinuxPolicy) -> None: + """Range proper dominate (gt).""" + # equal + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 > range2) + + # strictly dominates (wider low) + range1 = compiled_policy.lookup_range("s0-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 > range2 + + # strictly dominates (wider high) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 > range2 + + # strictly dominates (wider both) + range1 = compiled_policy.lookup_range("s0-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert range1 > range2 + + # dominated by + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c11") + assert not (range1 > range2) + + # incomp + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s1:c5-s2:c0.c11") + assert not (range1 > range2) + + def test_proper_domby(self, compiled_policy: setools.SELinuxPolicy) -> None: + """Range proper dominated-by (lt).""" + # equal + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 < range2) + + # strictly dominated by (narrower low) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c10") + assert range1 < range2 + + # strictly dominated by (narrower high) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c11") + assert range1 < range2 + + # strictly dominated by (narrower both) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c11") + assert range1 < range2 + + # dominates + range1 = compiled_policy.lookup_range("s0-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 < range2) + + # incomp + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s1:c5-s2:c0.c11") + assert not (range1 < range2) + + def test_incomp(self, compiled_policy: setools.SELinuxPolicy) -> None: + """Range incomparable (xor).""" + # equal + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 ^ range2) + + # dominates + range1 = compiled_policy.lookup_range("s0-s2:c0.c11") + range2 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + assert not (range1 ^ range2) + + # dominated by + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s0-s2:c0.c11") + assert not (range1 ^ range2) + + # incomp (overlapping but neither contains the other) + range1 = compiled_policy.lookup_range("s0:c1-s2:c0.c10") + range2 = compiled_policy.lookup_range("s1:c5-s2:c0.c11") + assert range1 ^ range2 + def test_range_statement(self, compiled_policy: setools.SELinuxPolicy) -> None: """Range has no statement.""" rangeobj = compiled_policy.lookup_range("s0") From eb4cb11d82bf8f2d2a5f608525d66deecdaee8c5 Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Thu, 2 Jul 2026 13:03:59 +0000 Subject: [PATCH 3/3] SEToolsMCPServer: Add tool for file_context lookups. Signed-off-by: Chris PeBenito --- setools/mcp/server.py | 49 ++++++++++++++++++++++++++++++-- tests/library/mcp/test_server.py | 36 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/setools/mcp/server.py b/setools/mcp/server.py index 5e8b4961..e4e2978b 100644 --- a/setools/mcp/server.py +++ b/setools/mcp/server.py @@ -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, @@ -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. @@ -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. @@ -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." ), ) @@ -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*. @@ -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) diff --git a/tests/library/mcp/test_server.py b/tests/library/mcp/test_server.py index 2f8cbd42..14a721d0 100644 --- a/tests/library/mcp/test_server.py +++ b/tests/library/mcp/test_server.py @@ -22,6 +22,7 @@ DTA_POLICY = "tests/library/dta.conf" DIFF_LEFT_POLICY = "tests/library/diff_left.conf" DIFF_RIGHT_POLICY = "tests/library/diff_right.conf" +FILE_CONTEXTS = "tests/library/policyrep/file_contexts" @pytest.fixture(scope="class") @@ -655,3 +656,38 @@ def test_portcons_component(self, pc = data["result"]["portcons"] for key in ("added", "removed"): assert key in pc + + +@pytest.mark.obj_args(SELINUX_POLICY) +class TestLookupFileContext: + def test_lookup_default_filetype(self, mcp_server: SEToolsMCPServer) -> None: + data = assert_payload(mcp_server.setools_lookup_file_context( + path="/var/run/test7", fc_path=FILE_CONTEXTS)) + assert data["count"] == 1 + assert data["result"] == "user0:object_r:type7:s4:c5" + + def test_lookup_file_filetype(self, mcp_server: SEToolsMCPServer) -> None: + data = assert_payload(mcp_server.setools_lookup_file_context( + path="/usr/bin/test0", filetype="file", fc_path=FILE_CONTEXTS)) + assert data["result"] == "user0:object_r:type0:s4:c5" + + def test_lookup_dir_filetype(self, mcp_server: SEToolsMCPServer) -> None: + data = assert_payload(mcp_server.setools_lookup_file_context( + path="/usr/lib/test1", filetype="dir", fc_path=FILE_CONTEXTS)) + assert data["result"] == "user0:object_r:type1:s4:c5" + + def test_lookup_regex_match(self, mcp_server: SEToolsMCPServer) -> None: + data = assert_payload(mcp_server.setools_lookup_file_context( + path="/opt/test_regex/subdir/file", fc_path=FILE_CONTEXTS)) + assert data["result"] == "user0:object_r:type8:s4:c5" + + def test_lookup_no_match_raises(self, mcp_server: SEToolsMCPServer) -> None: + from setools.exception import NoFileContextsMatch + with pytest.raises(NoFileContextsMatch): + mcp_server.setools_lookup_file_context( + path="/nonexistent/path/that/does/not/match", fc_path=FILE_CONTEXTS) + + def test_lookup_nonexistent_fc_path_raises(self, mcp_server: SEToolsMCPServer) -> None: + with pytest.raises(OSError): + mcp_server.setools_lookup_file_context( + path="/some/path", fc_path="/nonexistent/file_contexts")