Source code for

# Copyright 2017 The Forseti Security Authors. All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.

"""Rules engine for checking arbitrary properties ofKE clusters."""

from builtins import object
from collections import namedtuple
import threading

import jmespath

from import errors as resource_errors
from import resource as resource_mod
from import resource_util
from import logger
from import base_rules_engine as bre
from import errors as audit_errors

LOGGER = logger.get_logger(__name__)

[docs]class KeRulesEngine(bre.BaseRulesEngine): """Rules engine for KE scanner.""" def __init__(self, rules_file_path, snapshot_timestamp=None): """Initialize. Args: rules_file_path (str): file location of rules snapshot_timestamp (str): snapshot timestamp. Defaults to None. If set, this will be the snapshot timestamp used in the engine. """ super(KeRulesEngine, self).__init__(rules_file_path=rules_file_path) self.rule_book = None self.snapshot_timestamp = snapshot_timestamp self._lock = threading.Lock()
[docs] def build_rule_book(self, global_configs=None): """Build KeRuleBook from the rules definition file. Args: global_configs (dict): Global configurations. """ with self._lock: self.rule_book = KeRuleBook( self._load_rule_definitions())
[docs] def find_violations(self, ke_cluster, force_rebuild=False): """Check if KE cluster satisfies provided rules. Args: ke_cluster (KeCluster): A KE Cluster object to check. force_rebuild (bool): If True, rebuilds the rule book. This will reload the rules definition file and add the rules to the book. Returns: generator: A generator of rule violations. """ if self.rule_book is None or force_rebuild: self.build_rule_book() return self.rule_book.find_violations(ke_cluster)
[docs]class KeRuleBook(bre.BaseRuleBook): """The RuleBook for KE rules.""" def __init__(self, rule_defs=None): """Initialization. Args: rule_defs (list): KE rule definition dicts """ super(KeRuleBook, self).__init__() self._lock = threading.Lock() self.resource_rules_map = {} if not rule_defs: self.rule_defs = {} else: self.rule_defs = rule_defs self.add_rules(rule_defs)
[docs] def add_rules(self, rule_defs): """Add rules to the rule book. Args: rule_defs (dict): rule definitions dictionary """ for (i, rule) in enumerate(rule_defs.get('rules', [])): self.add_rule(rule, i)
[docs] def add_rule(self, rule_def, rule_index): """Add a rule to the rule book. Args: rule_def (dict): A dictionary containing rule definition properties. rule_index (int): The index of the rule from the rule definitions. Assigned automatically when the rule book is built. """ with self._lock: for resource in rule_def.get('resource'): resource_ids = resource.get('resource_ids') resource_type = None try: resource_type = resource_mod.ResourceType.verify( resource.get('type')) except resource_errors.InvalidResourceTypeError: raise audit_errors.InvalidRulesSchemaError( 'Missing resource type in rule {}'.format(rule_index)) if not resource_ids or len(resource_ids) < 1: raise audit_errors.InvalidRulesSchemaError( 'Missing resource ids in rule {}'.format(rule_index)) rule_mode = rule_def.get('mode') if rule_mode not in ('blacklist', 'whitelist'): raise audit_errors.InvalidRulesSchemaError( 'Unknown mode in rule {}'.format(rule_index)) rule_key = rule_def.get('key') if rule_key is None: raise audit_errors.InvalidRulesSchemaError( 'Missing key in rule {}'.format(rule_index)) rule_values = rule_def.get('values', []) # For each resource id associated with the rule, create a # mapping of resource => rules. for resource_id in resource_ids: gcp_resource = resource_util.create_resource( resource_id=resource_id, resource_type=resource_type) rule = Rule( rule_def.get('name'), rule_index, rule_mode, rule_key, rule_values, ) resource_rules = self.resource_rules_map.setdefault( gcp_resource, ResourceRules(resource=gcp_resource)) if rule not in resource_rules.rules: resource_rules.rules.add(rule)
# pylint: enable=invalid-name
[docs] def get_resource_rules(self, resource): """Get all the resource rules for resource. Args: resource (Resource): The gcp_type Resource find in the map. Returns: ResourceRules: A ResourceRules object. """ return self.resource_rules_map.get(resource)
[docs] def find_violations(self, ke_cluster): """Find violations in the rule book. Args: ke_cluster (KeCluster): KE Cluster and ServerConfig data. Returns: list: RuleViolation """ LOGGER.debug('Looking for KE violations: %r', ke_cluster) violations = [] resource_ancestors = resource_util.get_ancestors_from_full_name( ke_cluster.full_name) LOGGER.debug('Ancestors of resource: %r', resource_ancestors) checked_wildcards = set() for curr_resource in resource_ancestors: if not curr_resource: # resource_ancestors will contain all the resources including # the child resource, which has type kubernetes cluster and # cannot be created (return None) as part of the ancestor path, # we will skip the child as it's not part of the ancestor. continue resource_rules = self.get_resource_rules(curr_resource) if resource_rules: violations.extend( resource_rules.find_violations(ke_cluster)) wildcard_resource = resource_util.create_resource( resource_id='*', resource_type=curr_resource.type) if wildcard_resource in checked_wildcards: continue checked_wildcards.add(wildcard_resource) resource_rules = self.get_resource_rules(wildcard_resource) if resource_rules: violations.extend( resource_rules.find_violations(ke_cluster)) LOGGER.debug('Returning violations: %r', violations) return violations
[docs]class ResourceRules(object): """An association of a resource to rules.""" def __init__(self, resource=None, rules=None): """Initialize. Args: resource (Resource): The resource to associate with the rule. rules (set): rules to associate with the resource. """ if not isinstance(rules, set): rules = set([]) self.resource = resource self.rules = rules
[docs] def find_violations(self, ke_cluster): """Determine if the policy binding matches this rule's criteria. Args: ke_cluster (KeCluster): KE Cluster and ServerConfig data. Returns: list: RuleViolation """ violations = [] for rule in self.rules: rule_violations = rule.find_violations(ke_cluster) if rule_violations: violations.extend(rule_violations) return violations
[docs] def __eq__(self, other): """Compare == with another object. Args: other (ResourceRules): object to compare with Returns: int: comparison result """ if not isinstance(other, type(self)): return NotImplemented return (self.resource == other.resource and self.rules == other.rules)
[docs] def __ne__(self, other): """Compare != with another object. Args: other (object): object to compare with Returns: int: comparison result """ return not self == other
[docs] def __repr__(self): """String representation of this node. Returns: str: debug string """ return 'IapResourceRules<resource={}, rules={}>'.format( self.resource, self.rules)
[docs]class Rule(object): """Rule properties from the rule definition file, also finds violations.""" def __init__(self, rule_name, rule_index, rule_mode, rule_key, rule_values): """Initialize. Args: rule_name (str): Name of the loaded rule rule_index (int): The index of the rule from the rule definitions rule_mode (str): blacklist or whitelist rule_key (str): jmespath pointing to the desired key rule_values (list): list of values, interpreted per mode """ self.rule_name = rule_name self.rule_index = rule_index self.rule_mode = rule_mode self.rule_key = rule_key self.rule_values = rule_values # compile right away to return exceptions asap self.rule_jmespath = jmespath.compile(self.rule_key)
[docs] def find_violations(self, ke_cluster): """Find KE violations in based on the rule. Args: ke_cluster (KeCluster): KE Cluster and ServerConfig data. Returns: list: Returns a list of RuleViolation named tuples """ violations = [] try: actual = except jmespath.exceptions.JMESPathError as e: LOGGER.warning( 'JMESPath error processing KE cluster %s: %s',, e ) # The resource data violated some assumption the user made # about it. So we cannot know if any violations occurred. return violations LOGGER.debug('actual jmespath result: %s', actual) if self.rule_mode == 'whitelist': if actual not in self.rule_values: violations.append(self._make_violation( ke_cluster, '%s has value %s, which is not in the whitelist (%s)' % ( self.rule_jmespath.expression, actual, self.rule_values, ), actual, )) if self.rule_mode == 'blacklist': if actual in self.rule_values: violations.append(self._make_violation( ke_cluster, '%s has value %s, which is in the blacklist (%s)' % ( self.rule_jmespath.expression, actual, self.rule_values, ), actual, )) return violations
[docs] def _make_violation(self, ke_cluster, violation_reason, actual): """Build a RuleViolation for the cluster. Args: ke_cluster (KeCluster): KE Cluster and ServerConfig data. violation_reason (str): The violation details. actual (object): The actual value of the jmespath expression. Returns: RuleViolation: A new RuleViolation namedtuple. """ return RuleViolation( resource_type=resource_mod.ResourceType.KE_CLUSTER,, full_name=ke_cluster.full_name, rule_name=self.rule_name, rule_index=self.rule_index, rule_mode=self.rule_mode, rule_values=self.rule_values, actual_value=actual, violation_type='KE_VIOLATION', violation_reason=violation_reason,,,,, )
[docs] def __eq__(self, other): """Test whether Rule equals other Rule. Args: other (Rule): object to compare to Returns: int: comparison result """ if not isinstance(other, type(self)): return NotImplemented return all( self.rule_name == other.rule_name, self.rule_index == other.rule_index, self.rule_mode == other.rule_mode, self.rule_values == other.rule_values, )
[docs] def __ne__(self, other): """Test whether Rule is not equal to another Rule. Args: other (object): object to compare to Returns: int: comparison result """ return not self == other
[docs] def __hash__(self): """Make a hash of the rule index. For now, this will suffice since the rule index is assigned automatically when the rules map is built, and the scanner only handles one rule file at a time. Later on, we'll need to revisit this hash method when we process multiple rule files. Returns: int: The hash of the rule index. """ return hash(self.rule_index)
# pylint: enable=inconsistent-return-statements # Rule violation. # resource_type: string # resource_id: string # rule_name: string # rule_index: int # violation_type: KE_VIOLATION # violation_reason: string # project_id: string # cluster_name: string RuleViolation = namedtuple('RuleViolation', [ 'resource_type', 'resource_id', 'full_name', 'rule_name', 'rule_index', 'rule_mode', 'rule_values', 'actual_value', 'violation_type', 'violation_reason', 'project_id', 'cluster_name', 'resource_data', 'resource_name', ])