diff --git a/timesketch/lib/analyzers/contrib/__init__.py b/timesketch/lib/analyzers/contrib/__init__.py index 7c0d5d88fd..e5ad429f93 100644 --- a/timesketch/lib/analyzers/contrib/__init__.py +++ b/timesketch/lib/analyzers/contrib/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Contrib Analyzer module.""" +from timesketch.lib.analyzers.contrib import aws_cloudtrail from timesketch.lib.analyzers.contrib import bigquery_matcher from timesketch.lib.analyzers.contrib import misp_analyzer from timesketch.lib.analyzers.contrib import hashlookup_analyzer diff --git a/timesketch/lib/analyzers/contrib/aws_cloudtrail.py b/timesketch/lib/analyzers/contrib/aws_cloudtrail.py new file mode 100644 index 0000000000..01d69ce024 --- /dev/null +++ b/timesketch/lib/analyzers/contrib/aws_cloudtrail.py @@ -0,0 +1,180 @@ +"""Sketch analyzer plugin for aws cloudtrail.""" + +from __future__ import unicode_literals + +import json +import logging + +from timesketch.lib import emojis +from timesketch.lib.analyzers import interface +from timesketch.lib.analyzers import manager + +logger = logging.getLogger("timesketch.analyzers.aws_cloudtrail") + + +class AwsCloudtrailSketchPlugin(interface.BaseAnalyzer): + """Sketch analyzer for AwsCloudtrail.""" + + NAME = "aws_cloudtrail" + DISPLAY_NAME = "AWS CloudTrail Analyzer" + DESCRIPTION = ( + "Extract features and tag security relevant actions in AWS CloudTrail." + ) + + DEPENDENCIES = frozenset() + + CLOUD_TRAIL_EVENT = "cloud_trail_event" + EVENT_NAME = "event_name" + + def _parse_cloudtrail_event(self, event): + """Parses the CloudTrail event string into a dictionary.""" + cloud_trail_event_str = event.source.get(self.CLOUD_TRAIL_EVENT) + if not cloud_trail_event_str: + return + + try: + return json.loads(cloud_trail_event_str) + except json.JSONDecodeError: + return None + + def _cloudtrail_add_tag(self, event): + """Tags CloudTrail events based on event details and type.""" + cloud_trail_event = self._parse_cloudtrail_event(event) + event_name = event.source.get(self.EVENT_NAME) + + if cloud_trail_event: + if cloud_trail_event.get("readOnly") == True: + event.add_tags(["readOnly:true"]) + event.add_emojis([emojis.get_emoji("MAGNIFYING_GLASS")]) + elif cloud_trail_event.get("readOnly") == False: + event.add_tags(["readOnly:false"]) + event.add_emojis([emojis.get_emoji("SPARKLES")]) + + if cloud_trail_event.get("errorCode") in [ + "AccessDenied", + "UnauthorizedOperation", + ]: + event.add_tags(["UnauthorizedAPICall"]) + + user_name = cloud_trail_event.get("userIdentity", {}).get("userName") + error_message = cloud_trail_event.get("errorMessage") + if ( + user_name == "HIDDEN_DUE_TO_SECURITY_REASONS" + and error_message == "No username found in supplied account" + ): + event.add_tags(["FailedLoginNonExistentIAMUser"]) + + if event_name: + if event_name in ( + "AuthorizeSecurityGroupEgress", + "AuthorizeSecurityGroupIngress", + "CreateSecurityGroup", + "DeleteSecurityGroup", + "ModifySecurityGroupRules", + "RevokeSecurityGroupEgress", + "RevokeSecurityGroupIngress", + ): + event.add_tags(["SG"]) + event.add_tags(["NetworkChanged"]) + if event_name in ( + "CreateNetworkAcl", + "CreateNetworkAclEntry", + "DeleteNetworkAcl", + "DeleteNetworkAclEntry", + "ReplaceNetworkAclAssociation", + "ReplaceNetworkAclEntry", + ): + event.add_tags(["NACL"]) + event.add_tags(["NetworkChanged"]) + if ( + event_name + and any( + act in event_name + for act in [ + "Accept", + "Associate", + "Attach", + "Create", + "Delete", + "Replace", + ] + ) + and "Gateway" in event_name + ): + event.add_tags(["GW"]) + event.add_tags(["NetworkChanged"]) + if event_name in ( + "CreateRoute", + "CreateRouteTable", + "DeleteRoute", + "DeleteRouteTable", + "DisassociateRouteTable", + "ReplaceRoute", + "ReplaceRouteTableAssociation", + ): + event.add_tags(["RouteTable"]) + event.add_tags(["NetworkChanged"]) + if event_name in ( + "AcceptVpcPeeringConnection", + "AttachClassicLinkVpc", + "CreateVpc", + "CreateVpcPeeringConnection", + "DeleteVpc", + "DeleteVpcPeeringConnection", + "DetachClassicLinkVpc", + "DisableVpcClassicLink", + "EnableVpcClassicLink", + "ModifyVpcAttribute", + "RejectVpcPeeringConnection", + ): + event.add_tags(["VPC"]) + event.add_tags(["NetworkChanged"]) + + if event_name in ( + "AddRoleToInstanceProfile", + "AddUserToGroup", + "AssumeRole", + "AttachGroupPolicy", + "AttachRolePolicy", + "AttachUserPolicy", + "CreateAccessKey", + "CreateLoginProfile", + "CreatePolicyVersion", + "CreateRole", + "PassRole", + "PutGroupPolicy", + "PutRolePolicy", + "PutUserPolicy", + "SetDefaultPolicyVersion", + "UpdateAccessKey", + "UpdateLoginProfile", + ): + event.add_tags(["SuspicousIAMActivity"]) + + if event_name == "ConsoleLogin": + event.add_tags(["ConsoleLogin"]) + + if event_name == "GetCallerIdentity": + event.add_tags(["GetCallerIdentity"]) + + def run(self): + """Entry point for the analyzer. + + Returns: + String with summary of the analyzer result + """ + query = 'data_type:"aws:cloudtrail:entry"' + + return_fields = [self.CLOUD_TRAIL_EVENT, self.EVENT_NAME] + + events = self.event_stream(query_string=query, return_fields=return_fields) + + for event in events: + self._cloudtrail_add_tag(event) + # Add other analyzers here. + event.commit() + + return "AWS CloudTrail Analyzer completed" + + +manager.AnalysisManager.register_analyzer(AwsCloudtrailSketchPlugin) diff --git a/timesketch/lib/analyzers/contrib/aws_cloudtrail_test.py b/timesketch/lib/analyzers/contrib/aws_cloudtrail_test.py new file mode 100644 index 0000000000..d24b8cba35 --- /dev/null +++ b/timesketch/lib/analyzers/contrib/aws_cloudtrail_test.py @@ -0,0 +1,101 @@ +"""Tests for AwsCloudtrailPlugin.""" + +from __future__ import unicode_literals + +import mock + +from timesketch.lib.analyzers.contrib import aws_cloudtrail +from timesketch.lib.testlib import BaseTest +from timesketch.lib.testlib import MockDataStore + + +class TestAwsCloudtrailPlugin(BaseTest): + """Tests the functionality of the analyzer.""" + + def __init__(self, *args, **kwargs): + super(TestAwsCloudtrailPlugin, self).__init__(*args, **kwargs) + + @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) + def test_readOnly_tagging(self): + """Tests that AWS CloudTrail readOnly events are tagged as expected.""" + analyzer = aws_cloudtrail.AwsCloudtrailSketchPlugin("test_index", 1) + analyzer.datastore.client = mock.Mock() + datastore = analyzer.datastore + + source_attributes = { + "data_type": "aws:cloudtrail:entry", + "cloud_trail_event": '{"readOnly":true}', + } + + datastore.import_event("test_index", source_attributes, "0") + analyzer.run() + self.assertEqual(analyzer.tagged_events["0"]["tags"], ["readOnly"]) + + @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) + def test_unauthorizedAPICall_tagging(self): + """Tests that AWS CloudTrail AccessDenied events are tagged as expected.""" + analyzer = aws_cloudtrail.AwsCloudtrailSketchPlugin("test_index", 1) + analyzer.datastore.client = mock.Mock() + datastore = analyzer.datastore + + source_attributes = { + "data_type": "aws:cloudtrail:entry", + "cloud_trail_event": '{"errorCode":"AccessDenied"}', + } + + datastore.import_event("test_index", source_attributes, "0") + analyzer.run() + self.assertEqual(analyzer.tagged_events["0"]["tags"], ["UnauthorizedAPICall"]) + + @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) + def test_failedLoginNonExistentIAMUser_tagging(self): + """Tests that AWS CloudTrail FailedLoginNonExistentIAMUser events are tagged as expected.""" + analyzer = aws_cloudtrail.AwsCloudtrailSketchPlugin("test_index", 1) + analyzer.datastore.client = mock.Mock() + datastore = analyzer.datastore + + source_attributes = { + "data_type": "aws:cloudtrail:entry", + "cloud_trail_event": '{"userIdentity": {"userName": "HIDDEN_DUE_TO_SECURITY_REASONS"},"errorMessage": "No username found in supplied account"}', + } + + datastore.import_event("test_index", source_attributes, "0") + analyzer.run() + self.assertEqual( + analyzer.tagged_events["0"]["tags"], ["FailedLoginNonExistentIAMUser"] + ) + + @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) + def test_network_tagging(self): + """Tests that AWS CloudTrail NetworkChanged events are tagged as expected.""" + analyzer = aws_cloudtrail.AwsCloudtrailSketchPlugin("test_index", 1) + analyzer.datastore.client = mock.Mock() + datastore = analyzer.datastore + + source_attributes = { + "data_type": "aws:cloudtrail:entry", + "event_name": "AuthorizeSecurityGroupIngress", + } + + datastore.import_event("test_index", source_attributes, "0") + analyzer.run() + self.assertEqual( + sorted(analyzer.tagged_events["0"]["tags"]), + sorted(["NetworkChanged", "SG"]), + ) + + @mock.patch("timesketch.lib.analyzers.interface.OpenSearchDataStore", MockDataStore) + def test_consoleLogin_tagging(self): + """Tests that AWS CloudTrail ConsoleLogin events are tagged as expected.""" + analyzer = aws_cloudtrail.AwsCloudtrailSketchPlugin("test_index", 1) + analyzer.datastore.client = mock.Mock() + datastore = analyzer.datastore + + source_attributes = { + "data_type": "aws:cloudtrail:entry", + "event_name": "ConsoleLogin", + } + + datastore.import_event("test_index", source_attributes, "0") + analyzer.run() + self.assertEqual(analyzer.tagged_events["0"]["tags"], ["ConsoleLogin"])