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.

"""Scanner for the IAM rules engine."""

import json

from import iam_policy
from import BillingAccount
from import Bucket
from import Dataset
from import Folder
from import Organization
from import Project
from import ResourceType
from import logger
from import iam_rules_engine
from import base_scanner

LOGGER = logger.get_logger(__name__)

# List of Resource types that have supported IAM policy bindings.
# Add new supported resources to this dict to enable support in the scanner.
    ResourceType.BILLING_ACCOUNT: BillingAccount,
    ResourceType.BUCKET: Bucket,
    ResourceType.DATASET: Dataset,
    ResourceType.FOLDER: Folder,
    ResourceType.ORGANIZATION: Organization,
    ResourceType.PROJECT: Project,

# pylint: disable=too-many-branches
[docs]def _add_bucket_ancestor_bindings(policy_data): """Add bucket relevant IAM policy bindings from ancestors. Resources can inherit policy bindings from ancestors in the resource manager tree. For example: a GCS bucket inherits a 'objectViewer' role from a project or folder (up in the tree). So far the IAM rules engine only checks the set of bindings directly attached to a resource (direct bindings set (DBS)). We need to add relevant bindings inherited from ancestors to DBS so that these are also checked for violations. If we find one more than one binding with the same role name, we need to merge the members. NOTA BENE: this function only handles buckets and bindings relevant to these at present (but can and should be expanded to handle projects and folders going forward). Args: policy_data (list): list of (parent resource, iam_policy resource, policy bindings) tuples to find violations in. """ storage_iam_roles = frozenset([ 'roles/storage.admin', 'roles/storage.objectViewer', 'roles/storage.objectCreator', 'roles/storage.objectAdmin', ]) bucket_data = [] for (resource, _, bindings) in policy_data: if resource.type == 'bucket': bucket_data.append((resource, bindings)) for bucket, bucket_bindings in bucket_data: all_ancestor_bindings = [] for (resource, _, bindings) in policy_data: if resource.full_name == bucket.full_name: continue if bucket.full_name.find(resource.full_name): continue all_ancestor_bindings.append(bindings) for ancestor_bindings in all_ancestor_bindings: for ancestor_binding in ancestor_bindings: if ancestor_binding.role_name not in storage_iam_roles: continue if ancestor_binding in bucket_bindings: continue # Do we have a binding with the same 'role_name' already? for bucket_binding in bucket_bindings: if bucket_binding.role_name == ancestor_binding.role_name: bucket_binding.merge_members(ancestor_binding) break else: # no, add ancestor binding. bucket_bindings.append(ancestor_binding)
[docs]class IamPolicyScanner(base_scanner.BaseScanner): """Scanner for IAM data.""" SCANNER_OUTPUT_CSV_FMT = 'scanner_output_iam.{}.csv' def __init__(self, global_configs, scanner_configs, service_config, model_name, snapshot_timestamp, rules): """Initialization. Args: global_configs (dict): Global configurations. scanner_configs (dict): Scanner configurations. service_config (ServiceConfig): Forseti 2.0 service configs model_name (str): name of the data model snapshot_timestamp (str): Timestamp, formatted as YYYYMMDDTHHMMSSZ. rules (str): Fully-qualified path and filename of the rules file. """ super(IamPolicyScanner, self).__init__( global_configs, scanner_configs, service_config, model_name, snapshot_timestamp, rules) self.rules_engine = iam_rules_engine.IamRulesEngine( rules_file_path=self.rules, snapshot_timestamp=self.snapshot_timestamp) self.rules_engine.build_rule_book(self.global_configs)
[docs] @staticmethod def _flatten_violations(violations): """Flatten RuleViolations into a dict for each RuleViolation member. Args: violations (list): The RuleViolations to flatten. Yields: dict: Iterator of RuleViolations as a dict per member. """ for violation in violations: for member in violation.members: violation_data = { 'full_name': violation.full_name, 'role': violation.role, 'member': '%s:%s' % (member.type, } yield { 'resource_name': '{}/{}'.format(violation.resource_type, violation.resource_id), 'resource_id': violation.resource_id, 'resource_type': violation.resource_type, 'full_name': violation.full_name, 'rule_index': violation.rule_index, 'rule_name': violation.rule_name, 'violation_type': violation.violation_type, 'violation_data': violation_data, 'resource_data': violation.resource_data }
[docs] def _output_results(self, all_violations): """Output results. Args: all_violations (list): A list of violations. """ all_violations = self._flatten_violations(all_violations) self._output_results_to_db(all_violations)
[docs] def _find_violations(self, policies): """Find violations in the policies. Args: policies (list): list of (parent resource, iam_policy resource, policy bindings) tuples to find violations in. Returns: list: A list of all violations """ all_violations = []'Finding IAM policy violations...') for (resource, policy, policy_bindings) in policies: # At this point, the variable's meanings are switched: # "policy" is really the resource from the data model. # "resource" is the generated Forseti gcp type. LOGGER.debug('%s => %s', resource, policy) violations = self.rules_engine.find_violations( resource, policy, policy_bindings) all_violations.extend(violations) return all_violations
[docs] def _retrieve(self): """Retrieves the data for scanner. Returns: list: List of (gcp_type, forseti_data_model_resource) tuples. dict: A dict of resource counts. Raises: NoDataError: If no policies are found. """ model_manager = self.service_config.model_manager scoped_session, data_access = model_manager.get(self.model_name) with scoped_session as session: policy_data = [] resource_counts = {iam_type: 0 for iam_type in IAM_TYPE_RESOURCE_MAP} for policy in data_access.scanner_iter(session, 'iam_policy'): if policy.parent.type not in IAM_TYPE_RESOURCE_MAP: continue policy_bindings = [_f for _f in [ iam_policy.IamPolicyBinding.create_from(b) for b in json.loads('bindings', [])] if _f] resource_counts[policy.parent.type] += 1 resource_class = IAM_TYPE_RESOURCE_MAP[policy.parent.type] policy_data.append( (resource_class(, policy.parent.full_name,, policy, policy_bindings)) if not policy_data: LOGGER.warning('No policies found.') return [], 0 return policy_data, resource_counts
[docs] def run(self): """Runs the data collection.""" policy_data, _ = self._retrieve() _add_bucket_ancestor_bindings(policy_data) all_violations = self._find_violations(policy_data) self._output_results(all_violations)