Source code for google.cloud.forseti.scanner.audit.enabled_apis_rules_engine

# Copyright 2018 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
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Enabled APIS rules engine for organizations, folders, and projects.

Builds the RuleBook (EnabledApisRuleBook) from the rule definitions (file either
stored locally or in GCS) and compares a list of APIs  against the RuleBook to
determine whether there are violations.
"""

from builtins import object
import collections
import itertools
import threading

from google.cloud.forseti.common.gcp_type import resource_util
from google.cloud.forseti.common.util import logger
from google.cloud.forseti.common.util import relationship
from google.cloud.forseti.scanner.audit import base_rules_engine as bre
from google.cloud.forseti.scanner.audit import errors as audit_errors

LOGGER = logger.get_logger(__name__)

# Rule violation type.
VIOLATION_TYPE = 'ENABLED_APIS_VIOLATION'

# Rule Modes.
_WHITELIST = 'whitelist'
_BLACKLIST = 'blacklist'
_REQUIRED = 'required'
_RULE_MODES = frozenset([_WHITELIST, _BLACKLIST, _REQUIRED])


[docs]def _check_whitelist_apis(rule_apis=None, enabled_apis=None): """Whitelist: Checks that enabled APIs ARE in rule APIs. If an enabled api is NOT found in the rule APIs, add it to the violating APIs. Args: rule_apis (list): ManagedService APIs allowed in the rule. enabled_apis (list): ManagedService APIs enabled by the project. Returns: list: Enabled APIs NOT found in the whitelist (rule APIs). """ violating_apis = [] for api in enabled_apis: if api not in rule_apis: violating_apis.append(api) return violating_apis
[docs]def _check_blacklist_apis(rule_apis=None, enabled_apis=None): """Blacklist: Checks that enabled APIs ARE NOT in rule APIs. If an enabled API is found in the rule APIs, add it to the violating APIs. Args: rule_apis (list): ManagedService APIs dis-allowed in the rule. enabled_apis (list): ManagedService APIs enabled by the project. Returns: list: Enabled APIs found in the blacklist (rule APIs). """ violating_apis = [] for api in enabled_apis: if api in rule_apis: violating_apis.append(api) return violating_apis
[docs]def _check_required_apis(rule_apis=None, enabled_apis=None): """Required: Checks that rule APIs are in enabled APIs. If a required rule API is NOT found enabled in the project, add it to the violating APIs. Args: rule_apis (list): ManagedService APIs required in the rule. enabled_apis (list): ManagedService APIs enabled by the project. Returns: list: Required rule APIs not found enabled by the project. """ violating_apis = [] for api in rule_apis: if api not in enabled_apis: violating_apis.append(api) return violating_apis
[docs]class EnabledApisRulesEngine(bre.BaseRulesEngine): """Rules engine for enabled APIs.""" def __init__(self, rules_file_path, snapshot_timestamp=None): """Initialize. Args: rules_file_path (str): File location of rules. snapshot_timestamp (str): The snapshot to work with. """ super(EnabledApisRulesEngine, self).__init__( rules_file_path=rules_file_path, snapshot_timestamp=snapshot_timestamp) self.rule_book = None
[docs] def build_rule_book(self, global_configs=None): """Build EnabledApisRuleBook from the rules definition file. Args: global_configs (dict): Global configurations. """ self.rule_book = EnabledApisRuleBook( global_configs, self._load_rule_definitions(), snapshot_timestamp=self.snapshot_timestamp)
[docs] def find_violations(self, project, enabled_apis, force_rebuild=False): """Determine whether enabled APIs violates rules. Args: project (gcp_type): The project that these APIs are enabled on. enabled_apis (list): list of enabled APIs. force_rebuild (bool): If True, rebuilds the rule book. This will reload the rules definition file and add the rules to the book. Returns: iterable: A generator of rule violations. """ if self.rule_book is None or force_rebuild: self.build_rule_book() violations = self.rule_book.find_violations(project, enabled_apis) return set(violations)
[docs] def add_rules(self, rules): """Add rules to the rule book. Args: rules (list): The list of rules to add to the book. """ if self.rule_book is not None: self.rule_book.add_rules(rules)
[docs]class EnabledApisRuleBook(bre.BaseRuleBook): """The RuleBook for Enabled APIs resources. Rules from the rules definition file are parsed and placed into a map, which associates the GCP resource (project, folder or organization) with the rules defined for it. A project's enabled APIs are evaulated against rules for all ancestor resources of that project. """ supported_resource_types = frozenset([ 'project', 'folder', 'organization', ]) def __init__(self, global_configs, # pylint: disable= unused-argument rule_defs=None, snapshot_timestamp=None): """Initialize. Args: global_configs (dict): Global configurations. rule_defs (dict): The parsed dictionary of rules from the YAML definition file. snapshot_timestamp (str): The snapshot to lookup data. """ super(EnabledApisRuleBook, self).__init__() self._rules_sema = threading.BoundedSemaphore(value=1) self.resource_rules_map = collections.defaultdict(set) if not rule_defs: self.rule_defs = {} else: self.rule_defs = rule_defs self.add_rules(rule_defs) if snapshot_timestamp: self.snapshot_timestamp = snapshot_timestamp
[docs] def __eq__(self, other): """Equals. Args: other (object): Object to compare. Returns: bool: True or False. """ if not isinstance(other, type(self)): return NotImplemented return self.resource_rules_map == other.resource_rules_map
[docs] def __ne__(self, other): """Not Equals. Args: other (object): Object to compare. Returns: bool: True or False. """ return not self == other
[docs] def __repr__(self): """Object representation. Returns: str: The object representation. """ return 'EnabledApisRuleBook <{}>'.format(self.resource_rules_map)
[docs] def add_rules(self, rule_defs): """Add rules to the rule book. Args: rule_defs (dict): Rules parsed from the rule definition file. """ 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. The rule supplied to this method is the dictionary parsed from the rules definition file. For example, this rule... # rules yaml: rules: - name: a rule mode: whitelist resource: - type: project resource_ids: - my-project-123 services: - 'compute.googleapis.com' - 'storage-component.googleapis.com' - 'storage-api.googleapis.com' ... gets parsed into: { 'name': 'a rule', 'mode': 'whitelist', 'resource': { 'type': 'project', 'resource_ids': ['my-project-id'] }, 'services': [ 'compute.googleapis.com', 'storage-component.googleapis.com', 'storage-api.googleapis.com' ] } Args: rule_def (dict): Contains rule definition properties. rule_index (int): The index of the rule from the rule definitions. Assigned automatically when the rule book is built. """ self._rules_sema.acquire() try: resources = rule_def.get('resource') mode = rule_def.get('mode') services = rule_def.get('services') if not resources or not services or mode not in _RULE_MODES: raise audit_errors.InvalidRulesSchemaError( 'Faulty rule {}'.format(rule_index)) for resource in resources: resource_ids = resource.get('resource_ids') resource_type = resource.get('type') if resource_type not in self.supported_resource_types: raise audit_errors.InvalidRulesSchemaError( 'Invalid 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)) # For each resource id associated with the rule, create a # mapping of resource => rules. for resource_id in resource_ids: if resource_id == '*' and resource_type != 'project': raise audit_errors.InvalidRulesSchemaError( 'Wild-card must use project type in rule {}'.format( rule_index)) gcp_resource = resource_util.create_resource( resource_id=resource_id, resource_type=resource_type) rule_def_resource = { 'services': services, 'mode': mode, } rule = Rule(rule_name=rule_def.get('name'), rule_index=rule_index, rule=rule_def_resource) # If no mapping exists, create it. If the rule isn't in the # mapping, add it. self.resource_rules_map[gcp_resource].add(rule) finally: self._rules_sema.release()
[docs] def find_violations(self, project, enabled_apis): """Find enabled APIs violations in the rule book. Args: project (gcp_type): The project that these APIs are enabled on. enabled_apis (list): list of enabled APIs. Returns: iterable: A generator of the rule violations. """ violations = itertools.chain() # Check for rules on all ancestors, and the wildcard rule. resource_ancestors = ( relationship.find_ancestors(project, project.full_name)) resource_ancestors.append(resource_util.create_resource( resource_id='*', resource_type='project')) for curr_resource in resource_ancestors: resource_rules = self.resource_rules_map.get(curr_resource, []) for resource_rule in resource_rules: violations = itertools.chain( violations, resource_rule.find_violations(project, enabled_apis)) return violations
# Mapping from rule mode to mode check method. _RULE_MODE_METHODS = { _WHITELIST: _check_whitelist_apis, _BLACKLIST: _check_blacklist_apis, _REQUIRED: _check_required_apis, }
[docs]class Rule(object): """Rule properties from the rule definition file. Also finds violations.""" RuleViolation = collections.namedtuple( 'RuleViolation', ['resource_type', 'resource_id', 'full_name', 'rule_name', 'rule_index', 'violation_type', 'apis', 'resource_data', 'resource_name']) def __init__(self, rule_name, rule_index, rule): """Initialize. Args: rule_name (str): Name of the loaded rule. rule_index (int): The index of the rule from the rule definitions. rule (dict): The rule definition from the file. """ self.rule_name = rule_name self.rule_index = rule_index self.rule = rule
[docs] def find_violations(self, project, enabled_apis): """Find Enabled API violations in the rule book. Args: project (gcp_type): The project that these APIs are enabled on. enabled_apis (list): list of enabled APIs. Yields: namedtuple: Returns RuleViolation named tuple. """ violating_apis = _RULE_MODE_METHODS[self.rule['mode']]( rule_apis=self.rule['services'], enabled_apis=enabled_apis) if violating_apis: yield self.RuleViolation( resource_name=project.display_name, resource_type=project.type, resource_id=project.id, full_name=project.full_name, rule_name=self.rule_name, rule_index=self.rule_index, violation_type=VIOLATION_TYPE, apis=tuple(violating_apis), resource_data=project.data )