Skip to content

Commit

Permalink
Merge branch 'develop' into THREAT-420/stratus-aws-logging-detections
Browse files Browse the repository at this point in the history
  • Loading branch information
arielkr256 authored Dec 6, 2024
2 parents 50ebb38 + 2ffb95d commit f1e4fb1
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 11 deletions.
35 changes: 35 additions & 0 deletions data_models/aws_cloudtrail_data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,38 @@ def load_ip_address(event):
except ipaddress.AddressValueError:
return None
return source_ip


# get actor user from correct field based on identity type
# https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html#cloudtrail-event-reference-user-identity-fields
def get_actor_user(event):
user_type = deep_get(event, "userIdentity", "type")
if event.get("eventType") == "AwsServiceEvent":
actor_user = deep_get(event, "userIdentity", "invokedBy", default="UnknownAwsServiceEvent")
elif user_type == "Root":
actor_user = deep_get(
event,
"userIdentity",
"userName",
default=deep_get(event, "userIdentity", "accountId", default="UnknownRootUser"),
)
elif user_type in ("IAMUser", "Directory", "Unknown", "SAMLUser", "WebIdentityUser"):
actor_user = deep_get(event, "userIdentity", "userName", default=f"Unknown{user_type}")
elif user_type in ("AssumedRole", "Role", "FederatedUser"):
actor_user = deep_get(
event,
"userIdentity",
"sessionContext",
"sessionIssuer",
"userName",
default=f"Unknown{user_type}",
)
elif user_type == "IdentityCenterUser":
actor_user = deep_get(
event, "additionalEventData", "UserName", default=f"Unknown{user_type}"
)
elif user_type in ("AWSService", "AWSAccount"):
actor_user = event.get("sourceIdentity", f"Unknown{user_type}")
else:
actor_user = "UnknownUser"
return actor_user
2 changes: 1 addition & 1 deletion data_models/aws_cloudtrail_data_model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Filename: aws_cloudtrail_data_model.py
Enabled: true
Mappings:
- Name: actor_user
Path: $.userIdentity..userName
Method: get_actor_user
- Name: event_type
Method: get_event_type
- Name: source_ip
Expand Down
76 changes: 76 additions & 0 deletions global_helpers/global_helpers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2391,5 +2391,81 @@ def test_change_filed_is_empty_on_update_context(self):
)


class TestPantherFlowInvestigation(unittest.TestCase):
def test_pantherflow_investigation(self):
# pylint: disable=line-too-long
event = {
"p_any_ip_addresses": ["12.34.56.78"],
"p_source_file": {
"aws_s3_bucket": "threat-research-trail-trail-bucket-0ipb5nzxam",
"aws_s3_key": "AWSLogs/123456789123/CloudTrail/us-east-1/2024/11/25/123456789123_CloudTrail_us-east-1_20241125T1505Z_XLixf09QqBSOD7c4.json.gz",
},
"p_any_trace_ids": ["ASIAQWERTYUIOPASDFGH"],
"p_any_actor_ids": ["AROAQWERTYUIOPASDFGH", "AROAQWERTYUIOPASDFGH:bob.ross"],
"p_any_aws_account_ids": ["123456789123"],
"p_any_aws_arns": [
"arn:aws:iam::123456789123:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin",
"arn:aws:sts::123456789123:assumed-role/AWSReservedSSO_DevAdmin/bob.ross",
"arn:aws:iam::123456789123:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin",
],
"p_any_usernames": ["AWSReservedSSO_DevAdmin", "bob.ross"],
"p_event_time": "2024-11-25 15:00:21.000000",
"p_log_type": "AWS.CloudTrail",
"p_parse_time": "2024-11-25 15:05:54.123385",
"p_row_id": "d66379c617d1f7b3b2e7ce9623c104",
"p_schema_version": 0,
"p_source_id": "d0a1e235-6548-4e7f-952a-35063b304007",
"p_source_label": "threat-research-trail-us-east-1",
"p_udm": {
"source": {"address": "12.34.56.78", "ip": "12.34.56.78"},
"user": {
"arns": [
"arn:aws:iam::123456789123:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin",
"arn:aws:sts::123456789123:assumed-role/AWSReservedSSO_DevAdmin/bob.ross",
]
},
},
}
event = ImmutableCaseInsensitiveDict(event)
query = """union panther_signals.public.correlation_signals
, panther_logs.public.aws_cloudtrail
| where p_event_time between datetime('2024-11-25 15:00:21.000000') - time.parse_timespan('30m') .. datetime('2024-11-25 15:00:21.000000') + time.parse_timespan('30m')
| where arrays.overlap(p_any_ip_addresses, ['12.34.56.78'])
or arrays.overlap(p_any_trace_ids, ['ASIAQWERTYUIOPASDFGH'])
or arrays.overlap(p_any_actor_ids, ['AROAQWERTYUIOPASDFGH', 'AROAQWERTYUIOPASDFGH:bob.ross'])
or arrays.overlap(p_any_aws_arns, ['arn:aws:iam::123456789123:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin', 'arn:aws:sts::123456789123:assumed-role/AWSReservedSSO_DevAdmin/bob.ross', 'arn:aws:iam::123456789123:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin'])
or arrays.overlap(p_any_usernames, ['AWSReservedSSO_DevAdmin', 'bob.ross'])
| sort p_event_time"""
self.assertEqual(p_b_h.pantherflow_investigation(event), query)


