From f1eee0c62510b5c24892a60887851068b61fb4d9 Mon Sep 17 00:00:00 2001 From: ben-githubs <38414634+ben-githubs@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:22:59 -0600 Subject: [PATCH 1/5] Wiz.Alert.Passthrough: New Dedup Logic (#1438) --- rules/wiz_rules/wiz_alert_passthrough.py | 9 +++- rules/wiz_rules/wiz_alert_passthrough.yml | 66 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/rules/wiz_rules/wiz_alert_passthrough.py b/rules/wiz_rules/wiz_alert_passthrough.py index 7e84ace86..3357201c3 100644 --- a/rules/wiz_rules/wiz_alert_passthrough.py +++ b/rules/wiz_rules/wiz_alert_passthrough.py @@ -13,12 +13,17 @@ def title(event): def severity(event): - # if event.get("severity") == "INFORMATIONAL": - # return "INFO" return event.get("severity") def dedup(event): + # For lower-severity events, dedup based on specific source rule to reduce overall alert volume + if event.get("severity") in ("INFO", "LOW"): + dedup_str = str(event.deep_get("sourceRule", "id")) + if dedup_str: + return dedup_str + # If the severity is higher, or for some reason we couldn't generate a dedup string based on + # the source rule, then use the alert severity + the resource ID itself. return event.deep_get( "entitySnapshot", "externalId", default="" ) + event.get("severity", "") diff --git a/rules/wiz_rules/wiz_alert_passthrough.yml b/rules/wiz_rules/wiz_alert_passthrough.yml index 4bb4b36a6..fa7640b20 100644 --- a/rules/wiz_rules/wiz_alert_passthrough.yml +++ b/rules/wiz_rules/wiz_alert_passthrough.yml @@ -78,6 +78,72 @@ Tests: "type": "TOXIC_COMBINATION", "updatedAt": "2024-06-04 02:28:06.763277000" } + - Name: Low-Severity Open Alert + ExpectedResult: true + Log: + { + "createdAt": "2024-06-04 02:28:06.763277000", + "entitySnapshot": { + "cloudProviderURL": "", + "externalId": "someExternalId", + "id": "12345", + "name": "someName", + "nativeType": "", + "providerId": "someProviderId", + "region": "", + "resourceGroupExternalId": "", + "subscriptionExternalId": "", + "subscriptionName": "", + "tags": { }, + "type": "DATA_FINDING" + }, + "id": "54321", + "notes": [ ], + "projects": [ + { + "businessUnit": "", + "id": "45678", + "name": "Project 2", + "riskProfile": { + "businessImpact": "MBI" + }, + "slug": "project-2" + }, + ], + "serviceTickets": [ ], + "severity": "LOW", + "sourceRule": { + "__typename": "Control", + "controlDescription": "Alert Description", + "id": "12345", + "name": "Alert Name", + "resolutionRecommendation": "Alert Resolution Recommendation", + "securitySubCategories": [ + { + "category": { + "framework": { + "name": "Wiz for Risk Assessment" + }, + "name": "High Profile Threats" + }, + "title": "High-profile vulnerability exploited in the wild" + }, + { + "category": { + "framework": { + "name": "MITRE ATT&CK Matrix" + }, + "name": "TA0001 Initial Access" + }, + "title": "T1190 Exploit Public-Facing Application" + }, + ] + }, + "status": "OPEN", + "statusChangedAt": "2024-06-04 02:28:06.597355000", + "type": "TOXIC_COMBINATION", + "updatedAt": "2024-06-04 02:28:06.763277000" + } - Name: Resolved Alert ExpectedResult: false Log: From ea063d5bba778b599cb4c46ac6ef2da9d601a541 Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:29:30 -0700 Subject: [PATCH 2/5] PantherFlow Investigator Helper (#1436) Co-authored-by: ben-githubs <38414634+ben-githubs@users.noreply.github.com> --- global_helpers/global_helpers_test.py | 48 ++++++++++++++++++++++++++ global_helpers/panther_aws_helpers.py | 2 ++ global_helpers/panther_base_helpers.py | 23 ++++++++++++ global_helpers/panther_okta_helpers.py | 4 +++ 4 files changed, 77 insertions(+) diff --git a/global_helpers/global_helpers_test.py b/global_helpers/global_helpers_test.py index 8e0be5120..10a71d671 100755 --- a/global_helpers/global_helpers_test.py +++ b/global_helpers/global_helpers_test.py @@ -2391,5 +2391,53 @@ 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) + + if __name__ == "__main__": unittest.main() diff --git a/global_helpers/panther_aws_helpers.py b/global_helpers/panther_aws_helpers.py index 738db8cb3..1b3927838 100644 --- a/global_helpers/panther_aws_helpers.py +++ b/global_helpers/panther_aws_helpers.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List import boto3 +from panther_base_helpers import pantherflow_investigation from panther_config import config @@ -38,6 +39,7 @@ def aws_rule_context(event): "sourceIPAddress": event.get("sourceIPAddress", ""), "userAgent": event.get("userAgent", ""), "userIdentity": event.get("userIdentity", ""), + "PantherFlow Investigation": pantherflow_investigation(event), } diff --git a/global_helpers/panther_base_helpers.py b/global_helpers/panther_base_helpers.py index 6a0970d3b..d541883c5 100644 --- a/global_helpers/panther_base_helpers.py +++ b/global_helpers/panther_base_helpers.py @@ -327,3 +327,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 diff --git a/global_helpers/panther_okta_helpers.py b/global_helpers/panther_okta_helpers.py index ffbfb8af8..8715fc777 100644 --- a/global_helpers/panther_okta_helpers.py +++ b/global_helpers/panther_okta_helpers.py @@ -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 { @@ -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), } From 1fd2f0962debc3cbc8720861d6308219a71a7be8 Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:02:13 -0700 Subject: [PATCH 3/5] Add get_user to AWS datamodel (#1441) --- data_models/aws_cloudtrail_data_model.py | 35 +++++++++++++++++++++++ data_models/aws_cloudtrail_data_model.yml | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/data_models/aws_cloudtrail_data_model.py b/data_models/aws_cloudtrail_data_model.py index deea50cc3..20ad3e3a7 100644 --- a/data_models/aws_cloudtrail_data_model.py +++ b/data_models/aws_cloudtrail_data_model.py @@ -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 diff --git a/data_models/aws_cloudtrail_data_model.yml b/data_models/aws_cloudtrail_data_model.yml index 1b202b634..57b8b075d 100644 --- a/data_models/aws_cloudtrail_data_model.yml +++ b/data_models/aws_cloudtrail_data_model.yml @@ -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 From 5120292023c42b512196e71ffada647269d835b2 Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:17:00 -0700 Subject: [PATCH 4/5] THREAT-413 - Tune EKS Anonymous API Access Detection Rule (#1405) (#1433) --- packs/aws.yml | 1 + rules/aws_eks_rules/anonymous_api_access.py | 43 ++++ rules/aws_eks_rules/anonymous_api_access.yml | 197 +++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 rules/aws_eks_rules/anonymous_api_access.py create mode 100644 rules/aws_eks_rules/anonymous_api_access.yml diff --git a/packs/aws.yml b/packs/aws.yml index c4591af93..820a4a43f 100644 --- a/packs/aws.yml +++ b/packs/aws.yml @@ -144,6 +144,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 diff --git a/rules/aws_eks_rules/anonymous_api_access.py b/rules/aws_eks_rules/anonymous_api_access.py new file mode 100644 index 000000000..e7e75f34a --- /dev/null +++ b/rules/aws_eks_rules/anonymous_api_access.py @@ -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) diff --git a/rules/aws_eks_rules/anonymous_api_access.yml b/rules/aws_eks_rules/anonymous_api_access.yml new file mode 100644 index 000000000..96ff68577 --- /dev/null +++ b/rules/aws_eks_rules/anonymous_api_access.yml @@ -0,0 +1,197 @@ +AnalysisType: rule +Filename: anonymous_api_access.py +RuleID: "Amazon.EKS.AnonymousAPIAccess" +DisplayName: "EKS Anonymous API Access Detected" +Enabled: true +LogTypes: + - Amazon.EKS.Audit +Severity: Low +Reports: + MITRE ATT&CK: + - "TA0001:T1190" # Initial Access: Exploit Public-Facing Application +Description: > + This rule detects anonymous API requests made to the Kubernetes API server. + In production environments, anonymous access should be disabled to prevent + unauthorized access to the API server. +DedupPeriodMinutes: 60 +Reference: + https://raesene.github.io/blog/2023/03/18/lets-talk-about-anonymous-access-to-Kubernetes/ +Runbook: > + Check the EKS cluster configuration and ensure that anonymous access + to the Kubernetes API server is disabled. This can be done by verifying the API + server arguments and authentication webhook configuration. +SummaryAttributes: + - user:username + - p_any_ip_addresses + - p_source_label +Tags: + - EKS + - Security Control + - API + - Initial Access:Exploit Public-Facing Application +Tests: + - Name: Anonymous API Access + ExpectedResult: true + Log: + { + "annotations": { + "authorization.k8s.io/decision": "allow", + "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer" + }, + "apiVersion": "audit.k8s.io/v1", + "auditID": "abcde12345", + "kind": "Event", + "level": "Request", + "objectRef": { + "apiVersion": "v1", + "name": "test-pod", + "namespace": "default", + "resource": "pods" + }, + "p_any_aws_account_ids": [ + "123412341234" + ], + "p_any_aws_arns": [ + "arn:aws:iam::123412341234:role/DevAdministrator" + ], + "p_any_ip_addresses": [ + "8.8.8.8" + ], + "p_any_usernames": [ + "system:anonymous" + ], + "p_event_time": "2022-11-29 00:09:04.38", + "p_log_type": "Amazon.EKS.Audit", + "p_parse_time": "2022-11-29 00:10:25.067", + "p_row_id": "2e4ab474b0f0f7a4a8fff4f014a9b32a", + "p_source_id": "4c859cd4-9406-469b-9e0e-c2dc1bee24fa", + "p_source_label": "example-cluster-eks-logs", + "requestReceivedTimestamp": "2022-11-29 00:09:04.38", + "requestURI": "/api/v1/namespaces/default/pods/test-pod", + "responseStatus": { + "code": 200 + }, + "sourceIPs": [ + "8.8.8.8" + ], + "stage": "ResponseComplete", + "user": { + "username": "system:anonymous" + }, + "userAgent": "kubectl/v1.25.4" + } + - Name: Non-Anonymous API Access + ExpectedResult: false + Log: + { + "annotations": { + "authorization.k8s.io/decision": "allow", + "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer" + }, + "apiVersion": "audit.k8s.io/v1", + "auditID": "abcde12345", + "kind": "Event", + "level": "Request", + "objectRef": { + "apiVersion": "v1", + "name": "test-pod", + "namespace": "default", + "resource": "pods" + }, + "p_any_aws_account_ids": [ + "123412341234" + ], + "p_any_aws_arns": [ + "arn:aws:iam::123412341234:role/DevAdministrator" + ], + "p_any_ip_addresses": [ + "8.8.8.8" + ], + "p_any_usernames": [ + "kubernetes-admin" + ], + "p_event_time": "2022-11-29 00:09:04.38", + "p_log_type": "Amazon.EKS.Audit", + "p_parse_time": "2022-11-29 00:10:25.067", + "p_row_id": "2e4ab474b0f0f7a4a8fff4f014a9b32a", + "p_source_id": "4c859cd4-9406-469b-9e0e-c2dc1bee24fa", + "p_source_label": "example-cluster-eks-logs", + "requestReceivedTimestamp": "2022-11-29 00:09:04.38", + "requestURI": "/api/v1/namespaces/default/pods/test-pod", + "responseStatus": { + "code": 200 + }, + "sourceIPs": [ + "8.8.8.8" + ], + "stage": "ResponseComplete", + "user": { + "username": "kubernetes-admin" + }, + "userAgent": "kubectl/v1.25.4" + } + - Name: Anonymous API Access Web Scanner Allowed + ExpectedResult: true + Log: + { + "annotations": { + "authorization.k8s.io/decision": "allow", + "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\"" + }, + "apiVersion": "audit.k8s.io/v1", + "auditID": "d976bfc6-a2bc-49d5-bdeb-074441e0b875", + "kind": "Event", + "level": "Metadata", + "requestReceivedTimestamp": "2024-11-13 18:34:10.595141000", + "requestURI": "/version", + "responseStatus": { + "code": 200 + }, + "sourceIPs": [ + "44.238.138.237" + ], + "stage": "ResponseComplete", + "stageTimestamp": "2024-11-13 18:34:10.595494000", + "user": { + "groups": [ + "system:unauthenticated" + ], + "username": "system:anonymous" + }, + "userAgent": "python-requests/2.31.0", + "verb": "get" + } + - Name: Anonymous API Access Web Scanner Denied + ExpectedResult: true + Log: + { + "annotations": { + "authorization.k8s.io/decision": "forbid", + "authorization.k8s.io/reason": "" + }, + "apiVersion": "audit.k8s.io/v1", + "auditID": "edf35e8d-92c3-4507-9bc6-4dd9cf068bcf", + "kind": "Event", + "level": "Metadata", + "requestReceivedTimestamp": "2024-11-13 23:50:32.672347000", + "requestURI": "/vendor/phpunit/src/Util/PHP/eval-stdin.php", + "responseStatus": { + "code": 403, + "message": "forbidden: User \"system:anonymous\" cannot get path \"/vendor/phpunit/src/Util/PHP/eval-stdin.php\"", + "reason": "Forbidden", + "status": "Failure" + }, + "sourceIPs": [ + "8.216.81.10" + ], + "stage": "ResponseComplete", + "stageTimestamp": "2024-11-13 23:50:32.673504000", + "user": { + "groups": [ + "system:unauthenticated" + ], + "username": "system:anonymous" + }, + "userAgent": "Custom-AsyncHttpClient", + "verb": "get" + } From 2ffb95d50fc597d6ab4ecaf5127acd165136adb4 Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:20:03 -0700 Subject: [PATCH 5/5] Update Email Regex (#1440) --- global_helpers/global_helpers_test.py | 28 +++++++++++++++++++ global_helpers/panther_base_helpers.py | 2 ++ packs/msft_graph.yml | 1 + packs/zoom.yml | 3 +- .../microsoft365_external_sharing.py | 5 ++-- .../zoom_user_promoted_to_privileged_role.py | 4 ++- .../zoom_user_promoted_to_privileged_role.yml | 4 +-- 7 files changed, 39 insertions(+), 8 deletions(-) diff --git a/global_helpers/global_helpers_test.py b/global_helpers/global_helpers_test.py index 10a71d671..f57b725b8 100755 --- a/global_helpers/global_helpers_test.py +++ b/global_helpers/global_helpers_test.py @@ -2439,5 +2439,33 @@ def test_pantherflow_investigation(self): 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 = [ + "asfe@acme.com", + "afef-awef@feaf.efaef.aef-aefc.org", + "ifjlid%fesfdj+123@gmail.com", + "a@b.co", + "alfij.fjii-fjids+123@fsjd-sdf-sjkj.co.co.co.uk", + ] + invalid_emails = [ + "asfe@acme", + "dff@.com", + "a@b", + "a@b.", + "a@b.c", + "a@b.c.", + "a@b.c.c", + "asdf?2d@gmail.com", + "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() diff --git a/global_helpers/panther_base_helpers.py b/global_helpers/panther_base_helpers.py index d541883c5..4f3260939 100644 --- a/global_helpers/panther_base_helpers.py +++ b/global_helpers/panther_base_helpers.py @@ -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 diff --git a/packs/msft_graph.yml b/packs/msft_graph.yml index a669f0d3f..c96b38dab 100644 --- a/packs/msft_graph.yml +++ b/packs/msft_graph.yml @@ -9,6 +9,7 @@ PackDefinition: - Microsoft365.MFA.Disabled - Microsoft365.Exchange.External.Forwarding # Globals + - panther_base_helpers - panther_msft_helpers - panther_config - panther_config_defaults diff --git a/packs/zoom.yml b/packs/zoom.yml index 9b3fdc469..81f4cb9e0 100644 --- a/packs/zoom.yml +++ b/packs/zoom.yml @@ -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 diff --git a/rules/microsoft_rules/microsoft365_external_sharing.py b/rules/microsoft_rules/microsoft365_external_sharing.py index 259f109b2..49f23d969 100644 --- a/rules/microsoft_rules/microsoft365_external_sharing.py +++ b/rules/microsoft_rules/microsoft365_external_sharing.py @@ -1,10 +1,9 @@ import re from fnmatch import fnmatch +from panther_base_helpers import EMAIL_REGEX from panther_msft_helpers import m365_alert_context -email_regex = re.compile(r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+") - ALLOWED_DOMAINS = ["mycompany.com", "alloweddomain.com"] # should be in lowercase ALLOWED_USERS = ["exception@outsider.com"] # should be in lowercase @@ -28,7 +27,7 @@ def rule(event): target = event.get("TargetUserOrGroupName", "") if target.lower() in ALLOWED_USERS: return False - if re.fullmatch(email_regex, target): + if re.fullmatch(EMAIL_REGEX, target): if target.split("@")[1].lower() not in ALLOWED_DOMAINS: return True return False diff --git a/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.py b/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.py index e026fe303..a0453b43d 100644 --- a/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.py +++ b/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.py @@ -1,12 +1,14 @@ import re +from panther_base_helpers import EMAIL_REGEX + PRIVILEGED_ROLES = ("Admin", "Co-Owner", "Owner", "Billing Admin") def extract_values(event): operator = event.get("operator", "") operation_detail = event.get("operation_detail", "") - email = re.search(r"[\w.+-c]+@[\w-]+\.[\w.-]+", operation_detail)[0] or "" + email = re.search(EMAIL_REGEX, operation_detail)[0] or "" fromto = re.findall(r"from ([-\s\w]+) to ([-\s\w]+)", operation_detail) or [ ("", "") ] diff --git a/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.yml b/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.yml index 507a72724..cbbfbc6e2 100644 --- a/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.yml +++ b/rules/zoom_operation_rules/zoom_user_promoted_to_privileged_role.yml @@ -11,7 +11,7 @@ Tests: action: Batch Update category_type: User operation_detail: "Change Role - homer.simpson@duff.io: from User to Co-Owner" - operator: admin@duff.io + operator: admin-test%1223+123@duff.dev.co time: "2022-07-05 20:28:48" Name: Admin Promotion Event - ExpectedResult: false @@ -59,7 +59,7 @@ Tests: action: SCIM API - Update category_type: User operation_detail: "Edit User homer.simpson@duff.co - Change Type: from Basic to Licensed" - operator: admin@duff.co + operator: admin-test%1223+123@duff.dev.co time: "2022-07-01 22:05:22" Name: Other Event DedupPeriodMinutes: 60