# Copyright 2014-2016, Tresys Technology, LLC
#
# This file is part of SETools.
#
# SETools is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 2.1 of
# the License, or (at your option) any later version.
#
# SETools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with SETools. If not, see
# <http://www.gnu.org/licenses/>.
#
# pylint: disable=protected-access
import itertools
from . import exception
from . import qpol
from . import symbol
# qpol does not expose an equivalent of a sensitivity declaration.
# qpol_level_t is equivalent to the level declaration:
# level s0:c0.c1023;
# qpol_mls_level_t represents a level as used in contexts,
# such as range_transitions or labeling statements such as
# portcon and nodecon.
# Here qpol_level_t is also used for MLSSensitivity
# since it has the sensitivity name, dominance, and there
# is a 1:1 correspondence between the sensitivity declarations
# and level declarations.
# Hashing has to be handled below because the qpol references,
# normally used for a hash key, are not the same for multiple
# instances of the same object (except for level decl).
def enabled(policy):
"""Determine if MLS is enabled."""
return bool(policy.capability(qpol.QPOL_CAP_MLS))
def category_factory(policy, sym):
"""Factory function for creating MLS category objects."""
if not enabled(policy):
raise exception.MLSDisabled
if isinstance(sym, Category):
assert sym.policy == policy
return sym
elif isinstance(sym, qpol.qpol_cat_t):
if sym.isalias(policy):
raise TypeError("{0} is an alias".format(sym.name(policy)))
return Category(policy, sym)
try:
return Category(policy, qpol.qpol_cat_t(policy, str(sym)))
except ValueError:
raise exception.InvalidCategory("{0} is not a valid category".format(sym))
def sensitivity_factory(policy, sym):
"""Factory function for creating MLS sensitivity objects."""
if not enabled(policy):
raise exception.MLSDisabled
if isinstance(sym, Sensitivity):
assert sym.policy == policy
return sym
elif isinstance(sym, qpol.qpol_level_t):
if sym.isalias(policy):
raise TypeError("{0} is an alias".format(sym.name(policy)))
return Sensitivity(policy, sym)
try:
return Sensitivity(policy, qpol.qpol_level_t(policy, str(sym)))
except ValueError:
raise exception.InvalidSensitivity("{0} is not a valid sensitivity".format(sym))
def level_factory(policy, sym):
"""
Factory function for creating MLS level objects (e.g. levels used
in contexts of labeling statements)
"""
if not enabled(policy):
raise exception.MLSDisabled
if isinstance(sym, Level):
assert sym.policy == policy
return sym
elif isinstance(sym, qpol.qpol_mls_level_t):
return Level(policy, sym)
sens_split = str(sym).split(":")
sens = sens_split[0]
try:
semantic_level = qpol.qpol_semantic_level_t(policy, sens)
except ValueError:
raise exception.InvalidLevel("{0} is invalid ({1} is not a valid sensitivity)".
format(sym, sens))
try:
cats = sens_split[1]
except IndexError:
pass
else:
for group in cats.split(","):
catrange = group.split(".")
if len(catrange) == 2:
try:
semantic_level.add_cats(policy, catrange[0], catrange[1])
except ValueError:
raise exception.InvalidLevel(
"{0} is invalid ({1} is not a valid category range)".format(sym, group))
elif len(catrange) == 1:
try:
semantic_level.add_cats(policy, catrange[0], catrange[0])
except ValueError:
raise exception.InvalidLevel("{0} is invalid ({1} is not a valid category)".
format(sym, group))
else:
raise exception.InvalidLevel("{0} is invalid (level parsing error)".format(sym))
# convert to level object
try:
policy_level = qpol.qpol_mls_level_t(policy, semantic_level)
except ValueError:
raise exception.InvalidLevel(
"{0} is invalid (one or more categories are not associated with the sensitivity)".
format(sym))
return Level(policy, policy_level)
def level_decl_factory(policy, sym):
"""
Factory function for creating MLS level declaration objects.
(level statements) Lookups are only by sensitivity name.
"""
if not enabled(policy):
raise exception.MLSDisabled
if isinstance(sym, LevelDecl):
assert sym.policy == policy
return sym
elif isinstance(sym, qpol.qpol_level_t):
if sym.isalias(policy):
raise TypeError("{0} is an alias".format(sym.name(policy)))
return LevelDecl(policy, sym)
try:
return LevelDecl(policy, qpol.qpol_level_t(policy, str(sym)))
except ValueError:
raise exception.InvalidLevelDecl("{0} is not a valid sensitivity".format(sym))
def range_factory(policy, sym):
"""Factory function for creating MLS range objects."""
if not enabled(policy):
raise exception.MLSDisabled
if isinstance(sym, Range):
assert sym.policy == policy
return sym
elif isinstance(sym, qpol.qpol_mls_range_t):
return Range(policy, sym)
# build range:
levels = str(sym).split("-")
# strip() levels to handle ranges with spaces in them,
# e.g. s0:c1 - s0:c0.c255
try:
low = level_factory(policy, levels[0].strip())
except exception.InvalidLevel as ex:
raise exception.InvalidRange("{0} is not a valid range ({1}).".format(sym, ex))
try:
high = level_factory(policy, levels[1].strip())
except exception.InvalidLevel as ex:
raise exception.InvalidRange("{0} is not a valid range ({1}).".format(sym, ex))
except IndexError:
high = low
# convert to range object
try:
policy_range = qpol.qpol_mls_range_t(policy, low.qpol_symbol, high.qpol_symbol)
except ValueError:
raise exception.InvalidRange("{0} is not a valid range ({1} is not dominated by {2})".
format(sym, low, high))
return Range(policy, policy_range)
class BaseMLSComponent(symbol.PolicySymbol):
"""Base class for sensitivities and categories."""
@property
def _value(self):
"""
The value of the component.
This is a low-level policy detail exposed for internal use only.
"""
return self.qpol_symbol.value(self.policy)
def aliases(self):
"""Generator that yields all aliases for this category."""
for alias in self.qpol_symbol.alias_iter(self.policy):
yield alias
class Category(BaseMLSComponent):
"""An MLS category."""
def statement(self):
aliases = list(self.aliases())
stmt = "category {0}".format(self)
if aliases:
if len(aliases) > 1:
stmt += " alias {{ {0} }}".format(' '.join(aliases))
else:
stmt += " alias {0}".format(aliases[0])
stmt += ";"
return stmt
def __lt__(self, other):
"""Comparison based on their index instead of their names."""
return self._value < other._value
class Sensitivity(BaseMLSComponent):
"""An MLS sensitivity"""
def __ge__(self, other):
return self._value >= other._value
def __gt__(self, other):
return self._value > other._value
def __le__(self, other):
return self._value <= other._value
def __lt__(self, other):
return self._value < other._value
def statement(self):
aliases = list(self.aliases())
stmt = "sensitivity {0}".format(self)
if aliases:
if len(aliases) > 1:
stmt += " alias {{ {0} }}".format(' '.join(aliases))
else:
stmt += " alias {0}".format(aliases[0])
stmt += ";"
return stmt
class BaseMLSLevel(symbol.PolicySymbol):
"""Base class for MLS levels."""
def __str__(self):
lvl = str(self.sensitivity)
# sort by policy declaration order
cats = sorted(self.categories(), key=lambda k: k._value)
if cats:
# generate short category notation
shortlist = []
for _, i in itertools.groupby(cats, key=lambda k,
c=itertools.count(): k._value - next(c)):
group = list(i)
if len(group) > 1:
shortlist.append("{0}.{1}".format(group[0], group[-1]))
else:
shortlist.append(str(group[0]))
lvl += ":" + ','.join(shortlist)
return lvl
@property
def sensitivity(self):
raise NotImplementedError
def categories(self):
"""
Generator that yields all individual categories for this level.
All categories are yielded, not a compact notation such as
c0.c255
"""
for cat in self.qpol_symbol.cat_iter(self.policy):
yield category_factory(self.policy, cat)
class LevelDecl(BaseMLSLevel):
"""
The declaration statement for MLS levels, e.g:
level s7:c0.c1023;
"""
def __hash__(self):
return hash(self.sensitivity)
# below comparisons are only based on sensitivity
# dominance since, in this context, the allowable
# category set is being defined for the level.
# object type is asserted here because this cannot
# be compared to a Level instance.
def __eq__(self, other):
assert not isinstance(other, Level), "Levels cannot be compared to level declarations"
try:
return self.sensitivity == other.sensitivity
except AttributeError:
return str(self) == str(other)
def __ge__(self, other):
assert not isinstance(other, Level), "Levels cannot be compared to level declarations"
return self.sensitivity >= other.sensitivity
def __gt__(self, other):
assert not isinstance(other, Level), "Levels cannot be compared to level declarations"
return self.sensitivity > other.sensitivity
def __le__(self, other):
assert not isinstance(other, Level), "Levels cannot be compared to level declarations"
return self.sensitivity <= other.sensitivity
def __lt__(self, other):
assert not isinstance(other, Level), "Levels cannot be compared to level declarations"
return self.sensitivity < other.sensitivity
@property
def sensitivity(self):
"""The sensitivity of the level."""
# since the qpol symbol for levels is also used for
# MLSSensitivity objects, use self's qpol symbol
return sensitivity_factory(self.policy, self.qpol_symbol)
def statement(self):
return "level {0};".format(self)
class Level(BaseMLSLevel):
"""An MLS level used in contexts."""
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
try:
othercats = set(other.categories())
except AttributeError:
return str(self) == str(other)
else:
selfcats = set(self.categories())
return self.sensitivity == other.sensitivity and selfcats == othercats
def __ge__(self, other):
"""Dom operator."""
selfcats = set(self.categories())
othercats = set(other.categories())
return self.sensitivity >= other.sensitivity and selfcats >= othercats
def __gt__(self, other):
selfcats = set(self.categories())
othercats = set(other.categories())
return ((self.sensitivity > other.sensitivity and selfcats >= othercats) or
(self.sensitivity >= other.sensitivity and selfcats > othercats))
def __le__(self, other):
"""Domby operator."""
selfcats = set(self.categories())
othercats = set(other.categories())
return self.sensitivity <= other.sensitivity and selfcats <= othercats
def __lt__(self, other):
selfcats = set(self.categories())
othercats = set(other.categories())
return ((self.sensitivity < other.sensitivity and selfcats <= othercats) or
(self.sensitivity <= other.sensitivity and selfcats < othercats))
def __xor__(self, other):
"""Incomp operator."""
return not (self >= other or self <= other)
@property
def sensitivity(self):
"""The sensitivity of the level."""
return sensitivity_factory(self.policy, self.qpol_symbol.sens_name(self.policy))
def statement(self):
raise exception.NoStatement
class Range(symbol.PolicySymbol):
"""An MLS range"""
def __str__(self):
high = self.high
low = self.low
if high == low:
return str(low)
return "{0} - {1}".format(low, high)
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
try:
return self.low == other.low and self.high == other.high
except AttributeError:
# remove all spaces in the string representations
# to handle cases where the other object does not
# have spaces around the '-'
other_str = str(other).replace(" ", "")
self_str = str(self).replace(" ", "")
return self_str == other_str
def __contains__(self, other):
return self.low <= other <= self.high
@property
def high(self):
"""The high end/clearance level of this range."""
return level_factory(self.policy, self.qpol_symbol.high_level(self.policy))
@property
def low(self):
"""The low end/current level of this range."""
return level_factory(self.policy, self.qpol_symbol.low_level(self.policy))
def statement(self):
raise exception.NoStatement