class TestEmailRegex(unittest.TestCase):
def test_email_regex(self):
email_regex = p_b_h.EMAIL_REGEX
valid_emails = [
"[email protected]",
"[email protected]",
"ifjlid%[email protected]",
"[email protected]",
"[email protected]",
]
invalid_emails = [
"asfe@acme",
"[email protected]",
"a@b",
"a@b.",
"[email protected]",
"[email protected].",
"[email protected]",
"[email protected]",
"asdf@",
"a.b@g&g.com",
]
for email in valid_emails:
self.assertTrue(email_regex.match(email))
for email in invalid_emails:
self.assertFalse(email_regex.match(email))


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions global_helpers/panther_aws_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Dict, List

import boto3
from panther_base_helpers import pantherflow_investigation
from panther_config import config


Expand Down Expand Up @@ -38,6 +39,7 @@ def aws_rule_context(event):
"sourceIPAddress": event.get("sourceIPAddress", "<MISSING_SOURCE_IP>"),
"userAgent": event.get("userAgent", "<MISSING_USER_AGENT>"),
"userIdentity": event.get("userIdentity", "<MISSING_USER_IDENTITY>"),
"PantherFlow Investigation": pantherflow_investigation(event),
}


Expand Down
25 changes: 25 additions & 0 deletions global_helpers/panther_base_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class PantherUnexpectedAlert(Exception):
# Generic Helpers #
# # # # # # # # # # # # # #

EMAIL_REGEX = re.compile(r"[\w.+%-]+@[\w.-]+\.[a-zA-Z]{2,}")


def deep_get(dictionary: dict, *keys, default=None):
"""Safely return the value of an arbitrarily nested map
Expand Down Expand Up @@ -327,3 +329,26 @@ def add_parse_delay(event, context: dict) -> dict:
parsing_delay = time_delta(event.get("p_event_time"), event.get("p_parse_time"))
context["parseDelay"] = f"{parsing_delay}"
return context


# generate a PantherFlow investigation from an event
def pantherflow_investigation(event, interval="30m"):
logtype = event.get("p_log_type", "").lower().replace(".", "_")
timestamp = event.get("p_event_time", "")

query = f"""union panther_signals.public.correlation_signals
, panther_logs.public.{logtype}
| where p_event_time between datetime('{timestamp}') - time.parse_timespan('{interval}') .. datetime('{timestamp}') + time.parse_timespan('{interval}')
"""

first = True
for key, value in event.items():
if key.startswith("p_any_") and key != "p_any_aws_account_ids":
if first:
query += f"| where arrays.overlap({key}, {value.copy()})\n"
first = False
else:
query += f" or arrays.overlap({key}, {value.copy()})\n"
query += "| sort p_event_time"

return query
4 changes: 4 additions & 0 deletions global_helpers/panther_okta_helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from panther_base_helpers import pantherflow_investigation


def okta_alert_context(event):
"""Returns common context for automation of Okta alerts"""
return {
Expand All @@ -12,4 +15,5 @@ def okta_alert_context(event):
"authentication_context": event.get("authenticationcontext", {}),
"security_context": event.get("securitycontext", {}),
"ips": event.get("p_any_ip_addresses", []),
"PantherFlow Investigation": pantherflow_investigation(event),
}
1 change: 1 addition & 0 deletions packs/aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ PackDefinition:
- AWS.CMK.KeyRotation
- AWS.DynamoDB.TableTTLEnabled
- AWS.EC2.Vulnerable.XZ.Image.Launched
- Amazon.EKS.AnonymousAPIAccess
- AWS.IAM.Policy.DoesNotGrantAdminAccess
- AWS.IAM.Policy.DoesNotGrantNetworkAdminAccess
- AWS.IAM.Resource.DoesNotHaveInlinePolicy
Expand Down
1 change: 1 addition & 0 deletions packs/msft_graph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ PackDefinition:
- Microsoft365.MFA.Disabled
- Microsoft365.Exchange.External.Forwarding
# Globals
- panther_base_helpers
- panther_msft_helpers
- panther_config
- panther_config_defaults
Expand Down
3 changes: 1 addition & 2 deletions packs/zoom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ PackDefinition:
# Data Models used in these detections
- Standard.Zoom.Operation
# Globals used in these detections


- panther_base_helpers
- panther_event_type_helpers
- panther_zoom_helpers
43 changes: 43 additions & 0 deletions rules/aws_eks_rules/anonymous_api_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from panther_aws_helpers import eks_panther_obj_ref


def rule(event):
src_ip = event.get("sourceIPs", ["0.0.0.0"]) # nosec
if src_ip == ["127.0.0.1"]:
return False
if event.get("userAgent", "") == "ELB-HealthChecker/2.0" and src_ip[0].startswith("10.0."):
return False

# Check if the username is set to "system:anonymous", which indicates anonymous access
if event.deep_get("user", "username") == "system:anonymous":
return True
return False


def title(event):
p_eks = eks_panther_obj_ref(event)
return (
f"Anonymous API access detected on Kubernetes API server "
f"from [{p_eks.get('sourceIPs')[0]}] to [{event.get('requestURI', 'NO_URI')}] "
f"on [{p_eks.get('p_source_label')}]"
)


def severity(event):
if event.deep_get("annotations", "authorization.k8s.io/decision") != "allow":
return "INFO"
if event.get("requestURI") == "/version":
return "INFO"
return "DEFAULT"


def dedup(event):
p_eks = eks_panther_obj_ref(event)
return f"anonymous_access_{p_eks.get('p_source_label')}_{event.get('userAgent')}"


def alert_context(event):
p_eks = eks_panther_obj_ref(event)
mutable_event = event.to_dict()
mutable_event["p_eks"] = p_eks
return dict(mutable_event)
Loading

0 comments on commit f1e4fb1

Please sign in to comment.