Example #1
0
    def executable(self):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> mock.mock(None, 'which', None)
        >>> Serverless("path/to/project").executable
        Traceback (most recent call last):
        SystemExit: 2
        >>> mock.calls_for('eprint')
        "error: could not find framework executable: '{}', try using --framework-path", 'serverless'

        >>> mock.mock(None, 'which', "/usr/bin/serverless")
        >>> Serverless("path/to/project").executable
        '/usr/bin/serverless'
        """

        if not hasattr(self, '_executable'):
            executable = which(self.executable_name)
            if not executable:
                eprint(
                    "error: could not find framework executable: '{}', try using --framework-path",
                    self.executable_name)
                raise SystemExit(2)

            self._executable = os.path.abspath(executable)
        return self._executable
Example #2
0
    def get_function_root(self, name):
        self.package()

        package_name = self._get_function_package_name(name)
        function_root = os.path.join(self.serverless_package.name,
                                     package_name)
        if os.path.exists(function_root):
            return function_root

        try:
            zipfile = ZipFile(
                os.path.join(self.serverless_package.name,
                             "{}.zip".format(package_name)), 'r')
        except FileNotFoundError:
            eprint(
                "error: serverless package did not create a function zip for '{}'",
                name)
            raise SystemExit(2)
        except BadZipFile:
            eprint(
                "error: serverless package did not create a valid function zip for '{}'",
                name)
            raise SystemExit(2)

        with zipfile:
            zipfile.extractall(function_root)
        return function_root
Example #3
0
def check_version():
    try:
        response = request.urlopen(
            "http://cli.puresec.io/verify/version/{}".format(
                puresec_cli.__version__))
    except urllib.error.URLError:
        return

    try:
        response = json.loads(response.read().decode())
    except ValueError:
        return

    if not isinstance(response, dict):
        return

    try:
        is_uptodate, last_version = response['is_uptodate'], response[
            'last_version']
    except KeyError:
        return

    if not is_uptodate:
        eprint(
            "warn: you are using an outdated version of PureSec CLI (installed={}, latest={})"
            .format(puresec_cli.__version__, last_version))
Example #4
0
    def get_function_name(self, provider_function_name):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> framework = ServerlessFramework("path/to/project", {})

        >>> framework._serverless_config_cache = {'service': {'functions': {'otherFunction': {'name': 'other-function'}}}}
        >>> framework.get_function_name('function-name')
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint')
        "error: could not find Serverless name for function: '{}'", 'function-name'

        >>> framework._serverless_config_cache = {'service': {'functions': {'functionName': {'name': 'function-name'}}}}
        >>> framework.get_function_name('function-name')
        'functionName'
        """

        for name, function_config in self.serverless_config['service'].get(
                'functions', {}).items():
            if function_config['name'] == provider_function_name:
                return name

        eprint("error: could not find Serverless name for function: '{}'",
               provider_function_name)
        raise SystemExit(-1)
Example #5
0
    def get_function_root(self, name):
        if not hasattr(self, 'functions_output'):
            self.functions_output = TemporaryDirectory(
                "puresec-serverless-functions-")

        package_name = self._get_function_package_name(name)
        function_root = os.path.join(self.functions_output.name, package_name)
        if os.path.exists(function_root):
            return function_root

        try:
            zipfile = ZipFile(
                os.path.join(self.serverless_package,
                             "{}.zip".format(package_name)), 'r')
        except FileNotFoundError:
            eprint(
                "error: serverless package did not create a function zip for '{}'",
                name)
            raise SystemExit(2)
        except BadZipFile:
            eprint(
                "error: serverless package did not create a valid function zip for '{}'",
                name)
            raise SystemExit(2)

        with zipfile:
            zipfile.extractall(function_root)
        return function_root
Example #6
0
    def serverless_config(self):
        """
        >>> from pprint import pprint
        >>> from collections import namedtuple
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> TemporaryDirectory = namedtuple('TemporaryDirectory', ('name',))

        >>> serverless = Serverless("path/to/project")
        >>> serverless.package = lambda: None

        >>> serverless.serverless_package = TemporaryDirectory('/tmp/package')
        >>> serverless.serverless_config
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint')
        'error: serverless package did not create serverless-state.json'

        >>> with mock.open('/tmp/package/serverless-state.json', 'w') as f:
        ...     f.write('invalid') and None
        >>> serverless.serverless_config
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint') # ValueError for <=3.4, JSONDecodeError for >=3.5
        'error: invalid serverless-state.json:\\n{}', ...Error('Expecting value: line 1 column 1 (char 0)',)

        >>> with mock.open('/tmp/package/serverless-state.json', 'w') as f:
        ...     f.write('{ "x": { "y": 1 }, "z": 2 }') and None
        >>> pprint(serverless.serverless_config)
        {'x': {'y': 1}, 'z': 2}
        """

        if hasattr(self, '_serverless_config_cache'):
            return self._serverless_config_cache

        self.package()
        try:
            serverless_config = open(os.path.join(self.serverless_package.name,
                                                  'serverless-state.json'),
                                     'r',
                                     errors='replace')
        except FileNotFoundError:
            eprint(
                "error: serverless package did not create serverless-state.json"
            )
            raise SystemExit(-1)

        with serverless_config:
            try:
                self._serverless_config_cache = json.load(serverless_config)
            except ValueError as e:
                eprint("error: invalid serverless-state.json:\n{}", e)
                raise SystemExit(-1)

        return self._serverless_config_cache
Example #7
0
 def default_account(self):
     if not hasattr(self, '_default_account'):
         try:
             self._default_account = self.session.client(
                 'sts').get_caller_identity()['Account']
         except (botocore.exceptions.BotoCoreError,
                 botocore.exceptions.ClientError) as e:
             eprint("error: failed to get account from aws:\n{}", e)
             raise SystemExit(-1)
     return self._default_account
Example #8
0
    def get_cached_api_result(self, service, region, account, api_method, api_kwargs={}):
        client = self.get_client(service, region, account)
        if client is None:
            eprint("error: cannot create {} client for region: '{}', account: '{}'", service, region, account)
            return

        cache_key = (client, api_method, frozenset(api_kwargs.items()))

        result = AwsApi.RESOURCE_CACHE.get(cache_key)

        if result is None:
            try:
                result = AwsApi.RESOURCE_CACHE[cache_key] = getattr(client, api_method)(**api_kwargs)
            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
                eprint("error: failed to list resources on {}:\n{}", service, e)
                raise SystemExit(-1)

        return result
Example #9
0
    def serverless_package(self):
        if self.args.framework_output:
            return self.args.framework_output

        if not hasattr(self, '_package'):
            # sanity check so that we know FileNotFoundError later means Serverless is not installed
            serverless_config_path = os.path.join(self.path, "serverless.yml")
            if not os.path.exists(serverless_config_path):
                eprint("error: could not find serverless config in: {}",
                       serverless_config_path)
                raise SystemExit(-1)

            self._package = TemporaryDirectory(
                prefix="puresec-serverless-package-")

            try:
                # Suppressing output
                subprocess.check_output(
                    ['serverless', 'package', '--package', self._package.name],
                    cwd=self.path,
                    stderr=subprocess.STDOUT)
            except FileNotFoundError:
                eprint(
                    "error: serverless framework not installed, run `npm install -g severless` (or use --framework-path if not globally installed)"
                )
                raise SystemExit(-1)
            except subprocess.CalledProcessError as e:
                eprint("error: serverless package failed:\n{}",
                       e.output.decode())
                raise SystemExit(-1)

        return self._package.name
Example #10
0
    def cloudformation_template(self):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> Aws(resource_template="path/to/cloudformation.json").cloudformation_template
        Traceback (most recent call last):
        SystemExit: 2
        >>> mock.calls_for('eprint')
        'error: could not find CloudFormation template in: {}', 'path/to/cloudformation.json'

        >>> with mock.open("path/to/cloudformation.json", 'w') as f:
        ...     f.write("not a JSON") and None
        >>> Aws(resource_template="path/to/cloudformation.json").cloudformation_template
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint') # ValueError for <=3.4, JSONDecodeError for >=3.5
        'error: invalid CloudFormation template:\\n{}', ...Error('Expecting value: line 1 column 1 (char 0)',)

        >>> with mock.open("path/to/cloudformation.json", 'w') as f:
        ...     f.write('{ "a": { "b": 1 } }') and None
        >>> Aws(resource_template="path/to/cloudformation.json").cloudformation_template
        {'a': {'b': 1}}
        """

        if not hasattr(self, '_cloudformation_template'):
            if not self.resource_template:
                self._cloudformation_template = None
                self.cloudformation_filetype = None
                return

            _, self.cloudformation_filetype = os.path.splitext(
                self.resource_template)

            if self.cloudformation_filetype not in Aws.TEMPLATE_LOADERS:
                eprint("error: don't know how to load '{}' file",
                       self.resource_template)
                raise SystemExit(2)

            try:
                resource_template = open(self.resource_template,
                                         'r',
                                         errors='replace')
            except FileNotFoundError:
                eprint("error: could not find CloudFormation template in: {}",
                       self.resource_template)
                raise SystemExit(2)

            with resource_template:
                try:
                    self._cloudformation_template = Aws.TEMPLATE_LOADERS[
                        self.cloudformation_filetype](
                            resource_template,
                            default_region=self.default_region)
                except ValueError as e:
                    eprint("error: invalid CloudFormation template:\n{}", e)
                    raise SystemExit(-1)
        return self._cloudformation_template
Example #11
0
    def session(self):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> class Framework:
        ...     def get_default_profile(self):
        ...         return "default_profile"

        >>> with mock.open("path/to/cloudformation.json", 'w') as f:
        ...     f.write('{"a": {"b": 1}}') and None

        >>> Aws(resource_template="path/to/cloudformation.json").session
        Session(...)

        >>> Aws(resource_template="path/to/cloudformation.json", framework=Framework()).session
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint')
        'error: failed to create aws session:\\n{}', ProfileNotFound('The config profile (default_profile) could not be found',)
        """

        if not hasattr(self, '_session'):
            if self.framework:
                profile = self.framework.get_default_profile()
            else:
                profile = None

            try:
                self._session = boto3.Session(profile_name=profile)
            except (botocore.exceptions.BotoCoreError,
                    botocore.exceptions.ClientError) as e:
                eprint("error: failed to create aws session:\n{}", e)
                raise SystemExit(-1)
        return self._session
