diff --git a/cloudiscovery/__init__.py b/cloudiscovery/__init__.py index b5b82b5..2b17458 100644 --- a/cloudiscovery/__init__.py +++ b/cloudiscovery/__init__.py @@ -32,6 +32,7 @@ from provider.iot.command import Iot from provider.all.command import All from provider.limit.command import Limit +from provider.security.command import Security from shared.common import ( exit_critical, @@ -107,6 +108,20 @@ def generate_parser(): For example: --threshold 50 will report all resources with more than 50%% threshold.", ) + security_parser = subparsers.add_parser( + "aws-security", help="Analyze aws several security checks." + ) + add_default_arguments(security_parser, diagram_enabled=False, filters_enabled=False) + security_parser.add_argument( + "-c", + "--commands", + action="append", + required=False, + help='Select the security check command that you want to run. \ + To see available commands, please type "-c list". \ + If not passed, command will check all services.', + ) + return parser @@ -262,12 +277,18 @@ def main(): command = Limit( region_names=region_names, session=session, threshold=args.threshold, ) + elif args.command == "aws-security": + command = Security( + region_names=region_names, session=session, commands=args.commands, + ) else: raise NotImplementedError("Unknown command") + if "services" in args and args.services is not None: services = args.services.split(",") else: services = [] + command.run(diagram, args.verbose, services, filters) diff --git a/cloudiscovery/provider/security/__init__.py b/cloudiscovery/provider/security/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudiscovery/provider/security/command.py b/cloudiscovery/provider/security/command.py new file mode 100644 index 0000000..82226ea --- /dev/null +++ b/cloudiscovery/provider/security/command.py @@ -0,0 +1,70 @@ +from typing import List + +from shared.common import ( + ResourceCache, + Filterable, + BaseOptions, +) +from shared.common_aws import BaseAwsOptions, BaseAwsCommand, AwsCommandRunner +from shared.diagram import NoDiagram + + +class SecurityOptions(BaseAwsOptions, BaseOptions): + commands: List[str] + + # pylint: disable=too-many-arguments + def __init__( + self, verbose: bool, filters: List[Filterable], session, region_name, commands, + ): + BaseAwsOptions.__init__(self, session, region_name) + BaseOptions.__init__(self, verbose, filters) + self.commands = commands + + +class SecurityParameters: + def __init__(self, session, region: str, commands, options: SecurityOptions): + self.region = region + self.cache = ResourceCache() + self.session = session + self.options = options + self.commands = commands + + +class Security(BaseAwsCommand): + def __init__(self, region_names, session, commands): + """ + All AWS resources + + :param region_names: + :param session: + :param commands: + """ + super().__init__(region_names, session) + self.commands = commands + + def run( + self, + diagram: bool, + verbose: bool, + services: List[str], + filters: List[Filterable], + ): + + for region in self.region_names: + security_options = SecurityOptions( + verbose=verbose, + filters=filters, + session=self.session, + region_name=region, + commands=self.commands, + ) + + command_runner = AwsCommandRunner() + command_runner.run( + provider="security", + options=security_options, + diagram_builder=NoDiagram(), + title="AWS Security - Region {}".format(region), + # pylint: disable=no-member + filename=security_options.resulting_file_name("security"), + ) diff --git a/cloudiscovery/provider/security/data/__init__.py b/cloudiscovery/provider/security/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudiscovery/provider/security/data/commands_enabled.py b/cloudiscovery/provider/security/data/commands_enabled.py new file mode 100644 index 0000000..5d29932 --- /dev/null +++ b/cloudiscovery/provider/security/data/commands_enabled.py @@ -0,0 +1,8 @@ +COMMANDS_ENABLED = { + "access-keys-rotated": { + "parameters": [{"name": "max_age", "default_value": "90", "type": "int"}], + "class": "IAM", + "method": "access_keys_rotated", + "short_description": "Checks whether the active access keys are rotated within the number of days.", + }, +} diff --git a/cloudiscovery/provider/security/resource/__init__.py b/cloudiscovery/provider/security/resource/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudiscovery/provider/security/resource/all.py b/cloudiscovery/provider/security/resource/all.py new file mode 100644 index 0000000..5771bb2 --- /dev/null +++ b/cloudiscovery/provider/security/resource/all.py @@ -0,0 +1,81 @@ +from typing import List + +import importlib + +from provider.security.command import SecurityOptions +from provider.security.data.commands_enabled import COMMANDS_ENABLED +from shared.common import ( + ResourceProvider, + Resource, + message_handler, +) +from shared.error_handler import exception + + +class SecuritytResources(ResourceProvider): + def __init__(self, options: SecurityOptions): + """ + All resources + + :param options: + """ + super().__init__() + self.options = options + + @exception + # pylint: disable=too-many-locals + def get_resources(self) -> List[Resource]: + + commands = self.options.commands + + result = [] + + # commands informed, checking for specific commands + if commands: + # show all commands to check + if commands[0] == "list": + message_handler("\nFollowing commands are enabled\n", "HEADER") + for detail_command in COMMANDS_ENABLED: + parameters = COMMANDS_ENABLED[detail_command]["parameters"][0][ + "name" + ] + default_value = COMMANDS_ENABLED[detail_command]["parameters"][0][ + "default_value" + ] + description = COMMANDS_ENABLED[detail_command]["short_description"] + + formated_command = 'cloudiscovery aws-security -c {}="{}={}"'.format( + detail_command, parameters, default_value + ) + message_handler( + "{} - {} \nExample: {}\n".format( + detail_command, description, formated_command + ), + "OKGREEN", + ) + else: + for command in commands: + command = command.split("=") + + # First position always is command + if command[0] not in COMMANDS_ENABLED: + message_handler( + "Command {} doesn't exists.".format(command[0]), "WARNING" + ) + else: + # Second and thrid parameters are class and method + _class = COMMANDS_ENABLED[command[0]]["class"] + _method = COMMANDS_ENABLED[command[0]]["method"] + _parameter = {command[1]: command[2]} + + module = importlib.import_module( + "provider.security.resource.commands." + _class + ) + instance = getattr(module, _class)(self.options) + result = getattr(instance, _method)(**_parameter) + else: + print( + 'You must inform a command. Run this command using "-c list" to check all commands available' + ) + + return result diff --git a/cloudiscovery/provider/security/resource/commands/IAM.py b/cloudiscovery/provider/security/resource/commands/IAM.py new file mode 100644 index 0000000..f1d44de --- /dev/null +++ b/cloudiscovery/provider/security/resource/commands/IAM.py @@ -0,0 +1,51 @@ +from datetime import datetime, timedelta +import pytz + +from provider.security.command import SecurityOptions + +from shared.common import ( + Resource, + ResourceDigest, + SecurityValues, +) + + +class IAM: + def __init__(self, options: SecurityOptions): + self.options = options + + def access_keys_rotated(self, max_age): + + client = self.options.client("iam") + + users = client.list_users() + + resources_found = [] + + for user in users["Users"]: + paginator = client.get_paginator("list_access_keys") + for keys in paginator.paginate(UserName=user["UserName"]): + for key in keys["AccessKeyMetadata"]: + + date_compare = datetime.utcnow() - timedelta(days=int(max_age)) + date_compare = date_compare.replace(tzinfo=pytz.utc) + last_rotate = key["CreateDate"] + + if last_rotate < date_compare: + resources_found.append( + Resource( + digest=ResourceDigest( + id=key["AccessKeyId"], type="access_keys_rotated" + ), + details="You must rotate your keys.", + name=key["UserName"], + group="iam_security", + security=SecurityValues( + status="CRITICAL", + parameter="max_age", + value=str(max_age), + ), + ) + ) + + return resources_found diff --git a/cloudiscovery/shared/common.py b/cloudiscovery/shared/common.py index 216eb61..acb01a6 100644 --- a/cloudiscovery/shared/common.py +++ b/cloudiscovery/shared/common.py @@ -58,6 +58,12 @@ class LimitsValues(NamedTuple): percent: float +class SecurityValues(NamedTuple): + status: str + parameter: str + value: str + + class ResourceTag(NamedTuple, Filterable): key: str value: str @@ -74,6 +80,7 @@ class Resource(NamedTuple): group: str = "" tags: List[ResourceTag] = [] limits: LimitsValues = None + security: SecurityValues = None attributes: Dict[str, object] = {}