def run(arguments=''): args = parse_args(arguments) if args.html: HTMLReport(args.output).save() if args.verbose: print(f'HTML report saved to {args.output}/report.html') exit(0) rmtree(args.output, ignore_errors=True) Path(args.output).mkdir(parents=True, exist_ok=True) configure_log(args.output) identity = STS(f'arn:aws:sts:{args.region}', args.profile, args.keys[0], args.keys[1]).identity if args.verbose: print(header, end='\n\n') for _ in ['UserId', 'Account', 'Arn']: align(_, identity[_], orange) print('') statistics = Statistics(args.output) visibility = VisibilityReport(args.output) for arn in get_functions(args): if arnparse(arn): arn = arnparse(arn) if args.verbose: count = '[' + f'{statistics.statistics["lambdas"]+1}'.rjust( 4, ' ') + '] ' print(f'\r{green}{count}{arn.resource}{nocolor}'.ljust( 100, ' '), end='') lmbd = Lambda(arn.full, args) statistics.parse(lmbd.report()) visibility.save(lmbd.report()) SecurityReport(args.output).save() HTMLReport(args.output).save() if args.verbose: print('\r' + ' ' * 100, end='\r') # clear align('Lambdas', statistics.statistics["lambdas"]) align('Security', statistics.statistics["security"]["count"]) align('Triggers', statistics.statistics["triggers"]["count"]) align('Resources', statistics.statistics["resources"]["count"]) align('Layers', statistics.statistics["layers"]) align('Runtimes', len(statistics.statistics["runtimes"]["items"])) align('Regions', len(statistics.statistics["regions"]["items"])) align('Report', f'{args.output}/report.html') print('\n')
def get_triggers(self): ''' Tracks events that trigger the Lambda function ''' # Collect triggers from Event Sources try: eventSource = self.client.list_event_source_mappings( FunctionName=self.arn.resource)['EventSourceMappings'] for event in eventSource: if event['State'] != 'Enabled': continue self.triggers['items'][event['EventSourceArn']] = [ 'lambda:InvokeFunction' ] # Track services self.triggers['services'].append( arnparse(event['EventSourceArn']).service) except Exception: debug(self.arn.full) # Collect triggers from Function policy try: if self.policy: for statement in self.policy['Statement']: if 'Condition' in statement: if 'ArnLike' in statement['Condition']: if 'AWS:SourceArn' in statement['Condition'][ 'ArnLike']: arn = statement['Condition']['ArnLike'][ 'AWS:SourceArn'] if type(statement['Action']) == str: self.triggers['items'][arn] = [ statement['Action'] ] else: self.triggers['items'][arn] = statement[ 'Action'] # Track services self.triggers['services'].append( arnparse(arn).service) except Exception: debug(self.arn.full) self.triggers['services'] = list(set(self.triggers['services']))
def get_triggers(self): """ Tracks events that trigger the Lambda function """ # Collect triggers from Event Sources try: eventSource = self.client.list_event_source_mappings( FunctionName=self.arn.resource)["EventSourceMappings"] for event in eventSource: if event["State"] != "Enabled": continue self.triggers["items"][event["EventSourceArn"]] = [ "lambda:InvokeFunction" ] # Track services self.triggers["services"].append( arnparse(event["EventSourceArn"]).service) except Exception: debug(self.arn.full) # Collect triggers from Function policy try: if self.policy: for statement in self.policy["Statement"]: if "Condition" in statement: if "ArnLike" in statement["Condition"]: if "AWS:SourceArn" in statement["Condition"][ "ArnLike"]: arn = statement["Condition"]["ArnLike"][ "AWS:SourceArn"] if type(statement["Action"]) == str: self.triggers["items"][arn] = [ statement["Action"] ] else: self.triggers["items"][arn] = statement[ "Action"] # Track services self.triggers["services"].append( arnparse(arn).service) except Exception: debug(self.arn.full) self.triggers["services"] = list(set(self.triggers["services"]))
def __init__(self, arn, profile=None, access_key_id=None, secret_access_key=None): # AWS ARN self.arn = arnparse(arn) # AWS Profile and Keys self.profile = profile self.access_key_id = access_key_id self.secret_access_key = secret_access_key # AWS Resource-based policy self.policy = {} # Additional service information self.info = '' # AWS connection session = boto3.Session(profile_name=self.profile) self.client = session.client(self.arn.service, region_name=self.arn.region, aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key)
def test_arnparse_apigateway(self): arn_str = 'arn:aws:execute-api:us-east-1:0:api-id/stage/verb/path' arn = arnparse(arn_str) self.assertEqual(arn.service, 'apigateway') self.assertEqual(arn.resource_type, 'api-id') self.assertEqual(arn.resource, 'stage/verb/path') arn_str = 'arn:aws:execute-api:*:*:*' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, '*') self.assertEqual(arn.resource, '') arn_str = 'arn:aws:execute-api:us-east-1:*:api-id/stage/*' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'api-id') self.assertEqual(arn.resource, 'stage/*')
def test_arnparse_apigateway(self): arn_str = "arn:aws:execute-api:us-east-1:0:api-id/stage/verb/path" arn = arnparse(arn_str) self.assertEqual(arn.service, "apigateway") self.assertEqual(arn.resource_type, "api-id") self.assertEqual(arn.resource, "stage/verb/path") arn_str = "arn:aws:execute-api:*:*:*" arn = arnparse(arn_str) self.assertEqual(arn.resource_type, "*") self.assertEqual(arn.resource, "") arn_str = "arn:aws:execute-api:us-east-1:*:api-id/stage/*" arn = arnparse(arn_str) self.assertEqual(arn.resource_type, "api-id") self.assertEqual(arn.resource, "stage/*")
def __init__(self, arn, profile=None, access_key_id=None, secret_access_key=None): super().__init__(arn, profile, access_key_id, secret_access_key) self.caller = self.get_caller_identity() self.arn = arnparse(self.caller['Arn']) self.acl = ACL(self.caller['Arn'])
def test_arnparse_lambda(self): arn_str = "arn:aws:lambda:eu-west-1:0:function:function-name" arn = arnparse(arn_str) self.assertEqual(arn.full, arn_str) self.assertEqual(arn.partition, "aws") self.assertEqual(arn.service, "lambda") self.assertEqual(arn.region, "eu-west-1") self.assertEqual(arn.account_id, "0") self.assertEqual(arn.resource_type, "function") self.assertEqual(arn.resource, "function-name")
def test_arnparse_lambda(self): arn_str = 'arn:aws:lambda:eu-west-1:0:function:function-name' arn = arnparse(arn_str) self.assertEqual(arn.full, arn_str) self.assertEqual(arn.partition, 'aws') self.assertEqual(arn.service, 'lambda') self.assertEqual(arn.region, 'eu-west-1') self.assertEqual(arn.account_id, '0') self.assertEqual(arn.resource_type, 'function') self.assertEqual(arn.resource, 'function-name')
def __init__(self, arn, profile=None, access_key_id=None, secret_access_key=None): # Make sure we use IAM client super().__init__("arn:aws:iam::", profile, access_key_id, secret_access_key) self.arn = arnparse(arn) self.policy_documents = [] self.permissions = [] if self.arn.resource_type == "user": self.get_user_permissions() elif self.arn.resource_type == "assumed-role": self.get_role_permissions()
def test_arnparse_iam(self): arn_str = "arn:aws:iam::0:role/service-role/role-name" arn = arnparse(arn_str) self.assertEqual(arn.full, arn_str) self.assertEqual(arn.partition, "aws") self.assertEqual(arn.service, "iam") self.assertEqual(arn.region, None) self.assertEqual(arn.account_id, "0") self.assertEqual(arn.resource_type, "role/service-role") self.assertEqual(arn.resource, "role-name") arn_str = "arn:aws:iam::0:policy/service-role/role-name" arn = arnparse(arn_str) self.assertEqual(arn.resource_type, "policy/service-role") self.assertEqual(arn.resource, "role-name") arn_str = "arn:aws:iam::0:user/user-name" arn = arnparse(arn_str) self.assertEqual(arn.resource_type, "user") self.assertEqual(arn.resource, "user-name")
def test_arnparse_iam(self): arn_str = 'arn:aws:iam::0:role/service-role/role-name' arn = arnparse(arn_str) self.assertEqual(arn.full, arn_str) self.assertEqual(arn.partition, 'aws') self.assertEqual(arn.service, 'iam') self.assertEqual(arn.region, None) self.assertEqual(arn.account_id, '0') self.assertEqual(arn.resource_type, 'role/service-role') self.assertEqual(arn.resource, 'role-name') arn_str = 'arn:aws:iam::0:policy/service-role/role-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'policy/service-role') self.assertEqual(arn.resource, 'role-name') arn_str = 'arn:aws:iam::0:user/user-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'user') self.assertEqual(arn.resource, 'user-name')
def test_arnparse_to_dict(self): arn_str = 'arn:aws:lambda:eu-west-1:0:function:function-name' arn = arnparse(arn_str) self.assertEqual(arn.to_dict(), { 'full': arn_str, 'partition': 'aws', 'service': 'lambda', 'region': 'eu-west-1', 'account_id': '0', 'resource_type': 'function', 'resource': 'function-name' })
def test_arnparse_to_dict(self): arn_str = "arn:aws:lambda:eu-west-1:0:function:function-name" arn = arnparse(arn_str) self.assertEqual( arn.to_dict(), { "full": arn_str, "partition": "aws", "service": "lambda", "region": "eu-west-1", "account_id": "0", "resource_type": "function", "resource": "function-name", }, )
def parse(self, report, verbose=False): ''' Parses Lambda report and automatically extracts and tracks statistics. ''' self.statistics['lambdas'] += 1 self.track('regions', report['region']) self.track('runtimes', report['runtime']) for idx in ['triggers', 'resources']: for arn in report[idx]['items']: if arn == '*': continue self.track(idx, arnparse(arn).service) if 'count' in report['security']: for level, level_count in report['security']['count'].items(): self.track('security', level, level_count) self.save(verbose=verbose)
def parse(self, report, verbose=False): """ Parses Lambda report and automatically extracts and tracks statistics. """ self.statistics["lambdas"] += 1 self.statistics["layers"] += len(report["layers"]) self.track("regions", report["region"]) self.track("runtimes", report["runtime"]) for idx in ["triggers", "resources"]: for arn in report[idx]["items"]: if arn == "*": continue self.track(idx, arnparse(arn).service) if "count" in report["security"]: for level, level_count in report["security"]["count"].items(): self.track("security", level, level_count) self.save(verbose=verbose)
def get_regions(args): """ Valid region specification: Single: eu-west-1 Multiple: eu-west-1,ap-south-1,us-east-2 All: all Returns regions as a Python list """ if not isinstance(args.region, str): raise ValueError("No region specified") if args.function: return [arnparse(args.function).region] available = boto3.Session().get_available_regions("lambda") if args.region == "all": return available regions = args.region.split(",") if not regions: raise ValueError("No region specified") for region in regions: if region not in available: raise ValueError(f'Invalid region "{region}"') return regions
def test_arnparse_kms(self): arn_str = 'arn:aws:kms:eu-west-1:0:key/key-id' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'key') self.assertEqual(arn.resource, 'key-id')
def test_arnparse_s3(self): arn_str = 'arn:aws:s3:::bucket-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, None) self.assertEqual(arn.resource, 'bucket-name')
def test_arnparse_invalid(self): with self.assertRaises(ValueError): arnparse('') with self.assertRaises(ValueError): arnparse('arn:aws:service')
def test_arnparse_logs(self): arn_str = 'arn:aws:logs:eu-west-1:0:log-group:/aws/lambda/function:*' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'log-group') self.assertEqual(arn.resource, '/aws/lambda/function:*')
def test_arnparse_dynamodb(self): arn_str = 'arn:aws:dynamodb:us-west-2:0:table/table-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, 'table') self.assertEqual(arn.resource, 'table-name')
def audit(self): if type(self.statement) != dict: return None if self.statement["Effect"] != "Allow": return None # Where where = "in Policy Statement" if self.policy: where = f'in Role Policy {self.policy["name"]}' elif "Sid" in self.statement: where = f'{where} Sid {self.statement["Sid"]}' # Principal if self.is_unrestricted("Principal"): if self.is_undefined("Condition"): yield { "level": "high", "text": ("Service is publicly accessible due to unrestricted " f"Principal and undefined Condition {where}\n" "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html" ), } # NotPrincipal if self.get("NotPrincipal"): yield { "level": "high", "text": ("Anonymous or unauthenticated users may have access to this " f"service due to using NotPrincipal with Allow {where}\n" "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html" ), } yield { "level": "info", "text": f"Use NotPrincipal with Deny {where}" } # Resource if self.is_unrestricted("Resource"): if self.is_unrestricted("Action"): if self.is_undefined("Condition"): yield { "level": "info", "text": ("Service can perform any operation on any service due " f"to unrestricted Resource, unrestricted Action and undefined Condition {where}" ), } elif self.is_undefined("Condition"): operations = list( set([x.split(":")[0] for x in self.get("Action")])) for _ in operations: if _ not in [ "sqs", "s3", "iam", "sns", "vpc", "ec2", "ecs", "es", "logs", "dynamodb", "kms", "cloudformation", ]: operations.pop(operations.index(_)) if len(operations): yield { "level": "info", "text": (f'Service can perform {", ".join(operations)} operations on ' f"any service due to unrestricted Resource and undefined Condition {where}" ), } else: for resource in self.get("Resource"): arn = arnparse(resource) if arn.account_id == "*": yield { "level": "low", "text": f"Unrestricted AWS Account ID in {arn.service} {resource} {where}", } # Action for action in self.get("Action"): if action == "*" or action.split(":")[1] == "*": yield { "level": "low", "text": (f"Unrestricted Action {action} {where}\n" "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" ), } # NotAction if self.get("NotAction"): yield { "level": "medium", "text": (f"Users may have more permissions than intended due to using NotAction with Allow {where}\n" "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notaction.html" ), } yield {"level": "info", "text": f"Use NotAction with Deny {where}"} # Condition if self.is_undefined("Condition"): yield { "level": "info", "text": f"Use policy Conditions for extra security {where}", } # Managed vs Inline policies if self.policy and self.policy["type"] == "inline": yield { "level": "info", "text": f"Use Customer Managed policies instead of Inline policies {where}", } # Privilege Escalation via IAM permissions yield from PrivilegeEscalation(self.get("Action")).audit()
def run(arguments=""): """ Main routine """ args = parse_args(arguments) verbose(args, header, end="\n\n") if args.html: HTMLReport(args.output).save() verbose(args, f"Generated {args.output}/report.html", end="\n\n") exit(0) rmtree(args.output, ignore_errors=True) Path(args.output).mkdir(parents=True, exist_ok=True) configure_log(args.output) usage = get_usage(args) verbose(args, "Loading identity") region = list(usage.keys())[0] sts_arn = f"arn:aws:sts:{region}" identity = STS(sts_arn, args.profile, args.keys[0], args.keys[1]) if args.verbose: for _ in ["UserId", "Account", "Arn"]: align(_, identity.caller[_], orange) print("") statistics = Statistics(args.output) visibility = VisibilityReport(args.output) writes = LambdaWrite(args) total_count = 0 for region_count in usage.values(): total_count += region_count for region in usage.keys(): args.region = region for arn_str in get_functions(args): try: arn = arnparse(arn_str) counter = f'[ {statistics.statistics["lambdas"]+1}/{total_count} ] ' verbose(args, f"{counter}{arn.resource}") lmbd = Lambda(arn.full, args, identity) for w in writes.get_for_lambda(arn.full): lmbd.set_writes(w) statistics.parse(lmbd.report()) visibility.save(lmbd.report()) except Exception: debug(arn_str) SecurityReport(args.output).save() HTMLReport(args.output).save() if args.verbose: print("\r" + " " * 100, end="\r") # clear align("Lambdas", statistics.statistics["lambdas"]) align("Security", statistics.statistics["security"]["count"]) align("Triggers", statistics.statistics["triggers"]["count"]) align("Resources", statistics.statistics["resources"]["count"]) align("Layers", statistics.statistics["layers"]) align("Runtimes", len(statistics.statistics["runtimes"]["items"])) align("Regions", len(statistics.statistics["regions"]["items"])) print("") align("Report", f"{args.output}/report.html") align("Log", f"{args.output}/lambdaguard.log") print("")
def scan(self): ''' Scan Lambda report for vulnerabilities and provide recommendations. ''' # Audit Function policy if not self.report['policy']['function']: self.track(self.report['arn'], { 'level': 'info', 'text': 'Function policy is not defined' }) else: if 'Statement' in self.report['policy']['function']: for statement in self.report['policy']['function'][ 'Statement']: for _ in PolicyStatement(statement).audit(): self.track(self.report['arn'], _) # Audit Execution role policy if not len(self.report['policy']['role']['policies']): self.track(self.report['arn'], { 'level': 'info', 'text': 'Execution Role policy is not defined' }) else: for policy in self.report['policy']['role']['policies']: if 'Statement' in policy['document']: for statement in policy['document']['Statement']: for _ in PolicyStatement(statement, policy=policy).audit(): self.track(self.report['role'], _) # Audit Resources if 'logs' not in self.report['resources']['services']: self.track( self.report['arn'], { 'level': 'low', 'text': 'Function activity is not monitored by CloudWatch due to missing logs permissions' }) # Audit Triggers and Resources items = list( set( list(self.report['triggers']['items'].keys()) + list(self.report['resources']['items'].keys()))) for arn in items: if arn == '*': continue arn = arnparse(arn) if arn.resource in ['*', '']: continue self.item = None if arn.service == 's3': if arn.resource_type: # S3 object path continue self.item = S3(arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key) for _ in AccessControlList(self.item.acl).audit(): self.track(arn.full, _) for _ in Encryption(self.item).audit(): self.track(arn.full, _) elif arn.service == 'sqs': self.item = SQS(arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key) elif arn.service == 'sns': self.item = SNS(arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key) elif arn.service == 'apigateway': self.item = APIGateway( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key) elif arn.service == 'dynamodb': self.item = DynamoDB(arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key) if self.item: # Audit item Resource-based Policy if 'Statement' in self.item.policy: for statement in self.item.policy['Statement']: for _ in PolicyStatement(statement).audit(): self.track(arn.full, _) # If policy is missing, the the service is public else: for _ in Public(self.item).audit(): self.track(arn.full, _) # SonarQube if self.args.sonarqube: self.scan_sonarqube(arn, self.report['codeURL'], self.report['runtime']) for layer in self.report['layers']: self.scan_sonarqube(arn, layer['codeURL'], self.report['runtime']) # Sort findings by level sorted_items = [] for sort in ['high', 'medium', 'low', 'info']: for item in self.security['items']: if item['level'] == sort: if item not in sorted_items: sorted_items.append(item) self.security['items'] = sorted_items
def test_arnparse_sqs(self): arn_str = 'arn:aws:sqs:eu-west-1:0:queue-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, None) self.assertEqual(arn.resource, 'queue-name')
def test_arnparse_sns(self): arn_str = 'arn:aws:sns:us-east-1:0:topic-name' arn = arnparse(arn_str) self.assertEqual(arn.resource_type, None) self.assertEqual(arn.resource, 'topic-name')
def scan(self): """ Scan Lambda report for vulnerabilities and provide recommendations. """ # Audit Function policy if not self.report["policy"]["function"]: self.track( self.report["arn"], { "level": "info", "text": "Function policy is not defined" }, ) else: if "Statement" in self.report["policy"]["function"]: for statement in self.report["policy"]["function"][ "Statement"]: for _ in PolicyStatement(statement).audit(): self.track(self.report["arn"], _) # Audit Execution role policy if not len(self.report["policy"]["role"]["policies"]): self.track( self.report["arn"], { "level": "info", "text": "Execution Role policy is not defined" }, ) else: for policy in self.report["policy"]["role"]["policies"]: if "Statement" in policy["document"]: for statement in policy["document"]["Statement"]: for _ in PolicyStatement(statement, policy=policy).audit(): self.track(self.report["role"], _) # Audit KMS if "kms" in self.report: self.item = KMS( self.report["kms"], profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) if not self.item.rotation: self.track( self.report["kms"], { "level": "medium", "text": ("Automatic rotation of key material is disabled\n" "https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html" ), }, ) for _, policy in self.item.policies.items(): self.audit_policy_statements(self.item.arn, policy) # Audit Resources if "logs" not in self.report["resources"]["services"]: self.track( self.report["arn"], { "level": "low", "text": "Function activity is not monitored by CloudWatch due to missing logs permissions", }, ) # Audit Triggers and Resources items = list( set( list(self.report["triggers"]["items"].keys()) + list(self.report["resources"]["items"].keys()))) for arn in items: if arn == "*": continue arn = arnparse(arn) if arn.resource in ["*", ""]: continue self.item = None if arn.service == "s3": if arn.resource_type: # S3 object path continue self.item = S3( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) for _ in AccessControlList(self.item.acl).audit(): self.track(arn.full, _) for _ in Encryption(self.item).audit(): self.track(arn.full, _) elif arn.service == "sqs": self.item = SQS( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) elif arn.service == "sns": self.item = SNS( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) elif arn.service == "apigateway": self.item = APIGateway( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) elif arn.service == "dynamodb": self.item = DynamoDB( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) elif arn.service == "kms": self.item = KMS( arn.full, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, ) if self.item: if type(self.item) == KMS: # Audit KMS Policies for _, policy in self.item.policies.items(): self.audit_policy_statements(self.item.arn, policy) else: # Audit item Resource-based Policy self.audit_policy_statements(self.item.arn, self.item.policy) # If policy is missing, then the service is public for _ in Public(self.item).audit(): self.track(arn.full, _) # SonarQube if self.args.sonarqube: self.scan_sonarqube(arn, self.report["codeURL"], self.report["runtime"]) for layer in self.report["layers"]: self.scan_sonarqube(arn, layer["codeURL"], self.report["runtime"]) # Sort findings by level sorted_items = [] for sort in ["high", "medium", "low", "info"]: for item in self.security["items"]: if item["level"] == sort: if item not in sorted_items: sorted_items.append(item) self.security["items"] = sorted_items
def audit(self): if self.statement['Effect'] != 'Allow': return # Where where = 'in Policy Statement' if self.policy: where = f'in Role Policy {self.policy["name"]}' elif 'Sid' in self.statement: where = f'{where} Sid {self.statement["Sid"]}' # Principal if self.is_unrestricted('Principal'): if self.is_undefined('Condition'): yield { 'level': 'high', 'text': f'Service is publicly accessible due to unrestricted Principal and undefined Condition {where}\nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html' } # NotPrincipal if self.get('NotPrincipal'): yield { 'level': 'high', 'text': f'Anonymous or unauthenticated users may have access to this service due to using NotPrincipal with Allow {where}\nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html' } yield { 'level': 'info', 'text': f'Use NotPrincipal with Deny {where}' } # Resource if self.is_unrestricted('Resource'): if self.is_unrestricted('Action'): if self.is_undefined('Condition'): yield { 'level': 'info', 'text': f'Service can perform any operation on any service due to unrestricted Resource, unrestricted Action and undefined Condition {where}' } elif self.is_undefined('Condition'): operations = list( set([x.split(':')[0] for x in self.get('Action')])) for _ in operations: if _ not in [ 'sqs', 's3', 'iam', 'sns', 'vpc', 'ec2', 'ecs', 'es', 'logs', 'dynamodb', 'kms', 'cloudformation' ]: operations.pop(operations.index(_)) if len(operations): yield { 'level': 'info', 'text': f'Service can perform {", ".join(operations)} operations on any service due to unrestricted Resource and undefined Condition {where}' } else: for resource in self.get('Resource'): arn = arnparse(resource) if arn.account_id == '*': yield { 'level': 'low', 'text': f'Unrestricted AWS Account ID in {arn.service} {resource} {where}' } # Action for action in self.get('Action'): if '*' in action: yield { 'level': 'low', 'text': f'Unrestricted Action {action} {where}\nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege' } # NotAction if self.get('NotAction'): yield { 'level': 'medium', 'text': f'Users may have more permissions than intended due to using NotAction with Allow {where}\nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notaction.html' } yield {'level': 'info', 'text': f'Use NotAction with Deny {where}'} # Condition if self.is_undefined('Condition'): yield { 'level': 'info', 'text': f'Use policy Conditions for extra security {where}' } # Managed vs Inline policies if self.policy and self.policy['type'] == 'inline': yield { 'level': 'info', 'text': f'Use Customer Managed policies instead of Inline policies {where}' } # Privilege Escalation via IAM permissions yield from PrivilegeEscalation(self.get('Action')).audit()
def run(arguments=''): ''' Main routine ''' args = parse_args(arguments) verbose(args, header, end='\n\n') if args.html: HTMLReport(args.output).save() verbose(args, f'Generated {args.output}/report.html', end='\n\n') exit(0) rmtree(args.output, ignore_errors=True) Path(args.output).mkdir(parents=True, exist_ok=True) configure_log(args.output) usage = get_usage(args) verbose(args, f'Loading identity') region = list(usage.keys())[0] sts_arn = f'arn:aws:sts:{region}' identity = STS(sts_arn, args.profile, args.keys[0], args.keys[1]) if args.verbose: for _ in ['UserId', 'Account', 'Arn']: align(_, identity.caller[_], orange) print('') statistics = Statistics(args.output) visibility = VisibilityReport(args.output) writes = LambdaWrite(args) total_count = 0 for region_count in usage.values(): total_count += region_count for region in usage.keys(): args.region = region for arn_str in get_functions(args): try: arn = arnparse(arn_str) counter = f'[ {statistics.statistics["lambdas"]+1}/{total_count} ] ' verbose(args, f'{counter}{arn.resource}') lmbd = Lambda(arn.full, args, identity) for w in writes.get_for_lambda(arn.full): lmbd.set_writes(w) statistics.parse(lmbd.report()) visibility.save(lmbd.report()) except Exception: debug(arn_str) SecurityReport(args.output).save() HTMLReport(args.output).save() if args.verbose: print('\r' + ' ' * 100, end='\r') # clear align('Lambdas', statistics.statistics["lambdas"]) align('Security', statistics.statistics["security"]["count"]) align('Triggers', statistics.statistics["triggers"]["count"]) align('Resources', statistics.statistics["resources"]["count"]) align('Layers', statistics.statistics["layers"]) align('Runtimes', len(statistics.statistics["runtimes"]["items"])) align('Regions', len(statistics.statistics["regions"]["items"])) print('') align('Report', f'{args.output}/report.html') align('Log', f'{args.output}/lambdaguard.log') print('')