diff --git a/.gitattributes b/.gitattributes index e82f8afc6..b7f2f0b5b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ .github/CODEOWNERS merge=ours +global_helpers/panther_config_overrides.py merge=ours diff --git a/global_helpers/panther_config.py b/global_helpers/panther_config.py new file mode 100644 index 000000000..5b51b8878 --- /dev/null +++ b/global_helpers/panther_config.py @@ -0,0 +1,14 @@ +from typing import Any + +import panther_config_defaults +import panther_config_overrides + + +class Config: # pylint: disable=too-few-public-methods + def __getattr__(self, name) -> Any: + if hasattr(panther_config_overrides, name): + return getattr(panther_config_overrides, name) + return getattr(panther_config_defaults, name, None) + + +config = Config() diff --git a/global_helpers/panther_config.yml b/global_helpers/panther_config.yml new file mode 100644 index 000000000..ba800cd04 --- /dev/null +++ b/global_helpers/panther_config.yml @@ -0,0 +1,4 @@ +AnalysisType: global +GlobalID: "panther_config" +Filename: panther_config.py +Description: Configuration values for Panther diff --git a/global_helpers/panther_config_defaults.py b/global_helpers/panther_config_defaults.py new file mode 100644 index 000000000..6b1a5e70e --- /dev/null +++ b/global_helpers/panther_config_defaults.py @@ -0,0 +1,14 @@ +""" +Here, default values for `panther_config.config` are defined +""" + +# A list of public DNS domain names that fall under the administrative domain of +# the Panther installation +ORGANIZATION_DOMAINS = ["example.com"] + +DROPBOX_ALLOWED_SHARE_DOMAINS = ORGANIZATION_DOMAINS +DROPBOX_TRUSTED_OWNERSHIP_DOMAINS = ORGANIZATION_DOMAINS +GSUITE_TRUSTED_FORWARDING_DESTINATION_DOMAINS = ORGANIZATION_DOMAINS +GSUITE_TRUSTED_OWNERSHIP_DOMAINS = ORGANIZATION_DOMAINS +MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_DOMAINS = ORGANIZATION_DOMAINS +MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_EMAILS = ["postmaster@" + ORGANIZATION_DOMAINS[0]] diff --git a/global_helpers/panther_config_defaults.yml b/global_helpers/panther_config_defaults.yml new file mode 100644 index 000000000..0c140ea85 --- /dev/null +++ b/global_helpers/panther_config_defaults.yml @@ -0,0 +1,4 @@ +AnalysisType: global +GlobalID: "panther_config_defaults" +Filename: panther_config_defaults.py +Description: Default Configuration values for Panther diff --git a/global_helpers/panther_config_overrides.py b/global_helpers/panther_config_overrides.py new file mode 100644 index 000000000..bfb547594 --- /dev/null +++ b/global_helpers/panther_config_overrides.py @@ -0,0 +1,43 @@ +""" +Here, you can override the default, example configuration values from `panther_config_defaults` + +Any attribute found to be defined in here will take precedence at lookup time. +For example, we can totally re-define a value: + +# Total Override +panther_config_defaults.py +``` +SUSPICIOUS_DOMAINS = [ "evil.example.com" ] +``` + +panther_config_overrides.py +``` +SUSPICIOUS_DOMAINS = [ "betrug.example.com" ] +``` + +and at lookup-time: +``` +from panther_config import config +print(config.SUSPICIOUS_DOMAINS) +``` +prints ["betrug.example.com"] + +# Mixing Values +panther_config_defaults.py +``` +INTERNAL_NETWORKS = [ "10.0.0.0/8" ] +``` + +panther_config_overrides.py +``` +import panther_config_defaults +INTERNAL_NETWORKS = panther_config_defaults.INTERNAL_NETWORKS + [ "192.0.2.0/24" ] +``` + +and at lookup-time: +``` +from panther_config import config +print(config.INTERNAL_NETWORKS) +``` +prints ["10.0.0.0/8", "192.0.2.0/24" ] +""" diff --git a/global_helpers/panther_config_overrides.yml b/global_helpers/panther_config_overrides.yml new file mode 100644 index 000000000..a5187dd60 --- /dev/null +++ b/global_helpers/panther_config_overrides.yml @@ -0,0 +1,4 @@ +AnalysisType: global +GlobalID: "panther_config_overrides" +Filename: panther_config_overrides.py +Description: Overridden Configuration values for Panther diff --git a/packs/box.yml b/packs/box.yml index 822c895d4..2b0b593eb 100644 --- a/packs/box.yml +++ b/packs/box.yml @@ -15,4 +15,7 @@ PackDefinition: # Globals used in these detections - panther_base_helpers - panther_box_helpers + - panther_config + - panther_config_defaults + - panther_config_overrides DisplayName: "Panther Box Pack" diff --git a/packs/dropbox.yml b/packs/dropbox.yml index 18cd79bb9..3ad8e3845 100644 --- a/packs/dropbox.yml +++ b/packs/dropbox.yml @@ -11,4 +11,7 @@ PackDefinition: - Dropbox.Admin.sign.in.as.Session # Globals used in these detections - panther_base_helpers + - panther_config + - panther_config_defaults + - panther_config_overrides DisplayName: "Panther Dropbox Pack" diff --git a/packs/msft_graph.yml b/packs/msft_graph.yml index 6c7234572..231abe46c 100644 --- a/packs/msft_graph.yml +++ b/packs/msft_graph.yml @@ -10,4 +10,7 @@ PackDefinition: - Microsoft365.Exchange.External.Forwarding # Globals - panther_base_helpers + - panther_config + - panther_config_defaults + - panther_config_overrides DisplayName: "Microsoft Graph Detection Pack" diff --git a/rules/box_rules/box_event_triggered_externally.py b/rules/box_rules/box_event_triggered_externally.py index 8cef8a4eb..defda7226 100644 --- a/rules/box_rules/box_event_triggered_externally.py +++ b/rules/box_rules/box_event_triggered_externally.py @@ -1,8 +1,7 @@ from panther_base_helpers import deep_get +from panther_config import config -DOMAINS = { - "@example.com", -} +DOMAINS = {"@" + domain for domain in config.ORGANIZATION_DOMAINS} def rule(event): diff --git a/rules/dropbox_rules/dropbox_external_share.py b/rules/dropbox_rules/dropbox_external_share.py index 45e3a2f5e..86a9f206a 100644 --- a/rules/dropbox_rules/dropbox_external_share.py +++ b/rules/dropbox_rules/dropbox_external_share.py @@ -2,21 +2,22 @@ from unittest.mock import MagicMock from panther_base_helpers import deep_get +from panther_config import config -ALLOWED_DOMAINS = [ - # "example.com" -] +DROPBOX_ALLOWED_SHARE_DOMAINS = config.DROPBOX_ALLOWED_SHARE_DOMAINS def rule(event): - global ALLOWED_DOMAINS # pylint: disable=global-statement - if isinstance(ALLOWED_DOMAINS, MagicMock): - ALLOWED_DOMAINS = set(json.loads(ALLOWED_DOMAINS())) # pylint: disable=not-callable + global DROPBOX_ALLOWED_SHARE_DOMAINS # pylint: disable=global-statement + if isinstance(DROPBOX_ALLOWED_SHARE_DOMAINS, MagicMock): + DROPBOX_ALLOWED_SHARE_DOMAINS = set( + json.loads(DROPBOX_ALLOWED_SHARE_DOMAINS()) + ) # pylint: disable=not-callable if deep_get(event, "event_type", "_tag", default="") == "shared_content_add_member": participants = event.get("participants", [{}]) for participant in participants: email = participant.get("user", {}).get("email", "") - if email.split("@")[-1] not in ALLOWED_DOMAINS: + if email.split("@")[-1] not in DROPBOX_ALLOWED_SHARE_DOMAINS: return True return False @@ -28,7 +29,7 @@ def title(event): external_participants = [] for participant in participants: email = participant.get("user", {}).get("email", "") - if email.split("@")[-1] not in ALLOWED_DOMAINS: + if email.split("@")[-1] not in DROPBOX_ALLOWED_SHARE_DOMAINS: external_participants.append(email) return f"Dropbox: [{actor}] shared [{assets}] with external user [{external_participants}]." @@ -38,6 +39,6 @@ def alert_context(event): participants = event.get("participants", [{}]) for participant in participants: email = participant.get("user", {}).get("email", "") - if email.split("@")[-1] not in ALLOWED_DOMAINS: + if email.split("@")[-1] not in DROPBOX_ALLOWED_SHARE_DOMAINS: external_participants.append(email) return {"external_participants": external_participants} diff --git a/rules/dropbox_rules/dropbox_external_share.yml b/rules/dropbox_rules/dropbox_external_share.yml index 79bb87c5b..30c96a279 100644 --- a/rules/dropbox_rules/dropbox_external_share.yml +++ b/rules/dropbox_rules/dropbox_external_share.yml @@ -7,7 +7,7 @@ Severity: Medium Tests: - ExpectedResult: false Mocks: - - objectName: ALLOWED_DOMAINS + - objectName: DROPBOX_ALLOWED_SHARE_DOMAINS returnValue: >- [ "example.com" diff --git a/rules/dropbox_rules/dropbox_ownership_transfer.py b/rules/dropbox_rules/dropbox_ownership_transfer.py index a41f8a5a0..0392372c6 100644 --- a/rules/dropbox_rules/dropbox_ownership_transfer.py +++ b/rules/dropbox_rules/dropbox_ownership_transfer.py @@ -2,10 +2,9 @@ from unittest.mock import MagicMock from panther_base_helpers import deep_get +from panther_config import config -ALLOWED_DOMAINS = [ - # "example.com" -] +DROPBOX_TRUSTED_OWNERSHIP_DOMAINS = config.DROPBOX_TRUSTED_OWNERSHIP_DOMAINS def rule(event): @@ -27,10 +26,12 @@ def title(event): def severity(event): - global ALLOWED_DOMAINS # pylint: disable=global-statement - if isinstance(ALLOWED_DOMAINS, MagicMock): - ALLOWED_DOMAINS = set(json.loads(ALLOWED_DOMAINS())) # pylint: disable=not-callable + global DROPBOX_TRUSTED_OWNERSHIP_DOMAINS # pylint: disable=global-statement + if isinstance(DROPBOX_TRUSTED_OWNERSHIP_DOMAINS, MagicMock): + DROPBOX_TRUSTED_OWNERSHIP_DOMAINS = set( + json.loads(DROPBOX_TRUSTED_OWNERSHIP_DOMAINS()) + ) # pylint: disable=not-callable new_owner = deep_get(event, "details", "new_owner_email", default="") - if new_owner.split("@")[-1] not in ALLOWED_DOMAINS: + if new_owner.split("@")[-1] not in DROPBOX_TRUSTED_OWNERSHIP_DOMAINS: return "HIGH" return "LOW" diff --git a/rules/dropbox_rules/dropbox_ownership_transfer.yml b/rules/dropbox_rules/dropbox_ownership_transfer.yml index 836d006aa..ff3aff6c3 100644 --- a/rules/dropbox_rules/dropbox_ownership_transfer.yml +++ b/rules/dropbox_rules/dropbox_ownership_transfer.yml @@ -131,7 +131,7 @@ Tests: Name: Other - ExpectedResult: true Mocks: - - objectName: ALLOWED_DOMAINS + - objectName: DROPBOX_TRUSTED_OWNERSHIP_DOMAINS returnValue: >- [ "example.com" diff --git a/rules/gsuite_activityevent_rules/gsuite_doc_ownership_transfer.py b/rules/gsuite_activityevent_rules/gsuite_doc_ownership_transfer.py index 83eb0365c..6aa5e9920 100644 --- a/rules/gsuite_activityevent_rules/gsuite_doc_ownership_transfer.py +++ b/rules/gsuite_activityevent_rules/gsuite_doc_ownership_transfer.py @@ -1,7 +1,8 @@ from panther_base_helpers import deep_get +from panther_config import config -ORG_DOMAINS = { - "@example.com", +GSUITE_TRUSTED_OWNERSHIP_DOMAINS = { + "@" + domain for domain in config.GSUITE_TRUSTED_OWNERSHIP_DOMAINS } @@ -11,5 +12,7 @@ def rule(event): if bool(event.get("name") == "TRANSFER_DOCUMENT_OWNERSHIP"): new_owner = deep_get(event, "parameters", "NEW_VALUE", default="") - return bool(new_owner) and not any(new_owner.endswith(x) for x in ORG_DOMAINS) + return bool(new_owner) and not any( + new_owner.endswith(x) for x in GSUITE_TRUSTED_OWNERSHIP_DOMAINS + ) return False diff --git a/rules/gsuite_activityevent_rules/gsuite_external_forwarding.py b/rules/gsuite_activityevent_rules/gsuite_external_forwarding.py index 1e668baa9..0b2306c6b 100644 --- a/rules/gsuite_activityevent_rules/gsuite_external_forwarding.py +++ b/rules/gsuite_activityevent_rules/gsuite_external_forwarding.py @@ -1,6 +1,5 @@ from panther_base_helpers import deep_get - -ALLOWED_DOMAINS = ["example.com"] # List of external domains that are allowed to be forwarded to +from panther_config import config def rule(event): @@ -11,7 +10,7 @@ def rule(event): domain = deep_get(event, "parameters", "email_forwarding_destination_address").split("@")[ -1 ] - if domain not in ALLOWED_DOMAINS: + if domain not in config.GSUITE_TRUSTED_FORWARDING_DESTINATION_DOMAINS: return True return False diff --git a/rules/microsoft_rules/microsoft_exchange_external_forwarding.py b/rules/microsoft_rules/microsoft_exchange_external_forwarding.py index 83bcc6ad8..37cfa5852 100644 --- a/rules/microsoft_rules/microsoft_exchange_external_forwarding.py +++ b/rules/microsoft_rules/microsoft_exchange_external_forwarding.py @@ -1,6 +1,4 @@ -ALLOWED_FORWARDING_DESTINATION_DOMAINS = ["company.com"] - -ALLOWED_FORWARDING_DESTINATION_EMAILS = ["exception@example.com"] +from panther_config import config def rule(event): @@ -8,9 +6,12 @@ def rule(event): for param in event.get("parameters", []): if param.get("Name", "") in ("ForwardingSmtpAddress", "ForwardTo", "ForwardingAddress"): to_email = param.get("Value", "") - if to_email.lower().replace("smtp:", "") in ALLOWED_FORWARDING_DESTINATION_EMAILS: + if ( + to_email.lower().replace("smtp:", "") + in config.MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_EMAILS + ): return False - for domain in ALLOWED_FORWARDING_DESTINATION_DOMAINS: + for domain in config.MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_DOMAINS: if to_email.lower().replace("smtp:", "").endswith(domain): return False return True diff --git a/rules/microsoft_rules/microsoft_exchange_external_forwarding.yml b/rules/microsoft_rules/microsoft_exchange_external_forwarding.yml index a8fc013a7..bd2a734f1 100644 --- a/rules/microsoft_rules/microsoft_exchange_external_forwarding.yml +++ b/rules/microsoft_rules/microsoft_exchange_external_forwarding.yml @@ -76,7 +76,7 @@ Tests: - Name: Force Value: "False" - Name: ForwardTo - Value: hello@company.com + Value: hello@example.com - Name: Name Value: test forwarding - Name: StopProcessingRules @@ -105,7 +105,7 @@ Tests: - Name: Force Value: "False" - Name: ForwardTo - Value: exception@example.com + Value: postmaster@example.com - Name: Name Value: test forwarding - Name: StopProcessingRules @@ -132,7 +132,7 @@ Tests: - Name: Identity Value: ABC1.prod.outlook.com/Microsoft Exchange Hosted Organizations/simpsons.onmicrosoft.com/homer.simpson - Name: ForwardingSmtpAddress - Value: smtp:hello@company.com + Value: smtp:hello@example.com - Name: DeliverToMailboxAndForward Value: "False" recordtype: 1 @@ -157,7 +157,7 @@ Tests: - Name: Identity Value: ABC1.prod.outlook.com/Microsoft Exchange Hosted Organizations/simpsons.onmicrosoft.com/homer.simpson - Name: ForwardingSmtpAddress - Value: smtp:exception@example.com + Value: smtp:postmaster@example.com - Name: DeliverToMailboxAndForward Value: "False" recordtype: 1