def connectES(esEndPoint): """ Description: Creates a connection to the Elasticsearch Domain. Parameters: esEndPoint - string - The domain endpoint, found in the AWS Elasticsearch Console but not including the https:// prefix. """ consoleLog("Connecting to the ES Endpoint {0}".format(esEndPoint), "DEBUG", esLogLevelGv) try: esClient = Elasticsearch(timeout=120, hosts=[{ "host": esEndPoint, "port": 443 }], http_auth=esAuthTypeGv, use_ssl=True, verify_certs=True, connection_class=RequestsHttpConnection, retry_on_timeout=True) return esClient except Exception as E: consoleLog( "Unable to connect to Elasticsearch domain : {0}".format( esEndPoint) + " Exception : " + E, "ERROR", esLogLevelGv) exit(3)
def writeDemoData(esClient, DemoDataIndex, eventName="CreateUser"): global DEBUG jsonDoc = { "eventVersion": "1.05", "eventTime": "2020-08-02T03:48:24Z", "eventSource": "iam.amazonaws.com", "eventName": "CreateUser", "awsRegion": "us-east-1", "userIdentity": { "type": "AssumedRole", "principalId": "AROA4RKH6JM6LD63WC7LA:awsvolks-Isengard", "arn": "arn:aws:sts::861828696892:assumed-role/God/awsvolks-Isengard", "accountId": "861828696892", "accessKeyId": "TEST EVENT", "sessionContext": { "sessionIssuer": { "type": "Role", "principalId": "TEST EVENT", "arn": "TEST EVENT", "accountId": "861828696892", "userName": "******" } } }, "sourceIPAddress": "0.0.0.0", "userAgent": "TEST EVENT", "requestParameters": "{\"userName\": \"billybob-cliandconsole\", \"tags\": [{\"key\": \"test\", \"value\": \"true\"}]}", "responseElements": "TEST EVENT", "requestID": "f0c539ce-7f0e-489e-a89f-32612de519ff", "eventID": "TEST EVENT", "eventType": "AwsApiCall", "recipientAccountId": "861828696892", "Enriched": { "knownGood": "false", "knownBad": "false" }, "geoip": { "city_name": "", "country_name": "", "latitude": "", "longitude": "", "country_code3": "", "continent_code": "", "postal_code": "", "region_code": "", "region_name": "", "timezone": "" } } retval = esClient.index(index=DemoDataIndex, body=jsonDoc) consoleLog("Added demo document to Elasticsearch index {0}. Doc Id:{1}".format(DemoDataIndex, retval["_id"]),"DEBUG",esLogLevelGv) return retval["_id"]
def lambda_handler(event, context): # Connect to Elasticsearch esClient = connectES(esEndPointEv) consoleLog("lambda_handler : Elasticsearch connection.","DEBUG",esLogLevelGv) # # Write Demo Data # DemoDataIndex = "cloudtrail-2020.08.02" DocId = writeDemoData(esClient, DemoDataIndex) deleteDemoData(esClient, DemoDataIndex, DocId) return { 'statusCode': 200, 'body': json.dumps("ok") }
def lambda_handler(event, context): # # To do: Run all rule from this type, run only single rule from this type # # # Initialize iterator used in bulk load. esBulkMessagesGv = [] esBulkComplianceMessagesGv = [] # Mark start of function execution. timerDict = {} timerDict["lambda_handler : Started Processing"] = int(time.time()) consoleLog( "lambda_handler : Started Processing @ {0} {1}:{2}:{3}".format( datetime.now().strftime("%Y-%m-%d"), datetime.now().hour, datetime.now().minute, datetime.now().second), "INFO", esLogLevelGv) # If any errors are found reading environment variables, don't continue. if configError == True: consoleLog( "lambda_handler : Not executing, configuration error detected.", "ERROR", esLogLevelGv) return # Connect to Elasticsearch esClient = connectES(esEndPointEv) consoleLog("lambda_handler : Elasticsearch connection.", "DEBUG", esLogLevelGv) # Connect to DynamoDB dynamodb_client = boto3.resource('dynamodb') dynamodb_table = dynamodb_client.Table(DynamoDBname) consoleLog( "Dynamo table MonitorLizard status = " + dynamodb_table.table_status, "DEBUG", esLogLevelGv) # Connect to SNS sns = boto3.client('sns') if TEST_SNS: response = sns.publish( TopicArn=SnsTopicArn, Message='Test message from Monitor Lizard', ) consoleLog("Sent test SNS message.", "INFO", esLogLevelGv) return # # Execute rules of type "Login anomaly" # consoleLog("Executing rule type " + RuleType, "INFO", esLogLevelGv) response = dynamodb_table.scan( FilterExpression=Attr('RuleType').eq(RuleType)) if len(TEST_RULE): print("Executing TEST_RULE only ") runRule(esClient, dynamodb_table, sns, TEST_RULE, RuleType) return for Rule in response["Items"]: print() if not Rule["RuleActive"]: print( "Skipping SIEM rule because rule has been deactivated (RuleActive=false)" ) continue if Rule["LastRun"] + (Rule["RunScheduleInMinutes"] * 60) < int( time.time()): print("Executing rule: " + Rule["RuleId"]) runRule(esClient, dynamodb_table, sns, Rule["RuleId"], Rule["RuleType"]) # Updateing SIEM rule last run time stamp response = dynamodb_table.update_item( Key={ 'RuleId': Rule["RuleId"], 'RuleType': Rule["RuleType"] }, UpdateExpression="set LastRun=:l", ExpressionAttributeValues={':l': int(time.time())}, ReturnValues="UPDATED_NEW") else: print("--------------------------") if (TEST_IGNORE_LASTRUN): runRule(esClient, dynamodb_table, sns, Rule["RuleId"], Rule["RuleType"]) else: print("Skipping SIEM rule because of LastRun setting: " + Rule["RuleId"]) return {'statusCode': 200, 'body': json.dumps("ok")}
def runRule(esClient, dynamodb_table, sns, RuleId, RuleType): # Read SIEM rule from DynamoDB DBresponse = dynamodb_table.query( KeyConditionExpression=Key('RuleId').eq(RuleId) & Key('RuleType').eq(RuleType), ) try: Rule_ES_Index = DBresponse["Items"][0]["ES_Index"] Rule_Query = json.loads( DBresponse["Items"][0]["Query"]) # must include an aggregation Rule_LastAggResult = DBresponse["Items"][0]["LastAggResult"] Rule_AlertPeriodMinutes = DBresponse["Items"][0]["AlertPeriodMinutes"] Rule_AlertText = DBresponse["Items"][0]["AlertText"] Rule_Description = DBresponse["Items"][0]["Description"] Rule_AlertMinimumEventCount = DBresponse["Items"][0][ "AlertMinimumEventCount"] Rule_Active = DBresponse["Items"][0]["RuleActive"] except Exception as E: consoleLog("Error reading some of the database values.", "ERROR", esLogLevelGv) return # # Remove eventTime range filter and replace with new time filter # query_start_range = int(time.time()) - (Rule_AlertPeriodMinutes * 60) - ( TEST_AlertPeriod * 60) # unix time query_start_date = datetime.utcfromtimestamp(query_start_range).strftime( '%Y-%m-%dT%H:%M:%SZ') # Zulu time n = -1 for filter in Rule_Query["query"]["bool"]["filter"]: n = n + 1 if "range" in filter: if "eventTime" in filter["range"]: Rule_Query["query"]["bool"]["filter"].pop(n) # set new dateTime range filter Rule_Query["query"]["bool"]["filter"].append( {'range': { 'eventTime': { 'gte': query_start_date } }}) consoleLog( "Modified date range filter for query: " + str(Rule_Query["query"]["bool"]["filter"]), "DEBUG", esLogLevelGv) # # Read Elasticsearch aggregation query # with newly set time range filter # result = esClient.search(index=Rule_ES_Index, body=Rule_Query) hits = result["hits"]["total"]["value"] #bucket = result["aggregations"]["my_count"]["buckets"] ## replace my_count with a dynamically detected custom agg name # # Iterate through ElasticSearch aggregation result # # GroupValues_current is an array with all aggregated values (group by) and the unix time from this query CurrentAggResult = {} QueryTime = int(time.time()) AlertOccurrences = {} """ Example result: "aggregations" : { "agg1" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "arn:aws:sts::861828696892:assumed-role/God/awsvolks-Isengard", "doc_count" : 17, "agg2" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "United States", "doc_count" : 16 }, { "key" : "Australia", "doc_count" : 1 } ] } } ] } """ x = 0 # Get aggregation values and add current time for customAggName, value in result["aggregations"].items(): # level 1 (first custom aggregation name) if isinstance(value, (dict, list)): buckets_layer1 = result["aggregations"][customAggName]["buckets"] for bucket_layer1 in buckets_layer1: x = x + 1 Layer1_GroupValue = bucket_layer1["key"] Layer1_Count = bucket_layer1["doc_count"] print("Group Layer 1 value = ", Layer1_GroupValue, ' (count=', Layer1_Count, ")") # Get aggregation values of second layer for customAggName2, value2 in bucket_layer1.items(): if isinstance(value2, (dict, list)): AllLayer2GroupValues = "" for bucket_layer2 in bucket_layer1[customAggName2][ "buckets"]: AllLayer2GroupValues = AllLayer2GroupValues + bucket_layer2[ "key"] + ", " NumberOfLayer2Groups = len( bucket_layer1[customAggName2]["buckets"]) if NumberOfLayer2Groups >= Rule_AlertMinimumEventCount: consoleLog("Rule fired.", "DEBUG", esLogLevelGv) AlertOccurrences[x] = { 'GroupValue': Layer1_GroupValue, 'OccurrenceCount': NumberOfLayer2Groups, 'Occurrences': AllLayer2GroupValues } # # Raise alerts # for x in AlertOccurrences: Message = Rule_AlertText Message = Message + "\nDescription: " + Rule_Description Message = Message + "\nRule Id: " + RuleId + "\nRule Type: " + RuleType + "\nElasticsearch Index: " + Rule_ES_Index + "\nAlert window: " + str( Rule_AlertPeriodMinutes) + " minutes\n" Message = Message + "\nGroup value: " + AlertOccurrences[x][ 'GroupValue'] Message = Message + "\nNumber of Occurrences: " + str( AlertOccurrences[x]['OccurrenceCount']) Message = Message + "\nOccurrences (comma separated): " + AlertOccurrences[ x]['Occurrences'] consoleLog("ALERT MESSAGE:" + Message, "DEBUG", esLogLevelGv) if SEND_ALERT: consoleLog( "Send alert for new occurrence: " + AlertOccurrences[x]['GroupValue'], "INFO", esLogLevelGv) response = sns.publish( TopicArn=SnsTopicArn, Message=Message, ) # Add alert to Elasticsearch index AlertDateTime = datetime.utcfromtimestamp(int( time.time())).strftime('%Y-%m-%dT%H:%M:%SZ') jsonDoc = { "AlertDateTime": AlertDateTime, "Rule_Id": RuleId, "Rule_Type": RuleType, "Alert_Value": AlertOccurrences[x]['GroupValue'] } retval = esClient.index(index="monitorlizardalerts", body=jsonDoc) consoleLog( "Add document to Elasticsearch index MonitorLizardAlerts : {0}" .format(retval), "DEBUG", esLogLevelGv) else: consoleLog("Alerting deactivated.", "INFO", esLogLevelGv) return
"DynamoDB"] # Data storage bucket for persistent storage SnsTopicArn = os.environ["SNS_TOPIC_ARN"] # Init global Variables configError = False esAuthTypeGv = "" esLogLevelGv = "" esClient = "" dynamodb_client = "" dynamodb_table = "" #Is a valid log level set. if esLogLevelEv not in ("DEBUG", "INFO", "ERROR"): configError = True consoleLog( "Environment Variable : ES_LOG_LEVEL must be set to one of \"DEBUG\", \"INFO\" or \"ERROR\".", "ERROR", "ERROR") else: esLogLevelGv = esLogLevelEv consoleLog( "Lambda function log level set to {} defined in ES_LOG_LEVEL environment variable." .format(esLogLevelEv), "DEBUG", esLogLevelGv) #Check region has been set and is valid. if esRegionEv not in [ region['RegionName'] for region in boto3.client('ec2').describe_regions()['Regions'] ]: configError = True consoleLog("Environment Variable : ES_REGION is invalid.", "ERROR", esLogLevelGv)
def runRule(esClient, dynamodb_table, sns, RuleId, RuleType): global DEBUG # Read SIEM rule from DynamoDB DBresponse = dynamodb_table.query( KeyConditionExpression=Key('RuleId').eq(RuleId) & Key('RuleType').eq(RuleType), ) try: Rule_ES_Index = DBresponse["Items"][0]["ES_Index"] Rule_Query = json.loads( DBresponse["Items"][0]["Query"]) # must include an aggregation Rule_LastAggResult = DBresponse["Items"][0]["LastAggResult"] Rule_AlertPeriodMinutes = DBresponse["Items"][0]["AlertPeriodMinutes"] Rule_AlertText = DBresponse["Items"][0]["AlertText"] Rule_Description = DBresponse["Items"][0]["Description"] Rule_Condition = json.loads(DBresponse["Items"][0]["Rule_Condition"]) Rule_Active = DBresponse["Items"][0]["RuleActive"] except Exception as E: print(E) consoleLog("Error reading some of the database values. JSON error?.", "ERROR", esLogLevelGv) return # # Remove eventTime range filter and replace with new time filter # query_start_range = int(time.time()) - (Rule_AlertPeriodMinutes * 60) - ( TEST_AlertPeriod * 60) # unix time query_start_date = datetime.utcfromtimestamp(query_start_range).strftime( '%Y-%m-%dT%H:%M:%SZ') # Zulu time n = -1 for filter in Rule_Query["query"]["bool"]["filter"]: n = n + 1 if "range" in filter: if "eventTime" in filter["range"]: Rule_Query["query"]["bool"]["filter"].pop(n) # set new dateTime range filter Rule_Query["query"]["bool"]["filter"].append( {'range': { 'eventTime': { 'gte': query_start_date } }}) consoleLog( "Modified date range filter for query: " + str(Rule_Query["query"]["bool"]["filter"]), "DEBUG", esLogLevelGv) # # Run Elasticsearch query # result = esClient.search(index=Rule_ES_Index, body=Rule_Query) hits = result["hits"]["total"]["value"] AlertOccurrences = {} QueryTime = int(time.time()) print("Number of hits: ", hits) if hits > 0: for hit in result["hits"]["hits"]: if DEBUG: print("-------rule query hit---------") print("checking rule condition for document id " + hit["_id"]) #print(hit) # Test ''' Rule_Condition = { "matches": [{ "search_field" : "requestParameters", "search_regex" : '.*{"key": "test", "value": "true"}.*', "search_logic" : True },{ "search_field" : "recipientAccountId", "search_regex" : '861828696892', "search_logic" : True }] } this needs to be stored in DynamoDB in the following format: { "matches": [{ "search_field" : "requestParameters", "search_regex" : ".*{.*\\"key\\".*:.* \\"test\\", \\"value\\".*:.* \\"true\\".*", "search_logic" : "True" },{ "search_field" : "recipientAccountId", "search_regex" : "861828696892", "search_logic" : "True" }] } ''' # Check all conditions for this hit # Only alert, if all conditions are met RuleFired = True for match in Rule_Condition["matches"]: search_field = match["search_field"] # Elasticsearch field search_regex = r"{}".format(match["search_regex"]) # regex search_logic = match[ "search_logic"] # True or False (Rule fires if regex is a match or not match) # Convert to boolean value if search_logic.lower() == "true": search_logic = True else: search_logic = False print("Check if ", search_field, " matches ", search_regex, " is ", search_logic) # check if search_field exists if search_field in hit["_source"]: if hit["_source"][search_field]: # check for search_regex line = str(hit["_source"][search_field]) matchObj = re.search(search_regex, line, re.M | re.I) if matchObj and search_logic: print( ' > condition met. Alert only raised if all conditions match.' ) else: if not matchObj and not search_logic: print( ' > condition met. Alert only raised if all conditions match.' ) else: print( ' > condition NOT met. No alert for this rule raised.' ) RuleFired = False else: print("Field ", search_field, " not found in Elasticsearch document ") # Fire rule if all conditions are met if RuleFired: docId = hit["_id"] AlertOccurrences[docId] = hit["_source"] print("Rule fired ") # # Raise alerts # for AggField in AlertOccurrences: Message = Rule_AlertText Message = Message + "\nDescription: " + Rule_Description Message = Message + "\nQuery Result: " + str( AlertOccurrences[AggField]) Message = Message + "\nRule Id: " + RuleId + "\nRule Type: " + RuleType + "\nElasticsearch Index: " + Rule_ES_Index + "\nAlert window: " + str( Rule_AlertPeriodMinutes) + " minutes\n" Message = Message + "\nAlert Query: " + str(Rule_Query) consoleLog("ALERT MESSAGE:" + Message, "DEBUG", esLogLevelGv) if SEND_ALERT: consoleLog("Send alert for document _id: " + AggField, "INFO", esLogLevelGv) response = sns.publish( TopicArn=SnsTopicArn, Message=Message, ) # Add alert to Elasticsearch index AlertDateTime = datetime.utcfromtimestamp(int( time.time())).strftime('%Y-%m-%dT%H:%M:%SZ') jsonDoc = { "AlertDateTime": AlertDateTime, "Rule_Id": RuleId, "Rule_Type": RuleType, "Alert_Value": AggField } retval = esClient.index(index="monitorlizardalerts", body=jsonDoc) consoleLog( "Add document to Elasticsearch index MonitorLizardAlerts : {0}" .format(retval), "DEBUG", esLogLevelGv) else: consoleLog("Alerting deactivated.", "INFO", esLogLevelGv) return
def runRule(esClient, dynamodb_table, sns, RuleId, RuleType): # Read SIEM rule from DynamoDB DBresponse = dynamodb_table.query( KeyConditionExpression=Key('RuleId').eq(RuleId) & Key('RuleType').eq(RuleType), ) try: Rule_ES_Index = DBresponse["Items"][0]["ES_Index"] Rule_Query = json.loads( DBresponse["Items"][0]["Query"]) # must include an aggregation Rule_LastAggResult = DBresponse["Items"][0]["LastAggResult"] Rule_AlertPeriodMinutes = DBresponse["Items"][0]["AlertPeriodMinutes"] Rule_AlertText = DBresponse["Items"][0]["AlertText"] Rule_Description = DBresponse["Items"][0]["Description"] Rule_AlertMinimumEventCount = DBresponse["Items"][0][ "AlertMinimumEventCount"] Rule_Active = DBresponse["Items"][0]["RuleActive"] except Exception as E: consoleLog("Error reading some of the database values.", "ERROR", esLogLevelGv) return # # Remove eventTime range filter and replace with new time filter # query_start_range = int(time.time()) - (Rule_AlertPeriodMinutes * 60) - ( TEST_AlertPeriod * 60) # unix time query_start_date = datetime.utcfromtimestamp(query_start_range).strftime( '%Y-%m-%dT%H:%M:%SZ') # Zulu time n = -1 for filter in Rule_Query["query"]["bool"]["filter"]: n = n + 1 if "range" in filter: if "eventTime" in filter["range"]: Rule_Query["query"]["bool"]["filter"].pop(n) # set new dateTime range filter Rule_Query["query"]["bool"]["filter"].append( {'range': { 'eventTime': { 'gte': query_start_date } }}) consoleLog( "Modified date range filter for query: " + str(Rule_Query["query"]["bool"]["filter"]), "DEBUG", esLogLevelGv) # # Read Elasticsearch aggregation query # with newly set time range filter # result = esClient.search(index=Rule_ES_Index, body=Rule_Query) hits = result["hits"]["total"]["value"] # # Iterate through ElasticSearch aggregation result # # GroupValues_current is an array with all aggregated values (group by) and the unix time from this query CurrentAggResult = {} QueryTime = int(time.time()) # Get aggregation values and add current time for customAggName, value in result["aggregations"].items(): if isinstance(value, (dict, list)): bucket = result["aggregations"][customAggName]["buckets"] for GroupValue in bucket: if GroupValue["doc_count"] >= Rule_AlertMinimumEventCount: consoleLog( GroupValue["key"] + " found at least " + str(Rule_AlertMinimumEventCount) + " times.", "DEBUG", esLogLevelGv) CurrentAggResult[GroupValue[ "key"]] = QueryTime # current occurence with current time else: consoleLog( GroupValue["key"] + " found but did not reach minimum event count limit of " + str(Rule_AlertMinimumEventCount) + " times.", "DEBUG", esLogLevelGv) if DEBUG: print("Query result from current run (Group values with timestamp:") print(CurrentAggResult) print("Query result from previous run saved in DynamoDB:") print(Rule_LastAggResult) # # Compare current search result with stored results # AlertOccurrences = {} NewAggResult_tmp = Rule_LastAggResult NewAggResult = {} for currentAggField in CurrentAggResult: if currentAggField in Rule_LastAggResult.keys(): occurrencerTimeDiff = CurrentAggResult[ currentAggField] - Rule_LastAggResult[currentAggField] # Alert on this new occurrence (new within alert time frame) if (occurrencerTimeDiff / 60 > Rule_AlertPeriodMinutes): AlertOccurrences[currentAggField] = (occurrencerTimeDiff / 60 > Rule_AlertPeriodMinutes) consoleLog( currentAggField + " found in previous occurrences. Alert because last occurrence was too long ago ", "DEBUG", esLogLevelGv) NewAggResult_tmp[ currentAggField] = QueryTime # includes new and old occurrences (needs clean up later) else: # Alert on this new occurrence consoleLog( currentAggField + " found in current occurrences. Alert because its new", "DEBUG", esLogLevelGv) AlertOccurrences[currentAggField] = QueryTime NewAggResult_tmp[ currentAggField] = QueryTime # includes new and old occurrences (needs clean up later) # # Clean up NewAggResult (remove events older then Rule_AlertPeriodMinutes). # for AggField in NewAggResult_tmp: if (QueryTime - NewAggResult_tmp[AggField]) > Rule_AlertPeriodMinutes: consoleLog( AggField + " removed from stored list of occurrences (out of alert window)", "DEBUG", esLogLevelGv) else: # build new list NewAggResult[AggField] = NewAggResult_tmp[AggField] if DEBUG: print("New list of query results to be stored in DynamoDB") for AggField in NewAggResult: print(AggField + ":" + str(NewAggResult[AggField])) # # Update stored results (NewAggResult) # if not TEST_NOUPDATE: consoleLog("Updating database with list of occurrences", "INFO", esLogLevelGv) response = dynamodb_table.update_item( Key={ 'RuleId': RuleId, 'RuleType': RuleType }, UpdateExpression="set LastAggResult=:l", ExpressionAttributeValues={':l': NewAggResult}, ReturnValues="UPDATED_NEW") else: consoleLog( "Skip update of database for new list of occurrences (Test_NOUPDATE=True)", "INFO", esLogLevelGv) # # Raise alerts # for AggField in AlertOccurrences: Message = Rule_AlertText Message = Message + "\nDescription: " + Rule_Description Message = Message + "\n" + "Alert Value: " + AggField + "\nRule Id: " + RuleId + "\nRule Type: " + RuleType + "\nElasticsearch Index: " + Rule_ES_Index + "\nAlert window: " + str( Rule_AlertPeriodMinutes) + " minutes\n" Message = Message + "\nAlert Query: " + Rule_Query #consoleLog("ALERT MESSAGE:"+Message,"DEBUG",esLogLevelGv) if SEND_ALERT: consoleLog("Send alert for new occurrence: " + AggField, "INFO", esLogLevelGv) response = sns.publish( TopicArn=SnsTopicArn, Message=Message, ) # Add alert to Elasticsearch index AlertDateTime = datetime.utcfromtimestamp(int( time.time())).strftime('%Y-%m-%dT%H:%M:%SZ') jsonDoc = { "AlertDateTime": AlertDateTime, "Rule_Id": RuleId, "Rule_Type": RuleType, "Alert_Value": AggField } retval = esClient.index(index="monitorlizardalerts", body=jsonDoc) consoleLog( "Add document to Elasticsearch index MonitorLizardAlerts : {0}" .format(retval), "DEBUG", esLogLevelGv) else: consoleLog("Alerting deactivated.", "INFO", esLogLevelGv) return