Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a config system for Panther detections #950

Merged
merged 4 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.github/CODEOWNERS merge=ours
global_helpers/panther_config_overrides.py merge=ours
14 changes: 14 additions & 0 deletions global_helpers/panther_config.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 4 additions & 0 deletions global_helpers/panther_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AnalysisType: global
GlobalID: "panther_config"
Filename: panther_config.py
Description: Configuration values for Panther
14 changes: 14 additions & 0 deletions global_helpers/panther_config_defaults.py
Original file line number Diff line number Diff line change
@@ -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]]
4 changes: 4 additions & 0 deletions global_helpers/panther_config_defaults.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AnalysisType: global
GlobalID: "panther_config_defaults"
Filename: panther_config_defaults.py
Description: Default Configuration values for Panther
43 changes: 43 additions & 0 deletions global_helpers/panther_config_overrides.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome PR, thanks for opening it. Should add this file .gitattributes with ours ownership so it always overrides the upstream?

Copy link
Contributor Author

@jof jof Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that probably makes sense. From my user-perspective, we only ever merge upstream into our fork (or rebase). So long as we're never merging in the other direction (fork->upstream), this seems like the right choice.

Will add.

Original file line number Diff line number Diff line change
@@ -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" ]
"""
4 changes: 4 additions & 0 deletions global_helpers/panther_config_overrides.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AnalysisType: global
GlobalID: "panther_config_overrides"
Filename: panther_config_overrides.py
Description: Overridden Configuration values for Panther
3 changes: 3 additions & 0 deletions packs/box.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions packs/dropbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions packs/msft_graph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 2 additions & 3 deletions rules/box_rules/box_event_triggered_externally.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
19 changes: 10 additions & 9 deletions rules/dropbox_rules/dropbox_external_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}]."

Expand All @@ -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}
2 changes: 1 addition & 1 deletion rules/dropbox_rules/dropbox_external_share.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Severity: Medium
Tests:
- ExpectedResult: false
Mocks:
- objectName: ALLOWED_DOMAINS
- objectName: DROPBOX_ALLOWED_SHARE_DOMAINS
returnValue: >-
[
"example.com"
Expand Down
15 changes: 8 additions & 7 deletions rules/dropbox_rules/dropbox_ownership_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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="<NEW_OWNER_NOT_FOUND>")
if new_owner.split("@")[-1] not in ALLOWED_DOMAINS:
if new_owner.split("@")[-1] not in DROPBOX_TRUSTED_OWNERSHIP_DOMAINS:
return "HIGH"
return "LOW"
2 changes: 1 addition & 1 deletion rules/dropbox_rules/dropbox_ownership_transfer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Tests:
Name: Other
- ExpectedResult: true
Mocks:
- objectName: ALLOWED_DOMAINS
- objectName: DROPBOX_TRUSTED_OWNERSHIP_DOMAINS
returnValue: >-
[
"example.com"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}


Expand All @@ -11,5 +12,7 @@ def rule(event):

if bool(event.get("name") == "TRANSFER_DOCUMENT_OWNERSHIP"):
new_owner = deep_get(event, "parameters", "NEW_VALUE", default="<UNKNOWN USER>")
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
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
ALLOWED_FORWARDING_DESTINATION_DOMAINS = ["company.com"]

ALLOWED_FORWARDING_DESTINATION_EMAILS = ["[email protected]"]
from panther_config import config


def rule(event):
if event.get("operation", "") in ("Set-Mailbox", "New-InboxRule"):
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down