Example #12
0
    def __init__(self, path, config, resource_template=None, runtime=None, handler=None, function_name=None, framework=None, function=None, args=None):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> AwsProvider("path/to/project", config={})
        Traceback (most recent call last):
        SystemExit: 2
        >>> mock.calls_for('eprint')
        'error: must supply either --resource-template, --runtime, or --framework'

        >>> with mock.open("path/to/cloudformation.json", 'w') as f:
        ...     f.write('{}') and None
        >>> AwsProvider("path/to/project", config={}, resource_template="path/to/cloudformation.json", runtime='nodejs') and None
        >>> mock.calls_for('eprint')
        'warn: ignoring --runtime when --resource-template or --framework supplied'
        """

        Base.__init__(
            self,
            path, config,
            resource_template=resource_template,
            runtime=runtime,
            handler=handler,
            function_name=function_name,
            framework=framework,
            function=function,
            args=args
        )
        Aws.__init__(
            self,
            resource_template=self.resource_template,
            framework=self.framework,
        )

        if not self.resource_template and not self.runtime:
            eprint("error: must supply either --resource-template, --runtime, or --framework")
            raise SystemExit(2)
        if self.resource_template:
            if self.runtime:
                eprint("warn: ignoring --runtime when --resource-template or --framework supplied")
            if self.handler:
                eprint("warn: ignoring --handler when --resource-template or --framework supplied")
            if self.function_name:
                eprint("warn: ignoring --function-name when --resource-template or --framework supplied")
Example #13
0
    def generate_provider(self, path, framework, config):
        if framework:
            provider = framework.get_provider_name()
            if provider:
                if self.args.provider:
                    # self.args.provider always in providers.__all__, no need to check
                    if self.args.provider != provider:
                        eprint(
                            "error: conflict between --provider ('{}') option and framework ('{}')"
                            .format(self.args.provider, provider))
                        raise SystemExit(2)
                elif provider not in providers.__all__:
                    eprint("error: provider not yet supported: '{}'".format(
                        provider))
                    raise SystemExit(2)
            else:
                if not self.args.provider:
                    eprint(
                        "error: could not determine provider from framework, please specify with --provider"
                    )
                    raise SystemExit(2)
                provider = self.args.provider
        else:
            if not self.args.provider:
                eprint("error: must specify either --provider or --framework")
                raise SystemExit(2)
            provider = self.args.provider

        provider = import_module(
            "puresec_cli.actions.generate_roles.providers.{}".format(
                provider)).Provider(
                    path,
                    config,
                    resource_template=self.args.resource_template,
                    runtime=self.args.runtime,
                    handler=self.args.handler,
                    function_name=self.args.function_name,
                    framework=framework,
                    function=self.args.function,
                    args=self.args,
                )
        with provider:
            yield provider
Example #14
0
    def package(self):
        """
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')

        >>> Serverless("path/to/project").package()
        Traceback (most recent call last):
        SystemExit: -1
        >>> mock.calls_for('eprint')
        'error: could not find serverless config in: {}', 'path/to/project/serverless.yml'
        """

        if not hasattr(self, 'serverless_package'):
            # sanity check so that we know FileNotFoundError later means Serverless is not installed
            serverless_config_path = os.path.join(self.path, "serverless.yml")
            if not os.path.exists(serverless_config_path):
                eprint("error: could not find serverless config in: {}",
                       serverless_config_path)
                raise SystemExit(-1)

            self.serverless_package = TemporaryDirectory(prefix="puresec-")

            try:
                # Suppressing output
                subprocess.check_output([
                    self.executable, 'package', '--package',
                    self.serverless_package.name
                ],
                                        cwd=self.path,
                                        stderr=subprocess.STDOUT)
            except FileNotFoundError:
                eprint(
                    "error: serverless framework not installed, run `npm install -g severless` (or use --framework-path if not globally installed)"
                )
                raise SystemExit(-1)
            except subprocess.CalledProcessError as e:
                eprint("error: serverless package failed:\n{}",
                       e.output.decode())
                raise SystemExit(-1)
Example #15
0
    def _walk(self, processor, *args, **kwargs):
        """
        >>> from collections import namedtuple
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> mock.mock(pkg_resources, 'resource_filename', "/path/to/node_modules")

        >>> processed = []
        >>> def processor(filename, contents, custom_positional, custom_keyword):
        ...     processed.append((filename, contents, custom_positional, custom_keyword))

        >>> def stat(self, filename):
        ...     return namedtuple('Stat', ('st_size',))(5*1024*1024 if filename == "/path/to/function/large-file" else 512)
        >>> mock.mock(NodejsRuntime, '_stat', stat)

        >>> mock.filesystem = {'': {'path': {'to': {'function': {
        ...     'large-file': True,
        ...     'config': True,
        ...     'unreferenced': True,
        ... }}}}}
        >>> with mock.open("/path/to/function/config", 'w') as f:
        ...     f.write("some config") and None
        >>> with mock.open("/path/to/function/src/index.js", 'w') as f:
        ...     f.write("some code config large-file more code") and None

        >>> mock.mock(subprocess, 'check_output', b"/path/to/function/src/index.js\\n")
        >>> NodejsRuntime('/path/to/function', resource_properties={'Handler': "src/index.handler"}, provider=object()) \\
        ...     ._walk(processor, 'positional', custom_keyword='keyword')
        >>> mock.calls_for('subprocess.check_output')
        ['node', '/path/to/node_modules/dependency-tree/bin/cli.js', '/path/to/function/src/index.js', '--directory', '/path/to/function', '--list-form'], stderr=-2
        >>> processed
        [('/path/to/function/src/index.js', 'some code config large-file more code', 'positional', 'keyword'),
         ('/path/to/function/config', 'some config', 'positional', 'keyword')]
        """

        if hasattr(self, '_dependencies'):
            # cached
            for filename in self._dependencies:
                with open(filename, 'r', errors='replace') as file:
                    processor(filename, file.read(), *args, **kwargs)
            return

        # getting main JavaScript file (from Handler)
        handler = self.resource_properties.get('Handler')
        if not handler:
            # dummy CloudFormation? walking everything
            super()._walk(processor, *args, **kwargs)
            return
        module = '.'.join(handler.split(
            '.')[0:-1])  # all except the last part which is the method
        filename = os.path.abspath(
            os.path.join(self.root, "{}.js".format(module)))
        if not os.path.exists(filename):
            return

        # acquiring dependencies using NPM's dependency-tree
        dependency_tree_cli_path = os.path.abspath(
            os.path.join(
                pkg_resources.resource_filename('puresec_cli',
                                                'resources/node_modules'),
                'dependency-tree/bin/cli.js'))
        try:
            dependencies = subprocess.check_output([
                'node', dependency_tree_cli_path, filename, '--directory',
                self.root, '--list-form'
            ],
                                                   stderr=subprocess.STDOUT)
        except FileNotFoundError:
            eprint("error: function runtime (nodejs) must be installed")
            raise SystemExit(-1)
        except subprocess.CalledProcessError as e:
            eprint("error: failed to get dependency tree:\n{}",
                   e.output.decode())
            raise SystemExit(-1)

        dependencies = [
            dependency for dependency in dependencies.decode().split('\n')
            # skipping last blank line and aws-sdk
            if dependency and '/node_modules/aws-sdk/' not in dependency
        ]
        self._dependencies = dependencies[:]  # cache

        # getting all non-dependency files
        resources = []  # (abspath, filename)
        for path, dirs, filenames in os.walk(self.root):
            if path.endswith('node_modules'):
                # skipping aws-sdk
                try:
                    dirs.remove('aws-sdk')
                except ValueError:
                    pass

            paths_generator = (
                (os.path.abspath(os.path.join(path, filename)), filename)
                for filename in filenames if
                not NodejsRuntime.JAVASCRIPT_FILENAME_PATTERN.search(filename))
            resources.extend(paths_tuple for paths_tuple in paths_generator
                             if self._stat(paths_tuple[0]).st_size <
                             NodejsRuntime.MAX_FILE_SIZE)

        while dependencies:
            filename = dependencies.pop(0)
            with open(filename, 'r', errors='replace') as file:
                # adding resources referenced by current file
                used_resources_indexes = []
                contents = file.read()
                for index, (resource_abspath,
                            resource_filename) in enumerate(resources):
                    if resource_filename in contents:
                        dependencies.append(resource_abspath)
                        self._dependencies.append(resource_abspath)
                        used_resources_indexes.append(index)
                for index in reversed(used_resources_indexes):
                    resources.pop(index)
                # processing current file
                processor(filename, contents, *args, **kwargs)
Example #16
0
    def _normalize_actions(self, resources, parents):
        """ Convert set to match-all when there's at least one.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> resources = {'table/sometable': {'a', 'b', 'c'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'a', 'b', 'c'}}

        >>> resources = {'table/sometable': {'a', '*', 'c'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}}

        >>> resources = {'table/sometable': set(), 'table/sometable/stream/somestream': {'dynamodb:DescribeStream'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable/stream/somestream': {'dynamodb:DescribeStream'}}

        >>> resources = {'table/sometable/stream/somestream': set(), 'table/sometable': {'dynamodb:GetItem'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'dynamodb:GetItem'}}

        >>> mock.mock(None, 'eprint')
        >>> resources = {'table/sometable': set()}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> mock.calls_for('eprint')
        "warn: unknown actions for '{}:{}', couldn't find any relevant SDK methods in your code, falling back to '*'", 'dynamodb:us-west-1', 'table/sometable'
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}}

        >>> resources = {'table/sometable': set(), 'table/sometable/stream/somestream': set()}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}, 'table/sometable/stream/somestream': {'*'}}
        """
        for resource, actions in tuple(resources.items()):
            if not actions:
                # if there are no other resources with common name that *do* have actions
                if any((other_resource.startswith(resource.rstrip('*'))
                        or resource.startswith(other_resource.rstrip('*')))
                       and other_actions.difference({'*'})
                       for other_resource, other_actions in resources.items()):
                    # then it's fine
                    del resources[resource]
                else:
                    actions.add('*')
                    eprint(
                        "warn: unknown actions for '{}:{}', couldn't find any relevant SDK methods in your code, falling back to '*'",
                        ':'.join(parents), resource)
            elif '*' in actions:
                actions.clear()
                actions.add('*')
Example #17
0
    def _normalize_resources(self, resources, parents):
        """ Convert dict to match-all when there's at least one.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> resources = {'a': {'x'}, 'b': {'y'}, 'c': {'z'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(resources)
        {'a': {'x'}, 'b': {'y'}, 'c': {'z'}}

        >>> resources = {'a': {'x'}, '*': {'y'}, 'c': {'z'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'*': {'x', 'y', 'z'}}

        >>> resources = {'activity/b': {'z'}, 'execution/*': {'*'}, 'stateMachine/a': {'x'}, 'stateMachine/*': {'y'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'activity/b': {'z'}, 'execution/*': {'*'}, 'stateMachine/*': {'x', 'y'}}

        >>> mock.mock(None, 'eprint')

        >>> resources = defaultdict(set)
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> mock.calls_for('eprint')
        "warn: unknown resources for '{}', couldn't find anything relevant in your AWS account or CloudFormation, falling back to '*'", 'dynamodb:us-west-1'
        >>> dict(resources)
        {'*': set()}
        """

        if not resources:
            resources['*']  # accessing to initialize defaultdict
            eprint(
                "warn: unknown resources for '{}', couldn't find anything relevant in your AWS account or CloudFormation, falling back to '*'",
                ':'.join(parents))
        else:
            # mapping all resources wildcard matching to others
            wildcard_matches = {}
            for resource in resources.keys():
                if '*' in resource or '?' in resource:
                    matches = fnmatch.filter(resources.keys(), resource)
                    if len(matches) > 1:  # not just self
                        wildcard_matches[resource] = matches

            for wildcard, matches in wildcard_matches.items():
                if wildcard not in resources:
                    continue  # processed by another resource
                merged = set()
                for resource in matches:
                    if resource not in resources:
                        continue  # processed by another resource
                    merged.update(resources[resource])
                    del resources[resource]
                resources[wildcard] = merged
Example #18
0
class Base(RuntimeBase, BaseApi):
    __metaclass__ = abc.ABCMeta

    def __init__(self, root, resource_properties, provider):
        super().__init__(root, provider)
        self.resource_properties = resource_properties

        self.environment_variables = self.resource_properties.get(
            'Environment', {}).get('Variables', {})

        # { service: { region: { account: { resource: {action} } } } }
        self._permissions = defaultdict(
            lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(set))))

    @property
    def permissions(self):
        """
        >>> from pprint import pprint

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())
        >>> runtime._permissions = {
        ...     'dynamodb': {'us-west-1': {'111': {'table/a': {'GetRecords'}, 'table/b': {'UpdateItem'}}}},
        ...     'ses': {'*': {'111': {'*': {'*'}}, '222': {'*': {'*'}}}},
        ... }

        >>> pprint(runtime.permissions)
        {'arn:aws:dynamodb:us-west-1:111:table/a': {'GetRecords'},
         'arn:aws:dynamodb:us-west-1:111:table/b': {'UpdateItem'},
         'arn:aws:ses:*:111:*': {'*'},
         'arn:aws:ses:*:222:*': {'*'}}
        """

        permissions = {}
        for service, regions in self._permissions.items():
            for region, accounts in regions.items():
                for account, resources in accounts.items():
                    for resource, actions in resources.items():
                        permissions["arn:aws:{}:{}:{}:{}".format(
                            service, region, account, resource)] = actions
        return permissions

    # Processing (override these)

    def process(self):
        self._process_services()
        self._process_regions()
        self._process_resources()
        self._process_actions()

        self._cleanup()

    @abc.abstractmethod
    def _get_services(self, filename, contents):
        pass

    try:
        REGION_PATTERNS = dict(
            (region, re.compile(r"\b{}\b".format(re.escape(region))))
            for region in boto3.Session().get_available_regions('ec2'))
    except (botocore.exceptions.BotoCoreError,
            botocore.exceptions.ClientError) as e:
        eprint("error: failed to create aws session:\n{}", e)
        raise SystemExit(-1)
    # regions = set()
    def _get_regions(self, filename, contents, regions, service, account):
        """
        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={'Environment': {'Variables': {'var1': "gigi eu-west-1 laus-east-2", 'var2': "eu-central-1 ca-central-1" }}}, provider=object())

        >>> regions = set()
        >>> runtime._get_regions('filename', "lalala us-east-1 lululu us-west-1 us-east-2la us-west-5 nonono", regions, 'dynamodb', '*')
        >>> sorted(regions)
        ['ca-central-1', 'eu-central-1', 'eu-west-1', 'us-east-1', 'us-west-1']
        """

        # From file
        regions.update(region
                       for region, pattern in Base.REGION_PATTERNS.items()
                       if pattern.search(contents))
        # From environment
        if not hasattr(self, '_environment_regions'):
            self._environment_regions = set(
                region for region, pattern in Base.REGION_PATTERNS.items()
                if any(
                    pattern.search(value)
                    for value in self.environment_variables.values()
                    if isinstance(value, str)))
        regions.update(self._environment_regions)

    # resources = defaultdict(set)
    @abc.abstractmethod
    def _get_resources(self, filename, contents, resources, region, account,
                       service):
        pass

    # actions = set()
    @abc.abstractmethod
    def _get_actions(self, filename, contents, actions, service):
        pass

    # Sub processors

    def _process_services(self):
        self._walk(self._get_services)
        self._normalize_permissions(self._permissions)

    def _process_regions(self):
        """ Expands '*' regions to all regions seen within the code.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> mock.mock(Base, '_walk', lambda self, processor, possible_regions, service, account: possible_regions.update({'us-east-1', 'us-east-2'}))
        >>> runtime._permissions = {
        ...     'dynamodb': {'us-west-1': {'111': {'table/a': set(), 'table/b': set()}}},
        ...     'ses': defaultdict(dict, {'*': {'111': {'*': set()}, '222': {'*': set()}}})
        ... }
        >>> runtime._process_regions()
        >>> mock.calls_for('Base._walk')
        Runtime, _get_regions, {'us-east-1', 'us-east-2'}, account='111', service='ses'
        Runtime, _get_regions, {'us-east-1', 'us-east-2'}, account='222', service='ses'
        >>> pprint(normalize_dict(runtime._permissions))
        {'dynamodb': {'us-west-1': {'111': {'table/a': set(), 'table/b': set()}}},
         'ses': {'us-east-1': {'111': {'*': set()}, '222': {'*': set()}}, 'us-east-2': {'111': {'*': set()}, '222': {'*': set()}}}}
        """

        for service, regions in self._permissions.items():
            if '*' in regions:
                for account, resources in sorted(regions['*'].items()):
                    possible_regions = set()
                    self._walk(
                        self._get_regions,
                        # custom arguments to processor
                        possible_regions,
                        service=service,
                        account=account)
                    # moving the account from '*' to possible regions
                    if possible_regions:
                        for region in possible_regions:
                            regions[region][account] = deepcopy(resources)
                        del regions['*'][account]

                if not regions['*']:
                    # all moved
                    del regions['*']

    def _process_resources(self):
        for service, regions in self._permissions.items():
            for region, accounts in regions.items():
                for account, resources in accounts.items():
                    self._walk(
                        self._get_resources,
                        # custom arguments to processor
                        resources,
                        region=region,
                        account=account,
                        service=service,
                    )
                    self._normalize_resources(resources,
                                              (service, region, account))

    def _process_actions(self):
        for service, regions in self._permissions.items():
            actions = set()
            self._walk(
                self._get_actions,
                # custom arguments to processor
                actions,
                service=service,
            )

            for region, accounts in regions.items():
                for account, resources in accounts.items():
                    self._match_resources_actions(service, resources, actions)

                    self._normalize_actions(resources,
                                            (service, region, account))

    # get_all_resources_method: (region, account) => {resource: pattern}
    def _get_generic_resources(self, filename, contents, resources, region,
                               account, resource_format,
                               get_all_resources_method):
        """ Simply greps resources inside the given contents.

        >>> from collections import defaultdict
        >>> from functools import partial
        >>> from pprint import pprint
        >>> from tests.mock import Mock
        >>> from tests.utils import normalize_dict
        >>> mock = Mock(__name__)

        >>> class Provider:
        ...     pass
        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={'Environment': {'Variables': {'var1': "gigi table-1 latable-6", 'var2': "table-2 table-3"}}}, provider=Provider())
        >>> runtime.provider.cloudformation_template = None

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'TableNames': ["table-1", "table-2", "table-3", "table-4", "table-5", "table-6"]})

        >>> resources = defaultdict(set)
        >>> runtime._get_generic_resources('filename', "lalala table-4 lululu table-5 table-6la table-7 nonono", resources, region='us-east-1', account='some-account',
        ...                                resource_format="table/{}", get_all_resources_method=partial(runtime._get_generic_all_resources, 'dynamodb', template_type='AWS::DynamoDB::Table', api_method='list_tables', api_attribute='TableNames'))
        >>> pprint(normalize_dict(resources))
        {'table/table-1': set(), 'table/table-2': set(), 'table/table-3': set(), 'table/table-4': set(), 'table/table-5': set()}
        >>> mock.calls_for('Provider.get_cached_api_result')
        'dynamodb', account='some-account', api_kwargs={}, api_method='list_tables', region='us-east-1'
        """

        all_resources = get_all_resources_method(region=region,
                                                 account=account)
        if not all_resources:
            resources[resource_format.format('*')]
            return

        # From file
        for resource, pattern in all_resources.items():
            if pattern.search(contents):
                resources[resource_format.format(resource)]

        # From environment
        for resource, pattern in all_resources.items():
            if any(
                    pattern.search(value)
                    for value in self.environment_variables.values()
                    if isinstance(value, str)):
                resources[resource_format.format(resource)]

    def _match_resources_actions(self, service, resources, actions):
        """
        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> resources = defaultdict(set, {'table/sometable': set(), 'table/sometable/stream/somestream': set()})
        >>> actions = {'dynamodb:PutItem', 'dynamodb:GetRecords', 'dynamodb:DeleteItem', 'dynamodb:DescribeStream', 'dynamodb:ListTables'}
        >>> runtime._match_resources_actions('dynamodb', resources, actions)
        >>> pprint(normalize_dict(resources))
        {'*': {'dynamodb:ListTables'}, 'table/sometable': {'dynamodb:DeleteItem', 'dynamodb:PutItem'}, 'table/sometable/stream/somestream': {'dynamodb:DescribeStream', 'dynamodb:GetRecords'}}

        >>> resources = defaultdict(set, {'table/sometable': set()})
        >>> actions = {'dynamodb:GetRecords', 'dynamodb:DescribeStream'}
        >>> runtime._match_resources_actions('dynamodb', resources, actions)
        >>> pprint(normalize_dict(resources))
        {'table/*/stream/*': {'dynamodb:DescribeStream', 'dynamodb:GetRecords'}, 'table/sometable': set()}
        """

        matchers = Base.SERVICE_RESOURCE_ACTION_MATCHERS.get(service)

        if not matchers:
            if not resources:
                resources['*']
            # not specific matchers, just add all actions to all resources
            for resource, resource_actions in resources.items():
                resource_actions.update(actions)
            return

        unused_actions = set(actions)
        # matching with resources
        for resource, resource_actions in resources.items():
            for pattern, default, matching_actions in matchers:
                if pattern.match(resource):
                    matching = matching_actions.intersection(actions)
                    unused_actions.difference_update(matching)
                    resource_actions.update(matching)
                    break
        # adding unused actions to 'default' resources
        for action in unused_actions:
            for pattern, default, matching_actions in matchers:
                if action in matching_actions:
                    resources[default].add(action)

    def _cleanup(self):
        """ Merges region-less services and resource-less actions.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> runtime._permissions = {
        ...     'dynamodb': {'us-west-1': {'111': {'table/a': {'dynamodb:GetItem'}}}},
        ...     's3': defaultdict(dict, {'us-east-1': {'some-account': {'somebucket': {'s3:CreateBucket'}}}, 'us-west-1': {'another-account': {'anotherbucket': {'s3:ListObjects'}}}})
        ...     }
        >>> runtime._cleanup()
        >>> pprint(normalize_dict(runtime._permissions))
        {'dynamodb': {'us-west-1': {'111': {'table/a': {'dynamodb:GetItem'}}}},
         's3': {'': {'another-account': {'anotherbucket': {'s3:ListObjects'}}, 'some-account': {'*': {'s3:CreateBucket'}}}}}
        """
        # Region-less services
        for service in Base.REGIONLESS_SERVICES:
            if service not in self._permissions:
                continue
            merged = reduce(deepmerge, self._permissions[service].values())
            self._permissions[service].clear()
            self._permissions[service][''] = merged

        for service, resourceless_actions in Base.SERVICE_RESOURCELESS_ACTIONS.items(
        ):
            if service not in self._permissions:
                continue
            for region, accounts in self._permissions[service].items():
                for account, resources in accounts.items():
                    found_actions = set()
                    for resource, actions in tuple(resources.items()):
                        for action in resourceless_actions:
                            if action in actions:
                                found_actions.add(action)
                                actions.remove(action)
                        if not actions:
                            del resources[resource]
                    if found_actions:
                        resources['*'] = found_actions

    # Helpers

    def _normalize_permissions(self, tree):
        """ Merge trees when one of the keys have '*' permission.

        >>> from pprint import pprint
        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> tree = {'a': {'b': {'c': 1}, '*': {'d': 2}, 'e': {'f': 3}}}
        >>> runtime._normalize_permissions(tree)
        >>> pprint(tree)
        {'a': {'*': {'c': 1, 'd': 2, 'f': 3}}}

        >>> tree = {'b': {'c': 1}, '*': {'d': 2}, 'e': {'f': 3}}
        >>> runtime._normalize_permissions(tree)
        >>> pprint(tree)
        {'*': {'c': 1, 'd': 2, 'f': 3}}
        """

        if '*' in tree:
            merged = reduce(deepmerge, tree.values())
            tree.clear()
            tree['*'] = merged

        for k, v in tree.items():
            if isinstance(v, dict):
                self._normalize_permissions(v)

    def _normalize_resources(self, resources, parents):
        """ Convert dict to match-all when there's at least one.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> resources = {'a': {'x'}, 'b': {'y'}, 'c': {'z'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(resources)
        {'a': {'x'}, 'b': {'y'}, 'c': {'z'}}

        >>> resources = {'a': {'x'}, '*': {'y'}, 'c': {'z'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'*': {'x', 'y', 'z'}}

        >>> resources = {'activity/b': {'z'}, 'execution/*': {'*'}, 'stateMachine/a': {'x'}, 'stateMachine/*': {'y'}}
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'activity/b': {'z'}, 'execution/*': {'*'}, 'stateMachine/*': {'x', 'y'}}

        >>> mock.mock(None, 'eprint')

        >>> resources = defaultdict(set)
        >>> runtime._normalize_resources(resources, ['dynamodb', 'us-west-1'])
        >>> mock.calls_for('eprint')
        "warn: unknown resources for '{}', couldn't find anything relevant in your AWS account or CloudFormation, falling back to '*'", 'dynamodb:us-west-1'
        >>> dict(resources)
        {'*': set()}
        """

        if not resources:
            resources['*']  # accessing to initialize defaultdict
            eprint(
                "warn: unknown resources for '{}', couldn't find anything relevant in your AWS account or CloudFormation, falling back to '*'",
                ':'.join(parents))
        else:
            # mapping all resources wildcard matching to others
            wildcard_matches = {}
            for resource in resources.keys():
                if '*' in resource or '?' in resource:
                    matches = fnmatch.filter(resources.keys(), resource)
                    if len(matches) > 1:  # not just self
                        wildcard_matches[resource] = matches

            for wildcard, matches in wildcard_matches.items():
                if wildcard not in resources:
                    continue  # processed by another resource
                merged = set()
                for resource in matches:
                    if resource not in resources:
                        continue  # processed by another resource
                    merged.update(resources[resource])
                    del resources[resource]
                resources[wildcard] = merged

    def _normalize_actions(self, resources, parents):
        """ Convert set to match-all when there's at least one.

        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(Base):
        ...     pass
        >>> runtime = Runtime('path/to/function', resource_properties={}, provider=object())

        >>> resources = {'table/sometable': {'a', 'b', 'c'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'a', 'b', 'c'}}

        >>> resources = {'table/sometable': {'a', '*', 'c'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}}

        >>> resources = {'table/sometable': set(), 'table/sometable/stream/somestream': {'dynamodb:DescribeStream'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable/stream/somestream': {'dynamodb:DescribeStream'}}

        >>> resources = {'table/sometable/stream/somestream': set(), 'table/sometable': {'dynamodb:GetItem'}}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'dynamodb:GetItem'}}

        >>> mock.mock(None, 'eprint')
        >>> resources = {'table/sometable': set()}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> mock.calls_for('eprint')
        "warn: unknown actions for '{}:{}', couldn't find any relevant SDK methods in your code, falling back to '*'", 'dynamodb:us-west-1', 'table/sometable'
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}}

        >>> resources = {'table/sometable': set(), 'table/sometable/stream/somestream': set()}
        >>> runtime._normalize_actions(resources, ['dynamodb', 'us-west-1'])
        >>> pprint(normalize_dict(resources))
        {'table/sometable': {'*'}, 'table/sometable/stream/somestream': {'*'}}
        """
        for resource, actions in tuple(resources.items()):
            if not actions:
                # if there are no other resources with common name that *do* have actions
                if any((other_resource.startswith(resource.rstrip('*'))
                        or resource.startswith(other_resource.rstrip('*')))
                       and other_actions.difference({'*'})
                       for other_resource, other_actions in resources.items()):
                    # then it's fine
                    del resources[resource]
                else:
                    actions.add('*')
                    eprint(
                        "warn: unknown actions for '{}:{}', couldn't find any relevant SDK methods in your code, falling back to '*'",
                        ':'.join(parents), resource)
            elif '*' in actions:
                actions.clear()
                actions.add('*')
Example #19
0
    def get_client(self, service, region, account):
        """
        >>> from pprint import pprint
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Session:
        ...     def client(self, *args, **kwargs):
        ...         return (args, kwargs)
        >>> provider = AwsApi()
        >>> provider.config = {}
        >>> provider.session = Session()
        >>> provider.default_account = 'default_account'

        >>> class Args:
        ...     pass
        >>> args = Args()
        >>> args.no_input = False
        >>> provider.args = args

        >>> pprint(provider.get_client('dynamodb', 'us-east-1', 'default_account'))
        (('dynamodb',), {'region_name': 'us-east-1'})

        >>> mock.mock(None, 'eprint')
        >>> pprint(provider.get_client('dynamodb', 'us-east-1', '*'))
        (('dynamodb',), {'region_name': 'us-east-1'})
        >>> mock.calls_for('eprint')
        "warn: unknown account ('*'), using default session"

        >>> class Session:
        ...     def client(self, *args, **kwargs):
        ...         return (args, kwargs)
        >>> mock.mock(boto3, 'Session', Session())
        >>> mock.mock(None, 'input', lambda message: 'dummy')
        >>> pprint(provider.get_client('dynamodb', 'us-east-1', 'another_account'))
        (('dynamodb',), {'region_name': 'us-east-1'})
        >>> mock.calls_for('boto3.Session')
        profile_name='dummy'
        >>> pprint(provider.config)
        {'aws': {'accounts': {'another_account': {'profile': 'dummy'}}}}
        """

        client = AwsApi.CLIENTS_CACHE.get((service, region, account))
        if client:
            return client # from cache

        if region == '*':
            eprint("warn: unknown region ('*'), using the default ('{}')", self.default_region)
            region = self.default_region

        if account == '*':
            eprint("warn: unknown account ('*'), using default session")
            client = self.session.client(
                service,
                region_name=region
            )
        elif account == self.default_account:
            client = self.session.client(
                service,
                region_name=region
            )
        elif self.args.no_input:
            eprint("warn: unknown account ('{}') and --no-input set, using default session", account)
            client = self.session.client(
                service,
                region_name=region
            )
        else:
            account_config = self.config.setdefault('aws', {}).setdefault('accounts', {}).setdefault(account, {})
            if not 'profile' in account_config:
                account_config['profile'] = input("Enter configured AWS profile for {}: ".format(account))
            client = boto3.Session(profile_name=account_config['profile']).client(service, region_name=region)

        AwsApi.CLIENTS_CACHE[(service, region, account)] = client
        return client
Example #20
0
    def _walk(self, processor, *args, **kwargs):
        """
        >>> from collections import namedtuple
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> mock.mock(pkg_resources, 'resource_filename', "/path/to/list-dependencies.py")

        >>> processed = []
        >>> def processor(filename, contents, custom_positional, custom_keyword):
        ...     processed.append((filename, contents, custom_positional, custom_keyword))

        >>> def stat(self, filename):
        ...     return namedtuple('Stat', ('st_size',))(5*1024*1024 if filename == "/path/to/function/large-file" else 512)
        >>> mock.mock(PythonRuntime, '_stat', stat)

        >>> mock.filesystem = {'': {'path': {'to': {'function': {
        ...     'large-file': True,
        ...     'config': True,
        ...     'unreferenced': True,
        ... }}}}}
        >>> with mock.open("/path/to/function/config", 'w') as f:
        ...     f.write("some config") and None
        >>> with mock.open("/path/to/function/src/index.py", 'w') as f:
        ...     f.write("some code config large-file more code") and None

        >>> mock.mock(subprocess, 'check_output', b"/path/to/function/src/index.py\\n")
        >>> PythonRuntime('/path/to/function', resource_properties={'Handler': "src/index.handler", 'Runtime': 'python2.7'}, provider=object()) \\
        ...     ._walk(processor, 'positional', custom_keyword='keyword')
        >>> mock.calls_for('subprocess.check_output')
        ['python2.7', '/path/to/list-dependencies.py', '/path/to/function/src/index.py', '/path/to/function'], stderr=-2
        >>> processed
        [('/path/to/function/src/index.py', 'some code config large-file more code', 'positional', 'keyword'),
         ('/path/to/function/config', 'some config', 'positional', 'keyword')]
        """

        if hasattr(self, '_dependencies'):
            # cached
            for filename in self._dependencies:
                with open(filename, 'r', errors='replace') as file:
                    processor(filename, file.read(), *args, **kwargs)
            return

        # getting main Python file (from Handler)
        handler = self.resource_properties.get('Handler')
        if not handler:
            # dummy CloudFormation? walking everything
            super()._walk(processor, *args, **kwargs)
            return
        module = '.'.join(handler.split(
            '.')[0:-1])  # all except the last part which is the method
        filename = os.path.abspath(
            os.path.join(self.root, "{}.py".format(module.replace('.', '/'))))
        if not os.path.exists(filename):
            return

        # acquiring dependencies with the correct Python version using resources/list-dependencies.py script
        list_dependencies_script_path = pkg_resources.resource_filename(
            'puresec_cli', 'resources/list-dependencies.py')
        python_executable = self.resource_properties[
            'Runtime']  # e.g 'python2.7'
        try:
            dependencies = subprocess.check_output([
                python_executable, list_dependencies_script_path, filename,
                self.root
            ],
                                                   stderr=subprocess.STDOUT)
        except FileNotFoundError:
            eprint("error: function runtime ({}) must be installed",
                   python_executable)
            raise SystemExit(-1)
        except subprocess.CalledProcessError as e:
            eprint("error: failed to get dependency tree:\n{}",
                   e.output.decode())
            raise SystemExit(-1)

        dependencies = dependencies.decode().split('\n')
        dependencies.pop()  # last empty line
        self._dependencies = dependencies[:]  # cache

        # getting all non-dependency files
        resources = []  # (abspath, filename)
        for path, dirs, filenames in os.walk(self.root):
            paths_generator = (
                (os.path.abspath(os.path.join(path, filename)), filename)
                for filename in filenames
                if not PythonRuntime.PYTHON_FILENAME_PATTERN.search(filename))
            resources.extend(paths_tuple for paths_tuple in paths_generator
                             if self._stat(paths_tuple[0]).st_size <
                             PythonRuntime.MAX_FILE_SIZE)

        while dependencies:
            filename = dependencies.pop(0)
            with open(filename, 'r', errors='replace') as file:
                # adding resources referenced by current file
                used_resources_indexes = []
                contents = file.read()
                for index, (resource_abspath,
                            resource_filename) in enumerate(resources):
                    if resource_filename in contents:
                        dependencies.append(resource_abspath)
                        self._dependencies.append(resource_abspath)
                        used_resources_indexes.append(index)
                for index in reversed(used_resources_indexes):
                    resources.pop(index)
                # processing current file
                processor(filename, contents, *args, **kwargs)
Example #21
0
    def _get_generic_all_resources(self,
                                   service,
                                   region,
                                   account,
                                   template_type,
                                   api_method,
                                   api_attribute,
                                   api_inner_attribute=None,
                                   resource_converter=None,
                                   api_kwargs={},
                                   warn=True):
        """
        >>> from pprint import pprint
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Runtime(BaseApi):
        ...     pass
        >>> runtime = Runtime()

        >>> class Provider:
        ...     pass
        >>> runtime.provider = Provider()

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'TableNames': []})

        >>> runtime.provider.cloudformation_template = {'Resources': {'T1': {'Type': 'AWS::DynamoDB::Table', 'Properties': {'TableName': 'table-1'}},
        ...                                                           'T2': {'Type': 'AWS::DynamoDB::Table', 'Properties': {'TableName': 'table-2'}},
        ...                                                           'B1': {'Type': 'AWS::S3::Bucket', 'Properties': {'TableName': 'not-table-2'}}}}
        >>> pprint(runtime._get_generic_all_resources('dynamodb', 'us-east-1', 'some-account', 'AWS::DynamoDB::Table', 'list_tables', 'TableNames'))
        {'table-1': re.compile('\\\\btable\\\\-1\\\\b', re.IGNORECASE),
         'table-2': re.compile('\\\\btable\\\\-2\\\\b', re.IGNORECASE)}
        >>> mock.calls_for('Provider.get_cached_api_result')
        'dynamodb', account='some-account', api_kwargs={}, api_method='list_tables', region='us-east-1'

        >>> runtime.provider.cloudformation_template = None

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'TableNames': ['table-1', 'table-2']})

        >>> pprint(runtime._get_generic_all_resources('dynamodb', 'us-east-1', 'some-account', 'AWS::DynamoDB::Table', 'list_tables', 'TableNames'))
        {'table-1': re.compile('\\\\btable\\\\-1\\\\b', re.IGNORECASE),
         'table-2': re.compile('\\\\btable\\\\-2\\\\b', re.IGNORECASE)}
        >>> mock.calls_for('Provider.get_cached_api_result')
        'dynamodb', account='some-account', api_kwargs={}, api_method='list_tables', region='us-east-1'

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'Buckets': [{'Name': "bucket-1"}, {'Name': "bucket-2"}]})

        >>> pprint(runtime._get_generic_all_resources('s3', 'us-east-1', 'some-account', 'AWS::S3::Bucket', 'list_buckets', 'Buckets', 'Name'))
        {'bucket-1': re.compile('\\\\bbucket\\\\-1\\\\b', re.IGNORECASE),
         'bucket-2': re.compile('\\\\bbucket\\\\-2\\\\b', re.IGNORECASE)}
        >>> mock.calls_for('Provider.get_cached_api_result')
        's3', account='some-account', api_kwargs={}, api_method='list_buckets', region='us-east-1'

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'Topics': [{'TopicArn': "arn:aws:sns:us-east-1:123456789012:my_topic"}]})

        >>> pprint(runtime._get_generic_all_resources('sns', 'us-east-1', 'some-account', 'AWS::SNS::Topic', 'list_topics', 'Topics', 'TopicArn',
        ...                                           resource_converter=lambda topic_arn: BaseApi.ARN_RESOURCE_PATTERN.match(topic_arn).group(1)))
        {'my_topic': re.compile('\\\\bmy_topic\\\\b', re.IGNORECASE)}
        >>> mock.calls_for('Provider.get_cached_api_result')
        'sns', account='some-account', api_kwargs={}, api_method='list_topics', region='us-east-1'

        >>> mock.mock(None, 'eprint')

        >>> mock.mock(runtime.provider, 'get_cached_api_result', {'TableNames': []})

        >>> runtime._get_generic_all_resources('dynamodb', 'us-east-1', 'some-account', 'AWS::DynamoDB::Table', 'list_tables', 'TableNames')
        {}
        >>> mock.calls_for('eprint')
        "warn: no {} resources ({}) on '{}:{}', you're using this service but your AWS account and CloudFormation are empty", 'dynamodb', 'AWS::DynamoDB::Table', 'us-east-1', 'some-account'
        >>> mock.calls_for('Provider.get_cached_api_result')
        'dynamodb', account='some-account', api_kwargs={}, api_method='list_tables', region='us-east-1'
        """

        resources = {}

        if self.provider.cloudformation_template and template_type:
            name_attribute = "{}Name".format(template_type.split('::')[-1])
            for logical_id, properties in self.provider.cloudformation_template.get(
                    'Resources', {}).items():
                if properties.get('Type') == template_type:
                    resource = properties.get('Properties',
                                              {}).get(name_attribute)
                    if resource:
                        resources[resource] = re.compile(
                            BaseApi.RESOURCE_PATTERN.format(
                                re.escape(resource)), re.IGNORECASE)

        api_resources = self.provider.get_cached_api_result(
            service,
            region=region,
            account=account,
            api_method=api_method,
            api_kwargs=api_kwargs)[api_attribute]

        if api_inner_attribute:
            api_resources = (resource[api_inner_attribute]
                             for resource in api_resources)
        if resource_converter:
            api_resources = (resource_converter(resource)
                             for resource in api_resources)

        resources.update(
            (resource,
             re.compile(BaseApi.RESOURCE_PATTERN.format(re.escape(resource)),
                        re.IGNORECASE)) for resource in api_resources)

        if not resources and warn:
            if not hasattr(self, '_no_resources_warnings'):
                self._no_resources_warnings = set()
            warning_arguments = (service, template_type, region, account)
            if warning_arguments not in self._no_resources_warnings:
                eprint(
                    "warn: no {} resources ({}) on '{}:{}', you're using this service but your AWS account and CloudFormation are empty",
                    *warning_arguments)
                self._no_resources_warnings.add(warning_arguments)
            return {}

        return resources
Example #22
0
    def _reference_roles(self, permissions, config):
        """
        >>> from pprint import pprint
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Args:
        ...     pass
        >>> args = Args()
        >>> args.yes = True

        >>> permissions = {'some': "some permissions", 'another': "another permissions"}

        >>> config = {'functions': {'some': {}, 'another': {}}}
        >>> ServerlessFramework("path/to/project", {}, args=args)._reference_roles(permissions, config)
        >>> pprint(config)
        {'functions': {'another': {'role': 'puresecAnotherRole'}, 'some': {'role': 'puresecSomeRole'}}}

        >>> mock.mock(None, 'eprint')

        >>> config = {'functions': {'another': {}}}
        >>> ServerlessFramework("path/to/project", {}, args=args)._reference_roles(permissions, config)
        >>> pprint(config)
        {'functions': {'another': {'role': 'puresecAnotherRole'}}}
        >>> mock.calls_for('eprint')
        'warn: `{}` not found under the `functions` section in serverless.yml', 'some'

        >>> config = {}
        >>> ServerlessFramework("path/to/project", {}, args=args)._reference_roles(permissions, config)
        >>> pprint(config)
        {}
        >>> mock.calls_for('eprint')
        'warn: `functions` section not found in serverless.yml'

        >>> args.yes = False
        >>> args.reference = False
        >>> args.no_reference = False
        >>> args.no_input = False

        >>> mock.mock(None, 'input_query', False)
        >>> config = {'functions': {'some': {}, 'another': {}}}
        >>> ServerlessFramework("path/to/project", {}, args=args)._reference_roles(permissions, config)
        >>> pprint(config)
        {'functions': {'another': {}, 'some': {}}}
        >>> mock.calls_for('input_query')
        'Reference functions to new roles?{}', ''

        >>> mock.mock(None, 'input_query', True)
        >>> config = {'functions': {'some': {}, 'another': {}}}
        >>> ServerlessFramework("path/to/project", {}, args=args)._reference_roles(permissions, config)
        >>> pprint(config)
        {'functions': {'another': {'role': 'puresecAnotherRole'}, 'some': {'role': 'puresecSomeRole'}}}
        >>> mock.calls_for('input_query')
        'Reference functions to new roles?{}', ''
        """

        if 'functions' not in config:
            eprint("warn: `functions` section not found in serverless.yml")
            return

        if (self.args.yes or self.args.reference) or (
                not self.args.no_reference
                and not self.args.no_input and input_query(
                    "Reference functions to new roles?{}", self.query_suffix)):
            for name in permissions.keys():
                if name not in config['functions']:
                    eprint(
                        "warn: `{}` not found under the `functions` section in serverless.yml",
                        name)
                    continue
                config['functions'][name]['role'] = "puresec{}Role".format(
                    capitalize(name))
Example #23
0
    def _get_services(self, filename, contents):
        """
        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Provider:
        ...     pass
        >>> provider = Provider()
        >>> provider.default_region = 'default_region'
        >>> provider.default_account = 'default_account'

        >>> runtime = PythonRuntime('path/to/function', resource_properties={}, provider=provider)

        >>> runtime._get_services("filename.txt", ".client('s3')")
        >>> pprint(normalize_dict(runtime._permissions))
        {}

        >>> runtime._get_services("filename.py", ".client('s3')")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'default_account': {}}}}
        >>> runtime._permissions.clear()

        >>> runtime._get_services("filename.py", ".client('s3', region_name='localhost')")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'default_account': {}}}}
        >>> runtime._permissions.clear()

        >>> runtime._get_services("filename.py", ".client('s3', region_name='us-east-1')")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.py", '''
        ... boto3. \\
        ...     client('s3',
        ...         region_name='us-east-1'
        ...     )
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.py", '''
        ... boto3.
        ...     client('s3',
        ...         region_name='us-east-1', something='else'
        ...     )
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> mock.mock(None, 'eprint')

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.py", '''
        ... boto3.
        ...     client('s3',
        ...         region_name=getRegion()
        ...     )
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'*': {'default_account': {}}}}
        >>> mock.calls_for('eprint')
        'warn: incomprehensive region: {} (in {})', "'s3',\\n        region_name=getRegion()\\n    ", 'filename.py'

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.py", '''
        ... boto3.
        ...     client('s3',
        ...         region_name='us-' + region
        ...     )
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'*': {'default_account': {}}}}
        >>> mock.calls_for('eprint')
        'warn: incomprehensive region: {} (in {})', "'s3',\\n        region_name='us-' + region\\n    ", 'filename.py'

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.py", '''
        ... boto3.
        ...     client('s3',
        ...         aws_access_key_id='some key'
        ...     )
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'*': {}}}}
        >>> mock.calls_for('eprint')
        'warn: unknown account: {} (in {})', "'s3',\\n        aws_access_key_id='some key'\\n    ", 'filename.py'
        """

        if not PythonRuntime.PYTHON_FILENAME_PATTERN.search(filename):
            return

        for service, pattern in PythonApi.SERVICE_CALL_PATTERNS:
            for service_match in pattern.finditer(contents):
                arguments = get_inner_parentheses(service_match.group(1))
                if arguments:
                    # region
                    region = self._get_variable_from_arguments(
                        arguments, PythonRuntime.REGION_PATTERN)
                    if region is None or region == 'localhost':
                        region = self.provider.default_region
                    elif not region:
                        eprint("warn: incomprehensive region: {} (in {})",
                               arguments, filename)
                        region = '*'
                    elif not any(
                            pattern.match(region) for pattern in
                            PythonRuntime.REGION_PATTERNS.values()):
                        eprint("warn: incomprehensive region: {} (in {})",
                               arguments, filename)
                        region = '*'
                    # account
                    if PythonRuntime.AUTH_PATTERN.search(arguments):
                        eprint("warn: unknown account: {} (in {})", arguments,
                               filename)
                        account = '*'
                    else:
                        account = self.provider.default_account
                else:
                    region = self.provider.default_region
                    account = self.provider.default_account

                self._permissions[service][region][
                    account]  # accessing to initialize defaultdict
Example #24
0
    def process(self):
        """
        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)
        >>> mock.mock(None, 'eprint')
        >>> mock.mock(AwsProvider, '_process_configurations')
        >>> with mock.open("path/to/cloudformation.json", 'w') as f:
        ...     f.write('{}') and None
        >>> from puresec_cli.actions.generate_roles.frameworks.base import Base as FrameworkBase
        >>> class Framework(FrameworkBase):
        ...     def get_function_name(self, name):
        ...         return name[1:]

        >>> from puresec_cli.actions.generate_roles.runtimes.aws.base import Base as RuntimeBase
        >>> class RuntimeModule:
        ...     class Runtime(RuntimeBase):
        ...         def process(self):
        ...             pass
        >>> mock.mock(None, 'import_module', lambda name: RuntimeModule)

        >>> handler = AwsProvider("path/to/project", config={}, resource_template="path/to/cloudformation.json", framework=Framework("", {}))
        >>> mock.mock(AwsProvider, 'default_region', "default_region")
        >>> mock.mock(AwsProvider, 'default_account', "default_account")
        >>> mock.mock(handler, '_get_function_root', lambda name: "functions/{}".format(name))

        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'ResourceId': {
        ...             'Type': 'NotLambda'
        ...         }
        ...     }
        ... })
        >>> handler.process()
        >>> handler._function_permissions
        {}

        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'ResourceId': {
        ...             'Type': 'AWS::Lambda::Function'
        ...         }
        ...     }
        ... })
        >>> handler.process()
        Traceback (most recent call last):
        SystemExit: 2
        >>> mock.calls_for('eprint')
        'error: lambda name not specified at `{}`', 'ResourceId'

        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'ResourceId': {
        ...             'Type': 'AWS::Lambda::Function',
        ...             'Properties': {
        ...                 'FunctionName': "-functionName"
        ...             }
        ...         }
        ...     }
        ... })
        >>> handler.process()
        Traceback (most recent call last):
        SystemExit: 2
        >>> mock.calls_for('eprint')
        'error: lambda runtime not specified for `{}`', 'functionName'

        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'ResourceId': {
        ...             'Type': 'AWS::Lambda::Function',
        ...             'Properties': {
        ...                 'FunctionName': "-functionName",
        ...                 'Runtime': "abc4.3"
        ...             }
        ...         }
        ...     }
        ... })
        >>> handler.process()
        >>> mock.calls_for('eprint')
        'warn: lambda runtime not yet supported: `{}` (for `{}`)', 'abc', 'functionName'
        >>> handler._function_permissions
        {}

        >>> mock.mock(AwsProvider, 'cloudformation_template', None)
        >>> handler.runtime = 'nodejs'
        >>> handler.process()
        >>> mock.calls_for('import_module')
        'puresec_cli.actions.generate_roles.runtimes.aws.nodejs'
        >>> list(handler._function_permissions.keys())
        ['nnamed']

        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'ResourceId': {
        ...             'Type': 'AWS::Lambda::Function',
        ...             'Properties': {
        ...                 'FunctionName': "-functionName",
        ...                 'Runtime': "nodejs4.3"
        ...             }
        ...         }
        ...     }
        ... })
        >>> handler.process()
        >>> mock.calls_for('import_module')
        'puresec_cli.actions.generate_roles.runtimes.aws.nodejs'
        >>> list(handler._function_permissions.keys())
        ['functionName']

        >>> handler.function = 'functionOne'
        >>> mock.mock(AwsProvider, 'cloudformation_template', {
        ...     'Resources': {
        ...         'FunctionOneId': {
        ...             'Type': 'AWS::Lambda::Function',
        ...             'Properties': {
        ...                 'FunctionName': "-functionOne",
        ...                 'Runtime': "nodejs4.3"
        ...             }
        ...         },
        ...         'FunctionTwoId': {
        ...             'Type': 'AWS::Lambda::Function',
        ...             'Properties': {
        ...                 'FunctionName': "-functionTwo",
        ...                 'Runtime': "nodejs4.3"
        ...             }
        ...         }
        ...     }
        ... })
        >>> handler.process()
        >>> list(handler._function_permissions.keys())
        ['functionOne']
        """
        self._function_real_names = {}
        self._function_permissions = {}

        if self.cloudformation_template:
            resources = self.cloudformation_template.get('Resources', {})
        else:
            function_name = self.function_name or 'Unnamed'
            resources = {
                '{}Function'.format(camelcase(function_name)): {
                    'Type': 'AWS::Lambda::Function',
                    'Properties': {
                        'FunctionName': function_name,
                        'Runtime': self.runtime,
                        'Handler': self.handler,
                    }
                }
            }

        for resource_id, resource_config in resources.items():
            if resource_config.get('Type') == 'AWS::Lambda::Function':
                # Getting name
                name = resource_config.get('Properties', {}).get('FunctionName')
                if not name:
                    eprint("error: lambda name not specified at `{}`", resource_id)
                    raise SystemExit(2)
                if self.framework:
                    name = self.framework.get_function_name(name)

                if self.function and self.function != name:
                    continue

                root = os.path.join(self.path, self._get_function_root(name))
                # Getting runtime
                runtime = resource_config.get('Properties', {}).get('Runtime')
                if not runtime:
                    eprint("error: lambda runtime not specified for `{}`", name)
                    raise SystemExit(2)

                runtime = re.sub(r"[\d\.]+$", '', runtime) # ignoring runtime version (e.g nodejs4.3)

                if runtime not in runtimes.__all__:
                    eprint("warn: lambda runtime not yet supported: `{}` (for `{}`)", runtime, name)
                    continue

                runtime = import_module("puresec_cli.actions.generate_roles.runtimes.aws.{}".format(runtime)).Runtime(
                    root,
                    resource_properties=resource_config['Properties'],
                    provider=weakref.proxy(self),
                )

                runtime.process()
                self._function_permissions[name] = runtime.permissions
                self._process_configurations(name, resource_id, resource_config)
Example #25
0
    def _get_services(self, filename, contents):
        """
        >>> from pprint import pprint
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> class Provider:
        ...     pass
        >>> provider = Provider()
        >>> provider.default_region = 'default_region'
        >>> provider.default_account = 'default_account'

        >>> runtime = NodejsRuntime('path/to/function', resource_properties={}, provider=provider)

        >>> runtime._get_services("filename.txt", ".S3()")
        >>> pprint(normalize_dict(runtime._permissions))
        {}

        >>> runtime._get_services("filename.js", ".S3()")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'default_account': {}}}}
        >>> runtime._permissions.clear()

        >>> runtime._get_services("filename.js", ".S3({ region: 'localhost' })")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'default_account': {}}}}
        >>> runtime._permissions.clear()

        >>> runtime._get_services("filename.js", ".S3({ region: 'us-east-1' })")
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.js", '''
        ... aws.
        ...     S3({
        ...         region: 'us-east-1'
        ...     })
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.js", '''
        ... aws.
        ...     S3({
        ...         region: 'us-east-1', something: 'else'
        ...     })
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'us-east-1': {'default_account': {}}}}

        >>> mock.mock(None, 'eprint')

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.js", '''
        ... aws.
        ...     S3({
        ...         region: getRegion()
        ...     })
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'*': {'default_account': {}}}}
        >>> mock.calls_for('eprint')
        "warn: incomprehensive region: {} (in {}), falling back to '*'", '{\\n        region: getRegion()\\n    }', 'filename.js'

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.js", '''
        ... aws.
        ...     S3({
        ...         region: 'us-' + region
        ...     })
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'*': {'default_account': {}}}}
        >>> mock.calls_for('eprint')
        "warn: incomprehensive region: {} (in {}), falling back to '*'", "{\\n        region: 'us-' + region\\n    }", 'filename.js'

        >>> runtime._permissions.clear()
        >>> runtime._get_services("filename.js", '''
        ... aws.
        ...     S3({
        ...         accessKeyId: "some key"
        ...     })
        ... ''')
        >>> pprint(normalize_dict(runtime._permissions))
        {'s3': {'default_region': {'*': {}}}}
        >>> mock.calls_for('eprint')
        "warn: unknown account: {} (in {}), falling back to '*'", '{\\n        accessKeyId: "some key"\\n    }', 'filename.js'
        """

        if not NodejsRuntime.JAVASCRIPT_FILENAME_PATTERN.search(filename):
            return

        for service, pattern in NodejsApi.SERVICE_CALL_PATTERNS:
            for service_match in pattern.finditer(contents):
                arguments = get_inner_parentheses(service_match.group(1))
                if arguments:
                    # region
                    region = self._get_variable_from_arguments(
                        arguments, NodejsRuntime.REGION_PATTERN)
                    if region is None or region == 'localhost':
                        region = self.provider.default_region
                    elif not region:
                        eprint(
                            "warn: incomprehensive region: {} (in {}), falling back to '*'",
                            arguments, filename)
                        region = '*'
                    elif not any(
                            pattern.match(region) for pattern in
                            NodejsRuntime.REGION_PATTERNS.values()):
                        eprint(
                            "warn: incomprehensive region: {} (in {}), falling back to '*'",
                            arguments, filename)
                        region = '*'
                    # account
                    if NodejsRuntime.AUTH_PATTERN.search(arguments):
                        eprint(
                            "warn: unknown account: {} (in {}), falling back to '*'",
                            arguments, filename)
                        account = '*'
                    else:
                        account = self.provider.default_account
                else:
                    region = self.provider.default_region
                    account = self.provider.default_account

                self._permissions[service][region][
                    account]  # accessing to initialize defaultdict
Example #26
0
    def _process_stream_configuration(self, name, resource_id, resource_config):
        """
        >>> from tests.utils import normalize_dict
        >>> from tests.mock import Mock
        >>> mock = Mock(__name__)

        >>> provider = AwsApi()
        >>> provider.default_region = 'us-east-1'
        >>> provider.default_account = '1234'
        >>> mock.mock(provider, 'get_cached_api_result', {'EventSourceMappings': []})

        >>> provider.cloudformation_template = {'Resources': {'Mapping': {'Type': 'AWS::Lambda::EventSourceMapping',
        ...                                                               'Properties': {'FunctionName': 'SomeFunction',
        ...                                                                              'EventSourceArn': 'arn:aws:kinesis:us-east-1:1234:stream/SomeStream'}}}}
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> normalize_dict(provider._function_permissions)
        {'functionName': {'arn:aws:kinesis:us-east-1:1234:stream/SomeStream': {'kinesis:DescribeStream', 'kinesis:GetRecords', 'kinesis:GetShardIterator', 'kinesis:ListStreams'}}}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> provider.cloudformation_template = {'Resources': {'Mapping': {'Type': 'AWS::Lambda::EventSourceMapping',
        ...                                                               'Properties': {'FunctionName': 'arn:aws:lambda:us-east-1:1234:function:SomeFunction',
        ...                                                                              'EventSourceArn': 'arn:aws:kinesis:us-east-1:1234:stream/SomeStream'}}}}
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> normalize_dict(provider._function_permissions)
        {'functionName': {'arn:aws:kinesis:us-east-1:1234:stream/SomeStream': {'kinesis:DescribeStream', 'kinesis:GetRecords', 'kinesis:GetShardIterator', 'kinesis:ListStreams'}}}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> provider.cloudformation_template = {'Resources': {'Mapping': {'Type': 'AWS::Lambda::EventSourceMapping',
        ...                                                               'Properties': {'FunctionName': 'AnotherFunction',
        ...                                                                              'EventSourceArn': 'arn:aws:kinesis:us-east-1:1234:stream/SomeStream'}}}}
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> provider._function_permissions
        {}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> provider.cloudformation_template = {'Resources': {'Mapping': {'Type': 'AWS::Lambda::EventSourceMapping',
        ...                                                               'Properties': {'FunctionName': 'SomeFunction',
        ...                                                                              'EventSourceArn': 'arn:aws:dynamodb:us-east-1:1234:table/SomeTable'}}}}
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> provider._function_permissions
        {}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> provider.cloudformation_template = {'Resources': {'Mapping': {'Type': 'AWS::DynamoDB::Table'}}}
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> provider._function_permissions
        {}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> provider.cloudformation_template = None

        >>> mock.mock(provider, 'get_cached_api_result', {'EventSourceMappings': [{'EventSourceArn': 'arn:aws:kinesis:us-east-1:1234:stream/SomeStream'}]})
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> normalize_dict(provider._function_permissions)
        {'functionName': {'arn:aws:kinesis:us-east-1:1234:stream/SomeStream': {'kinesis:DescribeStream', 'kinesis:GetRecords', 'kinesis:GetShardIterator', 'kinesis:ListStreams'}}}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'

        >>> mock.mock(provider, 'get_cached_api_result', {'EventSourceMappings': [{'EventSourceArn': 'arn:aws:dynamodb:us-east-1:1234:table/SomeTable'}]})
        >>> provider._function_permissions = {}
        >>> provider._process_stream_configuration('functionName', 'SomeFunctionName', {'Properties': {'FunctionName': 'SomeFunction'}})
        >>> provider._function_permissions
        {}
        >>> mock.calls_for('AwsApi.get_cached_api_result')
        'lambda', account='1234', api_kwargs={'FunctionName': 'SomeFunction'}, api_method='list_event_source_mappings', region='us-east-1'
        """

        function_name = resource_config['Properties']['FunctionName']
        # From CloudFormation
        if self.cloudformation_template:
            function_id_pattern = re.compile(AwsApi.FUNCTION_ID_PATTERN.format(function_name, resource_id), re.IGNORECASE)
            for other_resource_id, other_resource_config in self.cloudformation_template.get('Resources', {}).items():
                if other_resource_config.get('Type') == 'AWS::Lambda::EventSourceMapping':
                    # either ARN, function name, or broken intrinsic function
                    target = other_resource_config.get('Properties', {}).get('FunctionName')
                    if target and function_id_pattern.search(target):
                        arn = other_resource_config['Properties'].get('EventSourceArn')
                        if not arn:
                            eprint("warn: event source mapping for `{}` missing `EventSourceArn`", name)
                            continue

                        service_match = AwsApi.STREAM_ARN_SERVICE_PATTERN.match(arn)
                        if not service_match:
                            continue
                        service = service_match.group(1)

                        self._function_permissions. \
                            setdefault(name, {}).   \
                            setdefault(arn, set()). \
                            update("{}:{}".format(service, action) for action in AwsApi.STREAM_ACTIONS)

        # From production environment
        event_source_mappings = self.get_cached_api_result('lambda', region=self.default_region, account=self.default_account, api_method='list_event_source_mappings', api_kwargs={'FunctionName': function_name})
        for event_source_mapping in event_source_mappings['EventSourceMappings']:
            service_match = AwsApi.STREAM_ARN_SERVICE_PATTERN.match(event_source_mapping['EventSourceArn'])
            if not service_match:
                continue
            service = service_match.group(1)

            self._function_permissions. \
                setdefault(name, {}).   \
                setdefault(event_source_mapping['EventSourceArn'], set()). \
                update("{}:{}".format(service, action) for action in AwsApi.STREAM_ACTIONS)