diff --git a/cloudwatchevents/LICENSE b/cloudwatchevents/LICENSE new file mode 100644 index 0000000..ba07b59 --- /dev/null +++ b/cloudwatchevents/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Sumo Logic Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cloudwatchevents/README.md b/cloudwatchevents/README.md index 42f34c3..904c4c2 100644 --- a/cloudwatchevents/README.md +++ b/cloudwatchevents/README.md @@ -13,7 +13,7 @@ First create an [HTTP collector endpoint](http://help.sumologic.com/Send_Data/So 2. Select `Blank Function` on the select blueprint page 3. Leave triggers empty for now, click next 4. Configure Lambda - * Select Node.js 10.x as runtime + * Select Node.js 14.x as runtime * Copy code from cloudwatchevents.js into the Lambda function code. * Add Environment variables (See below) 5. Scroll down to the `Lambda function handle and role` section, make sure you set the right values that match the function. For role, you can just use the basic execution role. Click next. diff --git a/cloudwatchevents/guardduty/cloudwatchevents.json b/cloudwatchevents/guardduty/cloudwatchevents.json index 8b13cff..2d04db7 100644 --- a/cloudwatchevents/guardduty/cloudwatchevents.json +++ b/cloudwatchevents/guardduty/cloudwatchevents.json @@ -85,7 +85,7 @@ ] }, "Timeout": 300, - "Runtime": "nodejs10.x" + "Runtime": "nodejs14.x" } }, "CloudWatchEventFunctionCloudWatchEventTriggerPermission": { diff --git a/cloudwatchevents/guardduty/template.yaml b/cloudwatchevents/guardduty/template.yaml index bb914fc..8024a39 100644 --- a/cloudwatchevents/guardduty/template.yaml +++ b/cloudwatchevents/guardduty/template.yaml @@ -1,37 +1,56 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - This function is invoked by AWS CloudWatch events in response to state change in your AWS resources which matches a event target definition. The event payload received is then forwarded to Sumo Logic HTTP source endpoint. + This function is invoked by AWS CloudWatch events in response to state change in your AWS resources which matches a event target definition. The event payload received is then forwarded to Sumo Logic HTTP source endpoint. # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 300 + Function: + Timeout: 300 + +Metadata: + AWS::ServerlessRepo::Application: + Author: Sumo Logic + Description: This function is invoked by AWS CloudWatch events in response to state change in your AWS resources which matches a event target definition. The event payload received is then forwarded to Sumo Logic HTTP source endpoint. + HomePageUrl: https://github.com/SumoLogic/sumologic-aws-lambda + Labels: + - sumologic + - serverless + - guardduty + - security + - cloudwatchevents + - guardduty + Name: sumologic-guardduty-events-processor + LicenseUrl: ../LICENSE + ReadmeUrl: ./README.md + SemanticVersion: 1.0.3 + SourceCodeUrl: https://github.com/SumoLogic/sumologic-aws-lambda/tree/master/cloudwatchevents/guardduty + SpdxLicenseId: Apache-2.0 Parameters: - SumoEndpointUrl: - Type: String + SumoEndpointUrl: + Type: String Resources: - CloudWatchEventFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: ../src/ - Handler: cloudwatchevents.handler - Runtime: nodejs10.x - Environment: - Variables: - SUMO_ENDPOINT: !Ref SumoEndpointUrl - Events: - CloudWatchEventTrigger: - Type: CloudWatchEvent - Properties: - Pattern: - source: - - aws.guardduty + CloudWatchEventFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: ../src/ + Handler: cloudwatchevents.handler + Runtime: nodejs14.x + Environment: + Variables: + SUMO_ENDPOINT: !Ref SumoEndpointUrl + Events: + CloudWatchEventTrigger: + Type: CloudWatchEvent + Properties: + Pattern: + source: + - aws.guardduty Outputs: - CloudWatchEventFunction: - Description: "CloudWatchEvent Processor Function ARN" - Value: !GetAtt CloudWatchEventFunction.Arn + CloudWatchEventFunction: + Description: "CloudWatchEvent Processor Function ARN" + Value: !GetAtt CloudWatchEventFunction.Arn diff --git a/cloudwatchevents/guarddutybenchmark/template_v2.yaml b/cloudwatchevents/guarddutybenchmark/template_v2.yaml index ad44c87..23aed10 100644 --- a/cloudwatchevents/guarddutybenchmark/template_v2.yaml +++ b/cloudwatchevents/guarddutybenchmark/template_v2.yaml @@ -54,10 +54,11 @@ Metadata: - benchmark - guardduty Name: sumologic-guardduty-benchmark - LicenseUrl: ./LICENSE + LicenseUrl: ../LICENSE ReadmeUrl: ./README.md - SemanticVersion: 1.0.10 + SemanticVersion: 1.0.11 SourceCodeUrl: https://github.com/SumoLogic/sumologic-aws-lambda/tree/master/cloudwatchevents/guarddutybenchmark + SpdxLicenseId: Apache-2.0 Parameters: CollectorName: @@ -85,7 +86,7 @@ Parameters: - us1 - in - fed - Description: "Enter au, ca, de, eu, jp, us2, or us1" + Description: "Enter in, fed, au, ca, de, eu, jp, us2, or us1" RemoveSumoResourcesOnDeleteStack: AllowedValues: - true @@ -109,7 +110,7 @@ Resources: - aws.guardduty Type: CloudWatchEvent Handler: cloudwatchevents.handler - Runtime: nodejs10.x + Runtime: nodejs14.x Type: AWS::Serverless::Function SumoAppUtils: @@ -117,7 +118,7 @@ Resources: Properties: Location: ApplicationId: arn:aws:serverlessrepo:us-east-1:956882708938:applications/sumologic-app-utils - SemanticVersion: 1.0.24 + SemanticVersion: 2.0.6 SumoHostedCollector: Type: Custom::Collector @@ -151,7 +152,7 @@ Resources: Properties: ServiceToken: !GetAtt SumoAppUtils.Outputs.SumoAppUtilsFunction Region: !Ref "AWS::Region" - AppName: "Amazon GuardDuty Benchmark" + AppName: "Global Intelligence for Amazon GuardDuty" AppId: "8e7efcb3-040a-4a92-9f8d-922fafb24afb" RemoveOnDeleteStack: !Ref RemoveSumoResourcesOnDeleteStack AppSources: diff --git a/cloudwatchevents/guarddutybenchmark/testdeploy.sh b/cloudwatchevents/guarddutybenchmark/testdeploy.sh deleted file mode 100644 index 105e18c..0000000 --- a/cloudwatchevents/guarddutybenchmark/testdeploy.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -if [ "$AWS_PROFILE" == "prod" ] -then - SAM_S3_BUCKET="appdevstore" - AWS_REGION="us-east-1" -else - SAM_S3_BUCKET="cf-templates-5d0x5unchag-us-east-2" - AWS_REGION="us-east-2" -fi - -version="1.0.10" - -sam package --template-file template_v2.yaml --s3-bucket $SAM_S3_BUCKET --output-template-file packaged_v2.yaml --s3-prefix "guarddutybenchmark/v$version" - -sam publish --template packaged_v2.yaml --region $AWS_REGION --semantic-version $version - - -ACCESS_ID="" -ACCESS_KEY="" -ORG_ID="" - -SUMO_DEPLOYMENT="us1" - -# sam deploy --template-file packaged_v2.yaml --stack-name testinggdbenchmarknew --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND --region $AWS_REGION --parameter-overrides SumoDeployment="$SUMO_DEPLOYMENT" SumoAccessID="$ACCESS_ID" SumoAccessKey="$ACCESS_KEY" RemoveSumoResourcesOnDeleteStack="true" - diff --git a/cloudwatchevents/test/__init__.py b/cloudwatchevents/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudwatchevents/test/requirements.txt b/cloudwatchevents/test/requirements.txt new file mode 100644 index 0000000..390a345 --- /dev/null +++ b/cloudwatchevents/test/requirements.txt @@ -0,0 +1,3 @@ +requests==2.20.0 +boto3==1.5.1 +sumologic-sdk==0.1.11 \ No newline at end of file diff --git a/cloudwatchevents/test/test-guardduty-benchmark.py b/cloudwatchevents/test/test-guardduty-benchmark.py new file mode 100644 index 0000000..7d9cbb5 --- /dev/null +++ b/cloudwatchevents/test/test-guardduty-benchmark.py @@ -0,0 +1,494 @@ +import copy +import datetime +import json +import os +import subprocess +import sys +import time +import unittest + +import boto3 +from sumologic import SumoLogic + +# Update the below values in case the template locations are changed. + +GUARD_DUTY_BENCHMARK_TEMPLATE = "guarddutybenchmark/template_v2.yaml" +GUARD_DUTY_BENCHMARK_SAM_TEMPLATE = "guarddutybenchmark/packaged.yaml" + +GUARD_DUTY_TEMPLATE = "guardduty/template.yaml" +GUARD_DUTY_SAM_TEMPLATE = "guardduty/packaged.yaml" + +CLOUDWATCH_TEMPLATE = "guardduty/cloudwatchevents.json" + +# Update the below values with preferred bucket name and aws region. +BUCKET_NAME = "" +AWS_REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") + +# Update the below values with preferred access id, access key and deployment +SUMO_ACCESS_ID = "" +SUMO_ACCESS_KEY = "" +SUMO_DEPLOYMENT = "" + + +def read_file(file_path): + file_path = os.path.join(os.path.dirname(os.getcwd()), file_path) + with open(file_path, "r") as f: + return f.read().strip() + + +def create_sam_package_and_upload(template, packaged_template, bucket_prefix): + print("Generating SAM package for the template %s at location %s." % (template, packaged_template)) + template_file_path = os.path.join(os.path.dirname(os.getcwd()), template) + packaged_template_path = os.path.join(os.path.dirname(os.getcwd()), packaged_template) + + # Create packaged template + run_command(["sam", "package", "--template-file", template_file_path, + "--output-template-file", packaged_template_path, "--s3-bucket", BUCKET_NAME, + "--s3-prefix", bucket_prefix]) + print("Generation complete for SAM template %s with files uploaded to Bucket %s, Prefix %s." % ( + packaged_template, BUCKET_NAME, bucket_prefix)) + + +def _run(command, input=None, check=False, **kwargs): + if sys.version_info >= (3, 5): + return subprocess.run(command, capture_output=True) + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = subprocess.PIPE + + process = subprocess.Popen(command, **kwargs) + try: + stdout, stderr = process.communicate(input) + except: + process.kill() + process.wait() + raise + retcode = process.poll() + if check and retcode: + raise subprocess.CalledProcessError( + retcode, process.args, output=stdout, stderr=stderr) + return retcode, stdout, stderr + + +def run_command(cmdargs): + resp = _run(cmdargs) + if resp.returncode != 0: + # traceback.print_exc() + raise Exception("Error in run command %s cmd: %s" % (resp, cmdargs)) + return resp.stdout + + +class SumoLogicResource(object): + + def __init__(self, source_category, finding_types, delay): + print("Initializing SumoLogicResource Object.") + self.sumo = SumoLogic(SUMO_ACCESS_ID, SUMO_ACCESS_KEY, self.api_endpoint) + self.verificationErrors = [] + self.delay = delay + self.source_category = source_category + self.findings = copy.deepcopy(finding_types) + self.findings.append("CreateSampleFindings") + print("Initialization complete for SumoLogicResource Object.") + + @property + def api_endpoint(self): + if SUMO_DEPLOYMENT == "us1": + return "https://api.sumologic.com/api" + elif SUMO_DEPLOYMENT in ["ca", "au", "de", "eu", "jp", "us2", "fed", "in"]: + return "https://api.%s.sumologic.com/api" % SUMO_DEPLOYMENT + else: + return 'https://%s-api.sumologic.net/api' % SUMO_DEPLOYMENT + + def create_collector(self, collector_name): + collector = { + 'collector': { + 'collectorType': "Hosted", + 'name': collector_name, + 'description': "This is a test collector." + } + } + response_collector = self.sumo.create_collector(collector, headers=None) + return json.loads(response_collector.text) + + def create_source(self, collector_id, source_name): + source_json = { + "source": + { + "name": source_name, + "category": self.source_category, + "automaticDateParsing": True, + "multilineProcessingEnabled": True, + "useAutolineMatching": True, + "forceTimeZone": False, + "defaultDateFormats": [{ + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "locator": ".*\"updatedAt\":\"(.*)\".*" + }], + "filters": [], + "cutoffTimestamp": 0, + "encoding": "UTF-8", + "fields": { + + }, + "messagePerRequest": False, + "sourceType": "HTTP" + } + } + response_source = self.sumo.create_source(collector_id, source_json) + return json.loads(response_source.text) + + def delete_collector(self, collector): + try: + self.sumo.delete_collector(collector) + except Exception as e: + print(e) + + def delete_source(self, collector_id, source): + try: + self.sumo.delete_source(collector_id, source) + except Exception as e: + print(e) + + def fetch_logs(self): + raw_messages = [] + # fetch Last 10 Minutes logs + to_time = int(time.time()) * 1000 + from_time = to_time - self.delay * 60 * 1000 + search_query = '_sourceCategory=%s' % self.source_category + search_job_response = self.sumo.search_job(search_query, fromTime=from_time, toTime=to_time, timeZone="IST") + print("Search Jobs API success with JOB ID as %s." % search_job_response["id"]) + state = "GATHERING RESULTS" + message_count = 0 + while state == "GATHERING RESULTS": + response = self.sumo.search_job_status(search_job_response) + if response and "state" in response: + state = response["state"] + if state == "DONE GATHERING RESULTS": + message_count = response["messageCount"] + elif state != "GATHERING RESULTS": + state = "EXIT" + else: + time.sleep(2) + if message_count != 0: + messages = self.sumo.search_job_messages(search_job_response, message_count, 0) + if messages and "messages" in messages: + messages = messages["messages"] + for message in messages: + if "map" in message and "_raw" in message["map"]: + raw_messages.append(json.loads(message["map"]["_raw"])) + print("Received message count as %s." % len(raw_messages)) + return raw_messages + + # Validate the specific findings generated + def assert_logs(self): + messages = self.fetch_logs() + for finding_type in self.findings: + try: + assert any((("type" in d and d["type"] == finding_type) + or ("eventName" in d and d["eventName"] == finding_type)) for d in messages) + except AssertionError as e: + self.verificationErrors.append( + "Finding Type \" %s \" not found in the Logs fetched from Sumo Logic." % finding_type) + + def assert_collector(self, collector_id, assertions): + collector_details, etag = self.sumo.collector(collector_id) + self.assertions(collector_details['collector'], assertions) + + def assert_httpsource(self, collector_id, source_id, assertions): + source_details, etag = self.sumo.source(collector_id, source_id) + self.assertions(source_details['source'], assertions) + + def assert_app(self, folder_id, assertions): + folder_details = self.sumo.get_folder(folder_id) + self.assertions(json.loads(folder_details.text), assertions) + + def assertions(self, data, assertions): + for key, value in assertions.items(): + try: + assert value == data[key] or value in data[key] + except AssertionError as e: + self.verificationErrors.append( + "Expected Value \" %s \" does not match the current value \" %s \" for the Key " + "as \" %s \"." % (value, data[key], key)) + + +class CloudFormation(object): + + def __init__(self, name, template_path): + self.cf = boto3.client('cloudformation', AWS_REGION) + self.stack_name = "%s-%s" % (name, datetime.datetime.now().strftime("%d-%m-%y-%H-%M-%S")) + self.template_data = read_file(template_path) + self.outputs = {} + self.resources = [] + + def stack_exists(self): + stacks = self.cf.list_stacks()['StackSummaries'] + for stack in stacks: + if stack['StackStatus'] == 'DELETE_COMPLETE': + continue + if self.stack_name == stack['StackName'] and stack['StackStatus'] == 'CREATE_COMPLETE': + print("%s stack exists." % self.stack_name) + stack_data = self.cf.describe_stacks(StackName=self.stack_name) + outputs_stacks = stack_data["Stacks"][0]["Outputs"] + for output in outputs_stacks: + self.outputs[output["OutputKey"]] = output["OutputValue"] + print("Fetched Outputs from Stack.") + self._fetch_resources() + print("Fetched Resources from Stack.") + return True + return False + + def _fetch_resources(self): + response = [] + for page in self.cf.get_paginator("list_stack_resources").paginate(StackName=self.stack_name): + response.extend(page["StackResourceSummaries"]) + if response: + for resource in response: + if "Custom::" in resource["ResourceType"]: + self.resources.append({ + "Type": resource["ResourceType"].split("::")[1], + "Id": resource["PhysicalResourceId"].split("/")[1] + }) + + def create_stack(self, parameters): + params = { + 'StackName': self.stack_name, + 'TemplateBody': self.template_data, + 'Capabilities': ['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], + 'Parameters': self.create_stack_parameters(parameters) + } + stack_result = self.cf.create_stack(**params) + print('Creating {}.'.format(self.stack_name), stack_result) + waiter = self.cf.get_waiter('stack_create_complete') + print("...waiting for stack to be ready...") + waiter.wait(StackName=self.stack_name) + + def delete_stack(self): + params = { + 'StackName': self.stack_name + } + stack_result = self.cf.delete_stack(**params) + print('Deleting {}.'.format(self.stack_name), stack_result) + waiter = self.cf.get_waiter('stack_delete_complete') + print("...waiting for stack to be removed...") + waiter.wait(StackName=self.stack_name) + + @staticmethod + def create_stack_parameters(parameters_dict): + parameters = [] + for key, value in parameters_dict.items(): + parameters.append({ + 'ParameterKey': key, + 'ParameterValue': value + }) + return parameters + + +class TestGuardDutyBenchmark(unittest.TestCase): + + @classmethod + def setUpClass(cls): + super(TestGuardDutyBenchmark, cls).setUpClass() + create_sam_package_and_upload(GUARD_DUTY_BENCHMARK_TEMPLATE, GUARD_DUTY_BENCHMARK_SAM_TEMPLATE, + "guarddutybenchmark") + print("Completed SetUp for All test Cases.") + + def setUp(self): + # Parameters + self.collector_name = "Test GuardDuty Benchmark Lambda" + self.source_name = "GuardDuty Benchmark" + self.source_category = "Labs/test/guard/duty/benchmark" + self.finding_types = ["Policy:S3/AccountBlockPublicAccessDisabled", "Policy:S3/BucketPublicAccessGranted"] + self.delay = 7 + + # Get GuardDuty details + self.guard_duty = boto3.client('guardduty', AWS_REGION) + response = self.guard_duty.list_detectors() + if "DetectorIds" in response: + self.detector_id = response["DetectorIds"][0] + + # Get CloudFormation client + self.cf = CloudFormation("TestGuardDutyBenchmark", GUARD_DUTY_BENCHMARK_SAM_TEMPLATE) + self.parameters = { + "SumoDeployment": SUMO_DEPLOYMENT, + "SumoAccessID": SUMO_ACCESS_ID, + "SumoAccessKey": SUMO_ACCESS_KEY, + "CollectorName": self.collector_name, + "SourceName": self.source_name, + "SourceCategoryName": self.source_category, + "RemoveSumoResourcesOnDeleteStack": "true" + } + # Get Sumo Logic Client + self.sumo_resource = SumoLogicResource(self.source_category, self.finding_types, self.delay) + + def tearDown(self): + if self.cf.stack_exists(): + self.cf.delete_stack() + + def test_guard_duty_benchmark(self): + self.cf.create_stack(self.parameters) + print("Testing Stack Creation.") + self.assertTrue(self.cf.stack_exists()) + # Generate some specific sample findings + print("Generating sample GuardDuty findings.") + self.guard_duty.create_sample_findings(DetectorId=self.detector_id, FindingTypes=self.finding_types) + # Check if the app, collector and source is installed + print("Validate Collector, source and app.") + resources = sorted(self.cf.resources, key=lambda i: i['Type']) + collector_id = "" + for resource in resources: + if resource['Type'] == 'Collector': + self.sumo_resource.assert_collector(resource['Id'], { + "name": self.collector_name, + "collectorType": "Hosted" + }) + collector_id = resource['Id'] + elif resource['Type'] == 'HTTPSource': + self.sumo_resource.assert_httpsource(collector_id, resource['Id'], { + "name": self.source_name, + "category": self.source_category, + "sourceType": "HTTP" + }) + elif resource['Type'] == 'App': + self.sumo_resource.assert_app(resource['Id'], { + "name": "Global Intelligence for Amazon GuardDuty", + "itemType": "Folder" + }) + print("Waiting for %s minutes for logs to appear in Sumo Logic." % self.delay) + time.sleep(self.delay * 60) + # Go to SumoLogic and check if you received the logs + # Assert one of the log for JSON format to check correctness + print("Validate Logs in Sumo Logic.") + self.sumo_resource.assert_logs() + + if len(self.sumo_resource.verificationErrors) > 0: + print("****************** Assertions Failures ******************") + print("Assertions failures are:- %s." % '\n'.join(self.sumo_resource.verificationErrors)) + print("****************** Assertions Failures ******************") + assert len(self.sumo_resource.verificationErrors) == 0 + + +class TestGuardDuty(unittest.TestCase): + + @classmethod + def setUpClass(cls): + super(TestGuardDuty, cls).setUpClass() + create_sam_package_and_upload(GUARD_DUTY_TEMPLATE, GUARD_DUTY_SAM_TEMPLATE, + "guardduty") + print("Completed SetUp for All test Cases.") + + def setUp(self): + # Parameters + self.collector_name = "Test GuardDuty Lambda" + self.source_name = "GuardDuty" + self.source_category = "Labs/test/guard/duty" + self.finding_types = ["DefenseEvasion:IAMUser/AnomalousBehavior", "Backdoor:EC2/Spambot"] + self.delay = 7 + + # Get GuardDuty details + self.guard_duty = boto3.client('guardduty', AWS_REGION) + response = self.guard_duty.list_detectors() + if "DetectorIds" in response: + self.detector_id = response["DetectorIds"][0] + + # Get Sumo Logic Client + self.sumo_resource = SumoLogicResource(self.source_category, self.finding_types, self.delay) + # Create a collector and http source for testing + self.collector = self.sumo_resource.create_collector(self.collector_name) + self.collector_id = self.collector['collector']['id'] + self.source = self.sumo_resource.create_source(self.collector_id, self.source_name) + self.source_id = self.source['source']['id'] + + # Get CloudFormation client + self.cf = CloudFormation("TestGuardDuty", GUARD_DUTY_SAM_TEMPLATE) + self.parameters = { + "SumoEndpointUrl": self.source['source']['url'], + } + + def tearDown(self): + if self.cf.stack_exists(): + self.cf.delete_stack() + self.sumo_resource.delete_source(self.collector_id, self.source) + self.sumo_resource.delete_collector(self.collector) + + def test_guard_duty(self): + self.cf.create_stack(self.parameters) + print("Testing Stack Creation.") + self.assertTrue(self.cf.stack_exists()) + # Generate some specific sample findings + print("Generating sample GuardDuty findings.") + self.guard_duty.create_sample_findings(DetectorId=self.detector_id, FindingTypes=self.finding_types) + print("Waiting for %s minutes for logs to appear in Sumo Logic." % self.delay) + time.sleep(self.delay * 60) + # Go to SumoLogic and check if you received the logs + # Assert one of the log for JSON format to check correctness + print("Validate Logs in Sumo Logic.") + self.sumo_resource.assert_logs() + + if len(self.sumo_resource.verificationErrors) > 0: + print("****************** Assertions Failures ******************") + print("Assertions failures are:- %s." % '\n'.join(self.sumo_resource.verificationErrors)) + print("****************** Assertions Failures ******************") + assert len(self.sumo_resource.verificationErrors) == 0 + + +class TestCloudWatchEvents(unittest.TestCase): + + def setUp(self): + # Parameters + self.collector_name = "Test CloudWatch Events Lambda" + self.source_name = "CloudWatch Events" + self.source_category = "Labs/test/cloudwatch/events" + self.finding_types = ["Recon:IAMUser/MaliciousIPCaller.Custom", "Discovery:S3/TorIPCaller"] + self.delay = 7 + + # Get GuardDuty details + self.guard_duty = boto3.client('guardduty', AWS_REGION) + response = self.guard_duty.list_detectors() + if "DetectorIds" in response: + self.detector_id = response["DetectorIds"][0] + + # Get Sumo Logic Client + self.sumo_resource = SumoLogicResource(self.source_category, self.finding_types, self.delay) + # Create a collector and http source for testing + self.collector = self.sumo_resource.create_collector(self.collector_name) + self.collector_id = self.collector['collector']['id'] + self.source = self.sumo_resource.create_source(self.collector_id, self.source_name) + self.source_id = self.source['source']['id'] + + # Get CloudFormation client + self.cf = CloudFormation("TestCloudWatchEvents", CLOUDWATCH_TEMPLATE) + self.parameters = { + "SumoEndpointUrl": self.source['source']['url'], + } + + def tearDown(self): + if self.cf.stack_exists(): + self.cf.delete_stack() + self.sumo_resource.delete_source(self.collector_id, self.source) + self.sumo_resource.delete_collector(self.collector) + + def test_cloudwatch_event(self): + self.cf.create_stack(self.parameters) + print("Testing Stack Creation.") + self.assertTrue(self.cf.stack_exists()) + # Generate some specific sample findings + print("Generating sample CloudWatch Events.") + self.guard_duty.create_sample_findings(DetectorId=self.detector_id, FindingTypes=self.finding_types) + print("Waiting for %s minutes for logs to appear in Sumo Logic." % self.delay) + time.sleep(self.delay * 60) + # Go to SumoLogic and check if you received the logs + # Assert one of the log for JSON format to check correctness + print("Validate Logs in Sumo Logic.") + self.sumo_resource.assert_logs() + + if len(self.sumo_resource.verificationErrors) > 0: + print("****************** Assertions Failures ******************") + print("Assertions failures are:- %s." % '\n'.join(self.sumo_resource.verificationErrors)) + print("****************** Assertions Failures ******************") + assert len(self.sumo_resource.verificationErrors) == 0 + + +if __name__ == '__main__': + unittest.main() diff --git a/cloudwatchevents/test/testdeploy.sh b/cloudwatchevents/test/testdeploy.sh new file mode 100644 index 0000000..37d24ab --- /dev/null +++ b/cloudwatchevents/test/testdeploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +export AWS_REGION="us-east-1" +export AWS_PROFILE="personal" + +if [[ "${AWS_PROFILE}" == "personal" ]] +then + SAM_S3_BUCKET="cf-templates-1qpf3unpuo1hw-us-east-1" +else + SAM_S3_BUCKET="appdevstore" +fi + +# define all application names that needs to be published. +app_names=( + "GuardDuty:template.yaml" + "guarddutybenchmark:template_V2.yaml" +) + +sam --version +# Regex to deploy only expected templates. +match_case="" + +for app_name in "${app_names[@]}" +do + KEY="${app_name%%:*}" + VALUE="${app_name##*:}" + + if [[ "${KEY}" == *"${match_case}"* ]]; then + # Grep Version from the SAM Template. + version=$(grep AWS::ServerlessRepo::Application: ../"${KEY}/${VALUE}" -A 20 | grep SemanticVersion | cut -d ':' -f 2 | xargs) + echo "Package and publish the Template file ${VALUE} with version ${version}." + + sam validate -t ../"${KEY}/${VALUE}" + + sam package --profile ${AWS_PROFILE} --template-file ../"${KEY}/${VALUE}" --s3-bucket ${SAM_S3_BUCKET} --output-template-file ../"${KEY}"/packaged.yaml \ + --s3-prefix "${KEY}/v${version}" + + sam publish --template ../"${KEY}"/packaged.yaml --region ${AWS_REGION} --semantic-version "${version}" + echo "Publish done" + fi +done