454 lines
16 KiB
Python
Executable File
454 lines
16 KiB
Python
Executable File
# Copyright 2014-2015, 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/>.
|
|
#
|
|
import sys
|
|
import logging
|
|
import copy
|
|
from collections import OrderedDict
|
|
from errno import ENOENT
|
|
|
|
from . import exception
|
|
from . import policyrep
|
|
from .descriptors import PermissionMapDescriptor
|
|
|
|
infoflow_directions = ["r", "w", "b", "n", "u"]
|
|
min_weight = 1
|
|
max_weight = 10
|
|
|
|
|
|
class PermissionMap(object):
|
|
|
|
"""Permission Map for information flow analysis."""
|
|
|
|
def __init__(self, permmapfile=None):
|
|
"""
|
|
Parameter:
|
|
permmapfile The path to the permission map to load.
|
|
"""
|
|
self.log = logging.getLogger(__name__)
|
|
self.permmap = OrderedDict()
|
|
self.permmapfile = None
|
|
|
|
if permmapfile:
|
|
self.load(permmapfile)
|
|
else:
|
|
for path in ["data/", sys.prefix + "/share/setools/"]:
|
|
try:
|
|
self.load(path + "perm_map")
|
|
break
|
|
except (IOError, OSError) as err:
|
|
if err.errno != ENOENT:
|
|
raise
|
|
else:
|
|
raise RuntimeError("Unable to load default permission map.")
|
|
|
|
def __str__(self):
|
|
return self.permmapfile
|
|
|
|
def __deepcopy__(self, memo):
|
|
newobj = PermissionMap.__new__(PermissionMap)
|
|
newobj.log = self.log
|
|
newobj.permmap = copy.deepcopy(self.permmap)
|
|
newobj.permmapfile = self.permmapfile
|
|
memo[id(self)] = newobj
|
|
return newobj
|
|
|
|
def load(self, permmapfile):
|
|
"""
|
|
Parameter:
|
|
permmapfile The path to the permission map to load.
|
|
"""
|
|
self.log.info("Opening permission map \"{0}\"".format(permmapfile))
|
|
|
|
# state machine
|
|
# 1 = read number of classes
|
|
# 2 = read class name and number of perms
|
|
# 3 = read perms
|
|
with open(permmapfile, "r") as mapfile:
|
|
total_perms = 0
|
|
class_count = 0
|
|
num_classes = 0
|
|
state = 1
|
|
|
|
self.permmap.clear()
|
|
|
|
for line_num, line in enumerate(mapfile, start=1):
|
|
entry = line.split()
|
|
|
|
if len(entry) == 0 or entry[0][0] == '#':
|
|
continue
|
|
|
|
if state == 1:
|
|
try:
|
|
num_classes = int(entry[0])
|
|
except ValueError:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Invalid number of classes: {2}".
|
|
format(permmapfile, line_num, entry[0]))
|
|
|
|
if num_classes < 1:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Number of classes must be positive: {2}".
|
|
format(permmapfile, line_num, entry[0]))
|
|
|
|
state = 2
|
|
|
|
elif state == 2:
|
|
if len(entry) != 3 or entry[0] != "class":
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Invalid class declaration: {2}".
|
|
format(permmapfile, line_num, entry))
|
|
|
|
class_name = str(entry[1])
|
|
|
|
try:
|
|
num_perms = int(entry[2])
|
|
except ValueError:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Invalid number of permissions: {2}".
|
|
format(permmapfile, line_num, entry[2]))
|
|
|
|
if num_perms < 1:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Number of permissions must be positive: {2}".
|
|
format(permmapfile, line_num, entry[2]))
|
|
|
|
class_count += 1
|
|
if class_count > num_classes:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Extra class found: {2}".
|
|
format(permmapfile, line_num, class_name))
|
|
|
|
self.permmap[class_name] = OrderedDict()
|
|
perm_count = 0
|
|
state = 3
|
|
|
|
elif state == 3:
|
|
perm_name = str(entry[0])
|
|
|
|
flow_direction = str(entry[1])
|
|
if flow_direction not in infoflow_directions:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Invalid information flow direction: {2}".
|
|
format(permmapfile, line_num, entry[1]))
|
|
|
|
try:
|
|
weight = int(entry[2])
|
|
except ValueError:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Invalid permission weight: {2}".
|
|
format(permmapfile, line_num, entry[2]))
|
|
|
|
if not min_weight <= weight <= max_weight:
|
|
raise exception.PermissionMapParseError(
|
|
"{0}:{1}:Permission weight must be {3}-{4}: {2}".
|
|
format(permmapfile, line_num, entry[2],
|
|
min_weight, max_weight))
|
|
|
|
self.log.debug("Read {0}:{1} {2} {3}".format(
|
|
class_name, perm_name, flow_direction, weight))
|
|
|
|
if flow_direction == 'u':
|
|
self.log.info("Permission {0}:{1} is unmapped.".format(
|
|
class_name, perm_name))
|
|
|
|
mapping = Mapping(self.permmap, class_name, perm_name, create=True)
|
|
mapping.direction = flow_direction
|
|
mapping.weight = weight
|
|
|
|
total_perms += 1
|
|
perm_count += 1
|
|
if perm_count >= num_perms:
|
|
state = 2
|
|
|
|
self.permmapfile = permmapfile
|
|
self.log.info("Successfully opened permission map \"{0}\"".format(permmapfile))
|
|
self.log.debug("Read {0} classes and {1} total permissions.".format(
|
|
class_count, total_perms))
|
|
|
|
def save(self, permmapfile):
|
|
"""
|
|
Save the permission map to the specified path. Existing files
|
|
will be overwritten.
|
|
|
|
Parameter:
|
|
permmapfile The path to write the permission map.
|
|
"""
|
|
with open(permmapfile, "w") as mapfile:
|
|
self.log.info("Writing permission map to \"{0}\"".format(permmapfile))
|
|
mapfile.write("{0}\n\n".format(len(self.permmap)))
|
|
|
|
for classname, perms in self.permmap.items():
|
|
mapfile.write("class {0} {1}\n".format(classname, len(perms)))
|
|
|
|
for permname, settings in perms.items():
|
|
direction = settings['direction']
|
|
weight = settings['weight']
|
|
|
|
assert min_weight <= weight <= max_weight, \
|
|
"{0}:{1} weight is out of range ({2}). This is an SETools bug.".format(
|
|
classname, permname, weight)
|
|
|
|
assert direction in infoflow_directions, \
|
|
"{0}:{1} flow direction ({2}) is invalid. This is an SETools bug.".format(
|
|
classname, permname, direction)
|
|
|
|
if direction == 'u':
|
|
self.log.warning("Warning: permission {0} in class {1} is unmapped.".format(
|
|
permname, classname))
|
|
|
|
mapfile.write("{0:>20} {1:>9} {2:>9}\n".format(permname, direction, weight))
|
|
|
|
mapfile.write("\n")
|
|
|
|
self.log.info("Successfully wrote permission map to \"{0}\"".format(permmapfile))
|
|
|
|
def classes(self):
|
|
"""
|
|
Generate class names in the permission map.
|
|
|
|
Yield:
|
|
class An object class name.
|
|
"""
|
|
for cls in self.permmap.keys():
|
|
yield cls
|
|
|
|
def perms(self, class_):
|
|
"""
|
|
Generate permission mappings for the specified class.
|
|
|
|
Parameter:
|
|
class_ An object class name.
|
|
|
|
Yield:
|
|
Mapping A permission's complete map (weight, direction, enabled)
|
|
"""
|
|
try:
|
|
for perm in self.permmap[class_].keys():
|
|
yield Mapping(self.permmap, class_, perm)
|
|
except KeyError:
|
|
raise exception.UnmappedClass("{0} is not mapped.".format(class_))
|
|
|
|
def mapping(self, class_, perm):
|
|
"""Retrieve a specific permission's mapping."""
|
|
return Mapping(self.permmap, class_, perm)
|
|
|
|
def exclude_class(self, class_):
|
|
"""
|
|
Exclude all permissions in an object class for calculating rule weights.
|
|
|
|
Parameter:
|
|
class_ The object class to exclude.
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
"""
|
|
for perm in self.perms(class_):
|
|
perm.enabled = False
|
|
|
|
def exclude_permission(self, class_, permission):
|
|
"""
|
|
Exclude a permission for calculating rule weights.
|
|
|
|
Parameter:
|
|
class_ The object class of the permission.
|
|
permission The permission name to exclude.
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
UnmappedPermission The specified permission is not mapped for the object class.
|
|
"""
|
|
Mapping(self.permmap, class_, permission).enabled = False
|
|
|
|
def include_class(self, class_):
|
|
"""
|
|
Include all permissions in an object class for calculating rule weights.
|
|
|
|
Parameter:
|
|
class_ The object class to include.
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
"""
|
|
|
|
for perm in self.perms(class_):
|
|
perm.enabled = True
|
|
|
|
def include_permission(self, class_, permission):
|
|
"""
|
|
Include a permission for calculating rule weights.
|
|
|
|
Parameter:
|
|
class_ The object class of the permission.
|
|
permission The permission name to include.
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
UnmappedPermission The specified permission is not mapped for the object class.
|
|
"""
|
|
|
|
Mapping(self.permmap, class_, permission).enabled = True
|
|
|
|
def map_policy(self, policy):
|
|
"""Create mappings for all classes and permissions in the specified policy."""
|
|
for class_ in policy.classes():
|
|
class_name = str(class_)
|
|
|
|
if class_name not in self.permmap:
|
|
self.log.debug("Adding unmapped class {0} from {1}".format(class_name, policy))
|
|
self.permmap[class_name] = OrderedDict()
|
|
|
|
perms = class_.perms
|
|
|
|
try:
|
|
perms |= class_.common.perms
|
|
except policyrep.exception.NoCommon:
|
|
pass
|
|
|
|
for perm_name in perms:
|
|
if perm_name not in self.permmap[class_name]:
|
|
self.log.debug("Adding unmapped permission {0} in {1} from {2}".
|
|
format(perm_name, class_name, policy))
|
|
Mapping(self.permmap, class_name, perm_name, create=True)
|
|
|
|
def rule_weight(self, rule):
|
|
"""
|
|
Get the type enforcement rule's information flow read and write weights.
|
|
|
|
Parameter:
|
|
rule A type enforcement rule.
|
|
|
|
Return: Tuple(read_weight, write_weight)
|
|
read_weight The type enforcement rule's read weight.
|
|
write_weight The type enforcement rule's write weight.
|
|
"""
|
|
|
|
write_weight = 0
|
|
read_weight = 0
|
|
class_name = str(rule.tclass)
|
|
|
|
if rule.ruletype != 'allow':
|
|
raise exception.RuleTypeError("{0} rules cannot be used for calculating a weight".
|
|
format(rule.ruletype))
|
|
|
|
# iterate over the permissions and determine the
|
|
# weight of the rule in each direction. The result
|
|
# is the largest-weight permission in each direction
|
|
for perm_name in rule.perms:
|
|
mapping = Mapping(self.permmap, class_name, perm_name)
|
|
|
|
if not mapping.enabled:
|
|
continue
|
|
|
|
if mapping.direction == "r":
|
|
read_weight = max(read_weight, mapping.weight)
|
|
elif mapping.direction == "w":
|
|
write_weight = max(write_weight, mapping.weight)
|
|
elif mapping.direction == "b":
|
|
read_weight = max(read_weight, mapping.weight)
|
|
write_weight = max(write_weight, mapping.weight)
|
|
|
|
return (read_weight, write_weight)
|
|
|
|
def set_direction(self, class_, permission, direction):
|
|
"""
|
|
Set the information flow direction of a permission.
|
|
|
|
Parameter:
|
|
class_ The object class of the permission.
|
|
permission The permission name.
|
|
direction The information flow direction the permission (r/w/b/n).
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
UnmappedPermission The specified permission is not mapped for the object class.
|
|
"""
|
|
Mapping(self.permmap, class_, permission).direction = direction
|
|
|
|
def set_weight(self, class_, permission, weight):
|
|
"""
|
|
Set the weight of a permission.
|
|
|
|
Parameter:
|
|
class_ The object class of the permission.
|
|
permission The permission name.
|
|
weight The weight of the permission (1-10).
|
|
|
|
Exceptions:
|
|
UnmappedClass The specified object class is not mapped.
|
|
UnmappedPermission The specified permission is not mapped for the object class.
|
|
"""
|
|
Mapping(self.permmap, class_, permission).weight = weight
|
|
|
|
|
|
#
|
|
# Settings Validation Functions
|
|
#
|
|
def validate_weight(weight):
|
|
if not min_weight <= weight <= max_weight:
|
|
raise ValueError("Permission weights must be 1-10: {0}".format(weight))
|
|
|
|
return weight
|
|
|
|
|
|
def validate_direction(direction):
|
|
if direction not in infoflow_directions:
|
|
raise ValueError("Invalid information flow direction: {0}".format(direction))
|
|
|
|
return direction
|
|
|
|
|
|
def validate_enabled(enabled):
|
|
return bool(enabled)
|
|
|
|
|
|
class Mapping(object):
|
|
|
|
"""A mapping for a permission in the permission map."""
|
|
|
|
weight = PermissionMapDescriptor("weight", validate_weight)
|
|
direction = PermissionMapDescriptor("direction", validate_direction)
|
|
enabled = PermissionMapDescriptor("enabled", validate_enabled)
|
|
|
|
def __init__(self, perm_map, classname, permission, create=False):
|
|
self.perm_map = perm_map
|
|
self.class_ = classname
|
|
self.perm = permission
|
|
|
|
if create:
|
|
if classname not in self.perm_map:
|
|
self.perm_map[classname] = OrderedDict()
|
|
|
|
self.perm_map[classname][permission] = {'direction': 'u',
|
|
'weight': 1,
|
|
'enabled': True}
|
|
|
|
else:
|
|
if classname not in self.perm_map:
|
|
raise exception.UnmappedClass("{0} is not mapped.".format(classname))
|
|
|
|
if permission not in self.perm_map[classname]:
|
|
raise exception.UnmappedPermission("{0}:{1} is not mapped.".
|
|
format(classname, permission))
|
|
|
|
def __lt__(self, other):
|
|
if self.class_ == other.class_:
|
|
return self.perm < other.perm
|
|
else:
|
|
return self.class_ < other.class_
|