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 service account key age."""

from builtins import object
from collections import namedtuple
import json
import threading

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

LOGGER = logger.get_logger(__name__)

[docs]class ServiceAccountKeyRulesEngine(bre.BaseRulesEngine): """Rules engine for service account key 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(ServiceAccountKeyRulesEngine, 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 ServiceAccountKeyRuleBook from the rules definition file. Args: global_configs (dict): Global configurations. """ with self._lock: self.rule_book = ServiceAccountKeyRuleBook( self._load_rule_definitions())
[docs] def find_violations(self, service_account, force_rebuild=False): """Determine whether service account key age violates rules. Args: service_account (ServiceAccount): A service account resource 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(service_account)
[docs]class ServiceAccountKeyRuleBook(bre.BaseRuleBook): """The RuleBook for service account key age rules.""" def __init__(self, rule_defs=None): """Initialization. Args: rule_defs (list): Serviceaccount keys rule definition dicts """ super(ServiceAccountKeyRuleBook, 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') 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)) key_max_age_str = rule_def.get('max_age', None) try: key_max_age = int(key_max_age_str) except (ValueError, TypeError): raise audit_errors.InvalidRulesSchemaError( 'Service account key "max_age" missing or not an ' 'integer 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: gcp_resource = resource_util.create_resource( resource_id=resource_id, resource_type=resource_type) rule = Rule( rule_def.get('name'), rule_index, key_max_age) 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, service_account): """Find violations in the rule book. Args: service_account (ServiceAccount): service account resource. Returns: list: RuleViolation """ LOGGER.debug('Looking for service account key violations: %s', service_account.full_name) violations = [] resource_ancestors = resource_util.get_ancestors_from_full_name( service_account.full_name) LOGGER.debug('Ancestors of resource: %r', resource_ancestors) checked_wildcards = set() for curr_resource in resource_ancestors: if not curr_resource: # The leaf node in the hierarchy continue resource_rule = self.get_resource_rules(curr_resource) if resource_rule: violations.extend( resource_rule.find_violations(service_account)) 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_rule = self.get_resource_rules(wildcard_resource) if resource_rule: violations.extend( resource_rule.find_violations(service_account)) 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, service_account): """Determine if the policy binding matches this rule's criteria. Args: service_account (ServiceAccount): service account resource. Returns: list: RuleViolation """ violations = [] for rule in self.rules: rule_violations = rule.find_violations(service_account) 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 'ServiceAccountKeyResourceRules<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, key_max_age): """Initialize. Args: rule_name (str): Name of the loaded rule rule_index (int): The index of the rule from the rule definitions key_max_age (int): Max allowed age in days of service account key """ self.rule_name = rule_name self.rule_index = rule_index self.key_max_age = key_max_age
[docs] def _is_more_than_max_age(self, created_time, scan_time): """Check if the key has been rotated: is the key creation time older than max_age in the policy Args: created_time (str): The time at which the key was created (this is the validAfterTime in the key API response (in string_formats.DEFAULT_FORSETI_TIMESTAMP) format scan_time (datetime): Snapshot timestamp. Returns: bool: Returns true if un_rotated """ created_time = date_time.get_datetime_from_string( created_time, string_formats.DEFAULT_FORSETI_TIMESTAMP) if (scan_time - created_time).days > self.key_max_age: return True return False
[docs] def find_violations(self, service_account): """Find service account key age violations based on the max_age. Args: service_account (ServiceAccount): ServiceAccount object. Returns: list: Returns a list of RuleViolation named tuples """ # Note: We're checking the age as of "now", the scanner run time # We could consider changing this to when the key was inventoried. scan_time = date_time.get_utc_now_datetime() violations = [] for key in service_account.keys: key_id = key.get('key_id') full_name = key.get('full_name') LOGGER.debug('Checking key rotation for %s', full_name) created_time = key.get('valid_after_time') if self._is_more_than_max_age(created_time, scan_time): violation_reason = ('Key ID %s not rotated since %s.' % (key_id, created_time)) violations.append(RuleViolation( resource_type=resource_mod.ResourceType.SERVICE_ACCOUNT_KEY,,, service_account_name=service_account.display_name, full_name=full_name, rule_name='%s (older than %s days)' % (self.rule_name, self.key_max_age), rule_index=self.rule_index, violation_type='SERVICE_ACCOUNT_KEY_VIOLATION', violation_reason=violation_reason, project_id=service_account.project_id, key_id=key_id, key_created_time=created_time, resource_data=json.dumps(key, sort_keys=True))) return violations
[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 (self.rule_name == other.rule_name and self.rule_index == other.rule_index and (self.key_max_age == other.key_max_age))
[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)
RuleViolation = namedtuple('RuleViolation', ['resource_type', 'resource_id', 'resource_name', 'service_account_name', 'full_name', 'rule_name', 'rule_index', 'violation_type', 'violation_reason', 'project_id', 'key_id', 'key_created_time', 'resource_data'])