From 0bd9fa8058091ae7e0e862b09df0bcfff5a704d5 Mon Sep 17 00:00:00 2001 From: Evan Gibler Date: Wed, 29 Nov 2023 10:58:53 -0600 Subject: [PATCH 01/10] Update GitHub Data Model to display admin-add events instead of UNKNOWN_ROLE (#979) --- data_models/github_data_model.py | 16 ++++++++++----- rules/standard_rules/admin_assigned.yml | 27 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/data_models/github_data_model.py b/data_models/github_data_model.py index e381fe74c..b6f337089 100644 --- a/data_models/github_data_model.py +++ b/data_models/github_data_model.py @@ -1,14 +1,20 @@ import panther_event_type_helpers as event_type +ADMIN_EVENTS = { + "business.add_admin", + "business.invite_admin", + "team.promote_maintainer", +} -def get_admin_role(_): - # github doesn't record the admin role in the event - return "" + +def get_admin_role(event): + action = event.get("action", "") + return action if action in ADMIN_EVENTS else "" def get_event_type(event): - if event.get("action") == "team.promote_maintainer": + if event.get("action", "") in ADMIN_EVENTS: return event_type.ADMIN_ROLE_ASSIGNED - if event.get("action") == "org.disable_two_factor_requirement": + if event.get("action", "") == "org.disable_two_factor_requirement": return event_type.MFA_DISABLED return None diff --git a/rules/standard_rules/admin_assigned.yml b/rules/standard_rules/admin_assigned.yml index c68aea2e4..a1f954efa 100644 --- a/rules/standard_rules/admin_assigned.yml +++ b/rules/standard_rules/admin_assigned.yml @@ -166,6 +166,33 @@ Tests: "p_log_type": "GitHub.Audit", "user": "bob" } + - Name: Github - Admin Added + ExpectedResult: true + Log: + { + "actor": "cat", + "action": "business.add_admin", + "p_log_type": "GitHub.Audit", + "user": "bob" + } + - Name: Github - Admin Invited + ExpectedResult: true + Log: + { + "actor": "cat", + "action": "business.invite_admin", + "p_log_type": "GitHub.Audit", + "user": "bob" + } + - Name: Github - Unknown Admin Role + ExpectedResult: false + Log: + { + "actor": "cat", + "action": "unknown.admin_role", + "p_log_type": "GitHub.Audit", + "user": "bob" + } - Name: Zendesk - Admin Role Downgraded ExpectedResult: false From fbd6e0990a548886a316336ac233b7b7dfe08522 Mon Sep 17 00:00:00 2001 From: Evan Gibler Date: Wed, 29 Nov 2023 12:40:51 -0600 Subject: [PATCH 02/10] Allow for auto-formatting on save when using VSCode (#981) --- .vscode/example_settings.json | 23 ++++++++++++++++++++--- README.md | 4 +++- pyproject.toml | 8 ++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 pyproject.toml diff --git a/.vscode/example_settings.json b/.vscode/example_settings.json index fb7c816b0..239926ced 100644 --- a/.vscode/example_settings.json +++ b/.vscode/example_settings.json @@ -1,8 +1,18 @@ { "python.defaultInterpreterPath": "XXX_pipenv_py_output_XXX", "yaml.schemas": { - "https://panther-community-us-east-1.s3.amazonaws.com/latest/logschema/schema.json": [ "schemas/*.yml", "schemas/*.yaml", "schemas/**/*yaml", "schemas/**/*.yaml"], - ".vscode/rule_jsonschema.json": [ "rules/*.yml", "rules/*.yaml", "rules/**/*.yaml", "rules/**/*.yml"] + "https://panther-community-us-east-1.s3.amazonaws.com/latest/logschema/schema.json": [ + "schemas/*.yml", + "schemas/*.yaml", + "schemas/**/*yaml", + "schemas/**/*.yaml" + ], + ".vscode/rule_jsonschema.json": [ + "rules/*.yml", + "rules/*.yaml", + "rules/**/*.yaml", + "rules/**/*.yml" + ] }, "python.analysis.extraPaths": [ "global_helpers" @@ -16,5 +26,12 @@ //"makefile.extensionOutputFolder": "./.vscode", "files.associations": { "panther_analysis_tool": "python" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 8b96a6457..ce654ad38 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ If you are comfortable using the Visual Studio Code IDE, the `make vscode-config In addition to this command, you will need to install these vscode add-ons: 1. [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) -2. [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) +2. [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) +3. [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) You will also need Visual Studio's [code](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) configured to open Visual Studio from your CLI. @@ -130,6 +131,7 @@ You will also need Visual Studio's [code](https://code.visualstudio.com/docs/set 1. Creates two debugging targets, which will give you single-button push support for running `panther_analysis_tool test` through the debugger. 1. Installs JSONSchema support for your custom panther-analysis schemas in the `schemas/` directory. This brings IDE hints about which fields are necessary for schemas/custom-schema.yml files. 1. Installs JSONSchema support for panther-analysis rules in the `rules/` directory. This brings IDE hints about which fields are necessary for rules/my-rule.yml files. +1. Configures `Black` and `isort` settings for auto-formatting on save (thus reducing the need to run `make fmt` on all files) ```shell user@computer:panther-analysis: make vscode-config diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..e4cea09ff --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 100 +target-version = ['py39'] +include = '\.pyi?$' + +[tool.isort] +line_length = 100 +profile = "black" From 10bdf4f1e26d0fc7d8c91f133fc27762630e0cde Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:37:36 -0700 Subject: [PATCH 03/10] updated GCP pack with some missing rules (#982) --- packs/gcp_audit.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packs/gcp_audit.yml b/packs/gcp_audit.yml index 3807f3781..488b7f00d 100644 --- a/packs/gcp_audit.yml +++ b/packs/gcp_audit.yml @@ -8,16 +8,26 @@ PackDefinition: - GCP.BigQuery.Large.Scan - GCP.Cloud.Storage.Buckets.Modified.Or.Deleted - GCP.Destructive.Queries + - GCP.DNS.Zone.Modified.or.Deleted + - GCP.Firewall.Rule.Created + - GCP.Firewall.Rule.Deleted + - GCP.Firewall.Rule.Modified - GCP.GCS.IAMChanges - GCP.GCS.Public + - GCP.IAM.AdminRoleAssigned - GCP.IAM.CorporateEmail - GCP.IAM.CustomRoleChanges - GCP.IAM.OrgFolderIAMChanges - GCP.Inbound.SSO.Profile.Created + - GCP.K8s.ExecIntoPod + - GCP.Log.Bucket.Or.Sink.Deleted - GCP.Logging.Settings.Modified + - GCP.Logging.Sink.Modified - GCP.Permissions.Granted.to.Create.or.Manage.Service.Account.Key + - GCP.Service.Account.Access.Denied - GCP.Service.Account.or.Keys.Created - GCP.SQL.ConfigChanges + - GCP.UnusedRegions - GCP.User.Added.to.IAP.Protected.Service - GCP.VPC.Flow.Logs.Disabled - GCP.Workforce.Pool.Created.or.Updated From c25a5ab97a53d703caa478399f179d47bb3dde99 Mon Sep 17 00:00:00 2001 From: Evan Gibler Date: Thu, 30 Nov 2023 17:40:19 -0600 Subject: [PATCH 04/10] Add linting config to example_settings.json (#984) * Add linting config to example_settings.json * Update README.md --- .bandit | 2 ++ .pylintrc | 15 +++++++++++++++ .vscode/example_settings.json | 14 +++++++++----- Makefile | 7 ++----- README.md | 7 ++++++- 5 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .bandit create mode 100644 .pylintrc diff --git a/.bandit b/.bandit new file mode 100644 index 000000000..a1be52009 --- /dev/null +++ b/.bandit @@ -0,0 +1,2 @@ +[bandit] +skips = B101 diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..a7425ed1c --- /dev/null +++ b/.pylintrc @@ -0,0 +1,15 @@ +[MAIN] +disable= + missing-docstring, + duplicate-code, + import-error, + fixme, + consider-iterating-dictionary, + global-variable-not-assigned, + broad-exception-raised + +load-plugins= + pylint.extensions.mccabe, + pylint_print + +max-line-length=100 diff --git a/.vscode/example_settings.json b/.vscode/example_settings.json index 239926ced..0b3f98e3c 100644 --- a/.vscode/example_settings.json +++ b/.vscode/example_settings.json @@ -21,9 +21,6 @@ "**/__pycache": true, "**/*pyc": true }, - //"python.analysis.logLevel": "Trace", - //"files.autoSave": "afterDelay", - //"makefile.extensionOutputFolder": "./.vscode", "files.associations": { "panther_analysis_tool": "python" }, @@ -32,6 +29,13 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true - } - } + }, + }, + // Add pylint.lintOnChange to your User (not Workspace) settings + // Cmd+Shift+P -> Preferences: Open Settings (JSON) + "pylint.lintOnChange": true, + "bandit.args": [ + "-r", + "." + ] } diff --git a/Makefile b/Makefile index 2edc7ff73..0c7068c3f 100644 --- a/Makefile +++ b/Makefile @@ -35,11 +35,8 @@ global-helpers-unit-test: lint: lint-pylint lint-fmt lint-pylint: - pipenv run bandit -r $(dirs) --skip B101 # allow assert statements in tests - pipenv run pylint $(dirs) \ - --disable=missing-docstring,duplicate-code,import-error,fixme,consider-iterating-dictionary,global-variable-not-assigned,broad-exception-raised \ - --load-plugins=pylint.extensions.mccabe,pylint_print \ - --max-line-length=100 + pipenv run bandit -r $(dirs) + pipenv run pylint $(dirs) lint-fmt: @echo Checking python file formatting with the black code style checker diff --git a/README.md b/README.md index ce654ad38..8d0bbd72b 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,9 @@ In addition to this command, you will need to install these vscode add-ons: 1. [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) 2. [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) -3. [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) +3. [Pylint](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint) +4 [Bandit](https://marketplace.visualstudio.com/items?itemName=nwgh.bandit) +5. [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) You will also need Visual Studio's [code](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) configured to open Visual Studio from your CLI. @@ -132,6 +134,9 @@ You will also need Visual Studio's [code](https://code.visualstudio.com/docs/set 1. Installs JSONSchema support for your custom panther-analysis schemas in the `schemas/` directory. This brings IDE hints about which fields are necessary for schemas/custom-schema.yml files. 1. Installs JSONSchema support for panther-analysis rules in the `rules/` directory. This brings IDE hints about which fields are necessary for rules/my-rule.yml files. 1. Configures `Black` and `isort` settings for auto-formatting on save (thus reducing the need to run `make fmt` on all files) +1. Configures `pylint` settings for linting when changes are made + - Ensure that `"pylint.lintOnChange": true` is present in the User-level VSCode settings (`Cmd+Shift+P` -> `Preferences: Open Settings (JSON)`) +1. Configures `Bandit` settings for linting when files are opened ```shell user@computer:panther-analysis: make vscode-config From 08c5cc20ca110aa4f7403e441ddc8855a09d197a Mon Sep 17 00:00:00 2001 From: dotbeseck <77022002+dotbeseck@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:04:20 -0500 Subject: [PATCH 05/10] Update kubernetes_pod_create_or_modify_host_path_vol_mount_query.yml (#983) Missing a tick in the hostPath where for /var/run/docker.sock Co-authored-by: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> --- ...ubernetes_pod_create_or_modify_host_path_vol_mount_query.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/kubernetes_queries/kubernetes_pod_create_or_modify_host_path_vol_mount_query.yml b/queries/kubernetes_queries/kubernetes_pod_create_or_modify_host_path_vol_mount_query.yml index 675b5530a..ed9c73a4a 100644 --- a/queries/kubernetes_queries/kubernetes_pod_create_or_modify_host_path_vol_mount_query.yml +++ b/queries/kubernetes_queries/kubernetes_pod_create_or_modify_host_path_vol_mount_query.yml @@ -14,7 +14,7 @@ Query: > WHERE verb IN ('create', 'update', 'patch') AND objectRef:resource = 'pods' - AND request_object:spec:volumes[0]:hostPath:path ilike ANY (/var/run/docker.sock','/var/run/crio/crio.sock','/var/lib/kubelet','/var/lib/kubelet/pki','/var/lib/docker/overlay2','/etc/kubernetes','/etc/kubernetes/manifests','/etc/kubernetes/pki','/home/admin') + AND request_object:spec:volumes[0]:hostPath:path ilike ANY ('/var/run/docker.sock','/var/run/crio/crio.sock','/var/lib/kubelet','/var/lib/kubelet/pki','/var/lib/docker/overlay2','/etc/kubernetes','/etc/kubernetes/manifests','/etc/kubernetes/pki','/home/admin') AND p_occurs_since('30 minutes') --insert allow-list for expected workloads that require a sensitive mount LIMIT 10 From fd2574b4e22734365d363fb8ff94c056e7f7bf22 Mon Sep 17 00:00:00 2001 From: Jonathan Lassoff Date: Mon, 4 Dec 2023 09:52:09 -0800 Subject: [PATCH 06/10] Add a config system for Panther detections (#950) * panther_config: Add a fork-friendly configuration scheme * Apply `panther_config` to existing uses of example.com --------- Co-authored-by: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> --- .gitattributes | 1 + global_helpers/panther_config.py | 14 ++++++ global_helpers/panther_config.yml | 4 ++ global_helpers/panther_config_defaults.py | 14 ++++++ global_helpers/panther_config_defaults.yml | 4 ++ global_helpers/panther_config_overrides.py | 43 +++++++++++++++++++ global_helpers/panther_config_overrides.yml | 4 ++ packs/box.yml | 3 ++ packs/dropbox.yml | 3 ++ packs/msft_graph.yml | 3 ++ .../box_event_triggered_externally.py | 5 +-- rules/dropbox_rules/dropbox_external_share.py | 19 ++++---- .../dropbox_rules/dropbox_external_share.yml | 2 +- .../dropbox_ownership_transfer.py | 15 ++++--- .../dropbox_ownership_transfer.yml | 2 +- .../gsuite_doc_ownership_transfer.py | 9 ++-- .../gsuite_external_forwarding.py | 5 +-- .../microsoft_exchange_external_forwarding.py | 11 ++--- ...microsoft_exchange_external_forwarding.yml | 8 ++-- 19 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 global_helpers/panther_config.py create mode 100644 global_helpers/panther_config.yml create mode 100644 global_helpers/panther_config_defaults.py create mode 100644 global_helpers/panther_config_defaults.yml create mode 100644 global_helpers/panther_config_overrides.py create mode 100644 global_helpers/panther_config_overrides.yml 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 From b863082bb6b069d45534ac9602e5e793bac7bfb9 Mon Sep 17 00:00:00 2001 From: Jonathan Lassoff Date: Mon, 4 Dec 2023 09:54:54 -0800 Subject: [PATCH 07/10] Update Teleport Rules (#955) * panther_config: Add a fork-friendly configuration scheme * Apply `panther_config` to existing uses of example.com * Teleport: Update Rules, using panther_config --------- Co-authored-by: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> --- global_helpers/panther_config_defaults.py | 1 + ...eport_company_domain_login_without_saml.py | 22 +++++++ ...port_company_domain_login_without_saml.yml | 63 +++++++++++++++++++ .../teleport_saml_login_not_company_domain.py | 23 +++++++ ...teleport_saml_login_not_company_domain.yml | 63 +++++++++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.py create mode 100644 rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.yml create mode 100644 rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.py create mode 100644 rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.yml diff --git a/global_helpers/panther_config_defaults.py b/global_helpers/panther_config_defaults.py index 6b1a5e70e..87a8f01bb 100644 --- a/global_helpers/panther_config_defaults.py +++ b/global_helpers/panther_config_defaults.py @@ -12,3 +12,4 @@ GSUITE_TRUSTED_OWNERSHIP_DOMAINS = ORGANIZATION_DOMAINS MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_DOMAINS = ORGANIZATION_DOMAINS MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_EMAILS = ["postmaster@" + ORGANIZATION_DOMAINS[0]] +TELEPORT_ORGANIZATION_DOMAINS = ORGANIZATION_DOMAINS diff --git a/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.py b/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.py new file mode 100644 index 000000000..b9cb6470b --- /dev/null +++ b/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.py @@ -0,0 +1,22 @@ +import re + +from panther_config import config + +TELEPORT_ORGANIZATION_DOMAINS_REGEX = r"@(" + "|".join(config.TELEPORT_ORGANIZATION_DOMAINS) + r")$" + + +def rule(event): + return bool( + event.get("event") == "user.login" + and event.get("success") is True + and bool(re.search(TELEPORT_ORGANIZATION_DOMAINS_REGEX, event.get("user"))) + and event.get("method") != "saml" + ) + + +def title(event): + return ( + f"User [{event.get('user', '')}] logged into " + f"[{event.get('cluster_name', '')}] without " + f"using SAML" + ) diff --git a/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.yml b/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.yml new file mode 100644 index 000000000..3ac8dd71b --- /dev/null +++ b/rules/gravitational_teleport_rules/teleport_company_domain_login_without_saml.yml @@ -0,0 +1,63 @@ +AnalysisType: rule +Filename: teleport_company_domain_login_without_saml.py +RuleID: Teleport.CompanyDomainLoginWithoutSAML +DisplayName: "A User from the company domain(s) Logged in without SAML" +Enabled: true +LogTypes: + - Gravitational.TeleportAudit +Tags: + - Teleport +Severity: High +Description: "A User from the company domain(s) Logged in without SAML" +DedupPeriodMinutes: 60 +Reports: + MITRE ATT&CK: + - TA0005:T1562 +Reference: https://goteleport.com/docs/management/admin/ +Runbook: > + A User from the company domain(s) Logged in without SAML +SummaryAttributes: + - event + - code + - user + - method + - mfa_device +Tests: + - + Name: A User from the company domain(s) logged in with SAML + ExpectedResult: false + Log: + { + "attributes": { + "firstName": [ + "" + ], + "groups": [ + "employees" + ] + }, + "cluster_name": "teleport.example.com", + "code": "T1001I", + "ei": 0, + "event": "user.login", + "method": "saml", + "success": true, + "time": "2023-09-18 00:00:00", + "uid": "88888888-4444-4444-4444-222222222222", + "user": "jane.doe@example.com" + } + - + Name: A User from the company domain(s) logged in without SAML + ExpectedResult: true + Log: + { + "cluster_name": "teleport.example.com", + "code": "T1001I", + "ei": 0, + "event": "user.login", + "method": "local", + "success": true, + "time": "2023-09-18 00:00:00", + "uid": "88888888-4444-4444-4444-222222222222", + "user": "jane.doe@example.com" + } diff --git a/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.py b/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.py new file mode 100644 index 000000000..c80a9298a --- /dev/null +++ b/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.py @@ -0,0 +1,23 @@ +import re + +from panther_config import config + +TELEPORT_COMPANY_DOMAINS_REGEX = r"@(" + "|".join(config.TELEPORT_ORGANIZATION_DOMAINS) + r")$" + + +def rule(event): + return ( + event.get("event") == "user.login" + and event.get("success") is True + and event.get("method") == "saml" + and not re.search(TELEPORT_COMPANY_DOMAINS_REGEX, event.get("user")) + ) + + +def title(event): + return ( + f"User [{event.get('user', '')}] logged into " + f"[{event.get('cluster_name', '')}] using " + f"SAML, but not from a known company domain in " + f"({','.join(config.TELEPORT_ORGANIZATION_DOMAINS)})" + ) diff --git a/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.yml b/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.yml new file mode 100644 index 000000000..a8765293f --- /dev/null +++ b/rules/gravitational_teleport_rules/teleport_saml_login_not_company_domain.yml @@ -0,0 +1,63 @@ +AnalysisType: rule +Filename: teleport_saml_login_not_company_domain.py +RuleID: Teleport.SAMLLoginWithoutCompanyDomain +DisplayName: "A user authenticated with SAML, but from an unknown company domain" +Enabled: true +LogTypes: + - Gravitational.TeleportAudit +Tags: + - Teleport +Severity: High +Description: "A user authenticated with SAML, but from an unknown company domain" +DedupPeriodMinutes: 60 +Reports: + MITRE ATT&CK: + - TA0003:T1098 +Reference: https://goteleport.com/docs/management/admin/ +Runbook: > + A user authenticated with SAML, but from an unknown company domain +SummaryAttributes: + - event + - code + - user + - method + - mfa_device +Tests: + - + Name: A user authenticated with SAML, but from a known company domain + ExpectedResult: false + Log: + { + "attributes": { + "firstName": [ + "" + ], + "groups": [ + "employees" + ] + }, + "cluster_name": "teleport.example.com", + "code": "T1001I", + "ei": 0, + "event": "user.login", + "method": "saml", + "success": true, + "time": "2023-09-18 00:00:00", + "uid": "88888888-4444-4444-4444-222222222222", + "user": "jane.doe@example.com" + } + - + Name: A user authenticated with SAML, but not from a company domain + ExpectedResult: true + Log: + { + "cluster_name": "teleport.example.com", + "code": "T1001I", + "ei": 0, + "event": "user.login", + "method": "saml", + "success": true, + "time": "2023-09-18 00:00:00", + "uid": "88888888-4444-4444-4444-222222222222", + "user": "wtf.how@omghax.gravitational.io" + } From cc4139ddb3a2c4a126c06d707c74c892457da9ef Mon Sep 17 00:00:00 2001 From: Ariel Ropek <79653153+arielkr256@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:28:04 -0700 Subject: [PATCH 08/10] gsuite pack refresh (#987) --- packs/gsuite_reports.yml | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packs/gsuite_reports.yml b/packs/gsuite_reports.yml index 61c0fd889..b8f69fb56 100644 --- a/packs/gsuite_reports.yml +++ b/packs/gsuite_reports.yml @@ -3,27 +3,48 @@ PackID: PantherManaged.GSuite.Reports Description: Panther GSuite Detections PackDefinition: IDs: + - Google.Workspace.Admin.Custom.Role + - Google.Workspace.Advanced.Protection.Program + - Google.Workspace.Apps.Marketplace.Allowlist + - Google.Workspace.Apps.Marketplace.New.Domain.Application + - Google.Workspace.Apps.New.Mobile.App.Installed - GSuite.AdvancedProtection - - GSuite.DriveOverlyVisible + - GSuite.BruteForceLogin + - GSuite.CalendarMadePublic + - GSuite.DocOwnershipTransfer + - GSuite.Drive.Many.Documents.Deleted + - Google.Drive.High.Download.Count + - GSuite.ExternalMailForwarding - GSuite.GoogleAccess - GSuite.GovernmentBackedAttack - GSuite.GroupBannedUser - GSuite.LeakedPassword - GSuite.LoginType - - GSuite.Rule - GSuite.DeviceCompromise - GSuite.DeviceUnlockFailure - GSuite.DeviceSuspiciousActivity + - GSuite.Rule + - GSuite.PermisssionsDelegated - GSuite.SuspiciousLogins - GSuite.TwoStepVerification - GSuite.UserSuspended - - Google.Workspace.Admin.Custom.Role - - Google.Workspace.Advanced.Protection.Program - - Google.Workspace.Apps.Marketplace.New.Domain.Application - - Google.Workspace.Apps.Marketplace.Allowlist - - Google.Workspace.Apps.New.Mobile.App.Installed + - GSuite.Workspace.CalendarExternalSharingSetting + - GSuite.Workspace.DataExportCreated + - GSuite.Workspace.GmailDefaultRoutingRuleModified + - GSuite.Workspace.GmailPredeliveryScanningDisabled + - GSuite.Workspace.GmailSecuritySandboxDisabled + - GSuite.Workspace.PasswordEnforceStrongDisabled + - GSuite.Workspace.PasswordReuseEnabled + - GSuite.Workspace.TrustedDomainsAllowlist + - GSuite.Drive.ExternalFileShare + - GSuite.DriveOverlyVisible + - GSuite.DriveVisibilityChanged + - GSuite.DriveVisiblityChanged # Data Models used in these detections - Standard.GSuite.Reports # Globals used in these detections - panther_base_helpers + - panther_config + - panther_config_defaults + - panther_config_overrides DisplayName: "Panther GSuite Pack" From 89bc775c81fa3775c7e9be60387d7ad3e0eba102 Mon Sep 17 00:00:00 2001 From: akozlovets098 <95437895+akozlovets098@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:09:32 +0200 Subject: [PATCH 09/10] Move URL from Description to Reference (microsoft_rules) (#986) --- rules/microsoft_rules/microsoft_graph_passthrough.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/microsoft_rules/microsoft_graph_passthrough.yml b/rules/microsoft_rules/microsoft_graph_passthrough.yml index 3b7e6db5a..4887e657c 100644 --- a/rules/microsoft_rules/microsoft_graph_passthrough.yml +++ b/rules/microsoft_rules/microsoft_graph_passthrough.yml @@ -1,5 +1,6 @@ AnalysisType: rule -Description: The Microsoft Graph security API federates queries to all onboarded security providers, including Azure AD Identity Protection, Microsoft 365, Microsoft Defender (Cloud, Endpoint, Identity) and Microsoft Sentinel. Details https://learn.microsoft.com/en-us/graph/api/resources/security-api-overview +Description: The Microsoft Graph security API federates queries to all onboarded security providers, including Azure AD Identity Protection, Microsoft 365, Microsoft Defender (Cloud, Endpoint, Identity) and Microsoft Sentinel +Reference: https://learn.microsoft.com/en-us/graph/api/resources/security-api-overview DisplayName: "Microsoft Graph Passthrough" Enabled: true Filename: microsoft_graph_passthrough.py From 19e3b7bd8f5838a3659d6dc06fd806cb1cfc5d09 Mon Sep 17 00:00:00 2001 From: akozlovets098 <95437895+akozlovets098@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:12:16 +0200 Subject: [PATCH 10/10] Move URL from Description to Reference (okta_rules) (#985) --- rules/okta_rules/okta_app_refresh_access_token_reuse.yml | 6 +++++- .../okta_threatinsight_security_threat_detected.yml | 3 ++- rules/okta_rules/okta_user_reported_suspicious_activity.yml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rules/okta_rules/okta_app_refresh_access_token_reuse.yml b/rules/okta_rules/okta_app_refresh_access_token_reuse.yml index 4126aed47..8afafca42 100644 --- a/rules/okta_rules/okta_app_refresh_access_token_reuse.yml +++ b/rules/okta_rules/okta_app_refresh_access_token_reuse.yml @@ -1,5 +1,9 @@ AnalysisType: rule -Description: https://developer.okta.com/docs/guides/refresh-tokens/main/#refresh-token-reuse-detection +Description: |- + When a client wants to renew an access token, it sends the refresh token with the access token request to the /token Okta endpoint. + Okta validates the incoming refresh token, issues a new set of tokens and invalidates the refresh token that was passed with the initial request. + This detection alerts when a previously used refresh token is used again with the token request +Reference: https://developer.okta.com/docs/guides/refresh-tokens/main/#refresh-token-reuse-detection DisplayName: "Okta App Refresh Access Token Reuse" Enabled: true Filename: okta_app_refresh_access_token_reuse.py diff --git a/rules/okta_rules/okta_threatinsight_security_threat_detected.yml b/rules/okta_rules/okta_threatinsight_security_threat_detected.yml index d9d908d84..4a22aa534 100644 --- a/rules/okta_rules/okta_threatinsight_security_threat_detected.yml +++ b/rules/okta_rules/okta_threatinsight_security_threat_detected.yml @@ -1,5 +1,6 @@ AnalysisType: rule -Description: https://help.okta.com/en-us/Content/Topics/Security/threat-insight/configure-threatinsight-system-log.htm +Description: Okta ThreatInsight identified request from potentially malicious IP address +Reference: https://help.okta.com/en-us/Content/Topics/Security/threat-insight/configure-threatinsight-system-log.htm DisplayName: "Okta ThreatInsight Security Threat Detected" Enabled: true Filename: okta_threatinsight_security_threat_detected.py diff --git a/rules/okta_rules/okta_user_reported_suspicious_activity.yml b/rules/okta_rules/okta_user_reported_suspicious_activity.yml index b5f9bc135..558fa976b 100644 --- a/rules/okta_rules/okta_user_reported_suspicious_activity.yml +++ b/rules/okta_rules/okta_user_reported_suspicious_activity.yml @@ -2,7 +2,7 @@ AnalysisType: rule Description: |- Suspicious Activity Reporting provides an end user with the option to report unrecognized activity from an account activity email notification. This detection alerts when a user marks the raised activity as suspicious. - https://help.okta.com/en-us/Content/Topics/Security/suspicious-activity-reporting.htm +Reference: https://help.okta.com/en-us/Content/Topics/Security/suspicious-activity-reporting.htm DisplayName: "Okta User Reported Suspicious Activity" Enabled: true Filename: okta_user_reported_suspicious_activity.py