├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── story.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── fmt.yml │ ├── test_release_publish.yml │ └── version_bump_pr.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── SECURITY.md ├── bin ├── panther_analysis_tool └── pat ├── example_panther_config.yml ├── panther_analysis_tool ├── __init__.py ├── analysis_utils.py ├── backend │ ├── __init__.py │ ├── client.py │ ├── errors.py │ ├── graphql │ │ ├── async_bulk_upload.graphql │ │ ├── async_bulk_upload_status.graphql │ │ ├── bulk_upload.graphql │ │ ├── create_or_update_schema.graphql │ │ ├── create_perf_test.graphql │ │ ├── delete_detections.graphql │ │ ├── delete_saved_queries.graphql │ │ ├── feature_flags.graphql │ │ ├── generate_enriched_event.graphql │ │ ├── get_rule_body.graphql │ │ ├── get_version.graphql │ │ ├── introspection_query.graphql │ │ ├── list_schemas.graphql │ │ ├── metrics.graphql │ │ ├── replay.graphql │ │ ├── stop_replay.graphql │ │ ├── test_correlation_rule.graphql │ │ ├── transpile_filters.graphql │ │ ├── transpile_sdl.graphql │ │ ├── validate_bulk_upload.graphql │ │ └── validate_bulk_upload_status.graphql │ ├── lambda_client.py │ ├── mocks.py │ └── public_api_client.py ├── cli_output.py ├── command │ ├── __init__.py │ ├── benchmark.py │ ├── bulk_delete.py │ ├── check_connection.py │ ├── standard_args.py │ └── validate.py ├── constants.py ├── destination.py ├── detection_schemas │ ├── __init__.py │ └── analysis_config_schema.json ├── directory.py ├── enriched_event.py ├── enriched_event_generator.py ├── immutable.py ├── log_schemas │ ├── __init__.py │ └── user_defined.py ├── main.py ├── schema_regexs.py ├── schemas.py ├── testing.py ├── util.py ├── validation.py └── zip_chunker.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── tests ├── __init__.py ├── fixtures ├── __init__.py ├── check-packs │ ├── missing-dependencies │ │ ├── advanced_rules │ │ │ ├── example_rule_data_model.py │ │ │ └── example_rule_data_model.yml │ │ ├── correlation_rules │ │ │ ├── aws_cloudtrail_iaas.yml │ │ │ ├── discovering_exfiltrated_credentials.yml │ │ │ └── github_cicd.yml │ │ ├── data_models │ │ │ ├── aws_cloudtrail_data_model.py │ │ │ └── aws_cloudtrail_data_model.yml │ │ ├── global_helpers │ │ │ ├── a_helper.py │ │ │ ├── a_helper.yml │ │ │ ├── b_helper.py │ │ │ ├── b_helper.yml │ │ │ ├── helpers.py │ │ │ └── helpers.yml │ │ ├── packs │ │ │ ├── missing_datamodel.yml │ │ │ ├── missing_global.yml │ │ │ ├── missing_query.yml │ │ │ └── missing_subrules.yml │ │ ├── policies │ │ │ ├── example_policy.py │ │ │ ├── example_policy.yml │ │ │ ├── example_policy_beta.py │ │ │ ├── example_policy_beta.yml │ │ │ ├── example_policy_extraneous_fields.py │ │ │ ├── example_policy_extraneous_fields.yml │ │ │ ├── example_policy_generated_functions.py │ │ │ └── example_policy_generated_functions.yml │ │ ├── queries │ │ │ ├── query_one.yml │ │ │ ├── query_three.yml │ │ │ └── query_two.yml │ │ ├── rules │ │ │ ├── example_rule.py │ │ │ ├── example_rule.yml │ │ │ ├── example_rule_extraneous_fields.py │ │ │ ├── example_rule_extraneous_fields.yml │ │ │ ├── example_rule_generated_functions.py │ │ │ ├── example_rule_generated_functions.yml │ │ │ ├── example_rule_global.py │ │ │ ├── example_rule_global.yml │ │ │ ├── example_rule_mocks.py │ │ │ └── example_rule_mocks.yml │ │ └── scheduled_rules │ │ │ ├── example_scheduled_rule.py │ │ │ └── example_scheduled_rule.yml │ └── packless-rule │ │ ├── packs │ │ └── test.yml │ │ └── rules │ │ └── test_rules │ │ ├── test_deprecated.yml │ │ ├── test_included.yml │ │ └── test_missing.yml ├── correlation-unit-tests │ ├── fails │ │ └── fails1.yml │ └── passes │ │ └── pass1.yml ├── custom-schemas │ ├── invalid │ │ ├── schema-1.yml │ │ └── schema-2.yaml │ └── valid │ │ ├── lookup-table-schema-1.yml │ │ ├── schema-1.yml │ │ ├── schema-2.yaml │ │ ├── schema-3.yml │ │ └── schema_1_tests.yml ├── derived_without_base │ └── derived.yml ├── detections │ ├── .panther_settings.yml │ ├── aws_globals.py │ ├── aws_globals.yml │ ├── destinations │ │ ├── example_available_destination_name.py │ │ └── example_available_destination_name.yml │ ├── disabled_rule │ │ ├── example_disabled_rule.py │ │ ├── example_disabled_rule.yml │ │ ├── example_rule.py │ │ └── example_rule.yml │ ├── example_data_model_conflict.yml │ ├── example_ignored.yml │ ├── example_ignored_multi.yml │ ├── example_invalid_pack.yml │ ├── example_malformed_policy.yml │ ├── example_malformed_yaml.yml │ ├── example_policy.json │ ├── example_policy.py │ ├── example_policy.yml │ ├── example_policy_bad_resource_type.py │ ├── example_policy_bad_resource_type.yml │ ├── example_policy_import.py │ ├── example_policy_import.yml │ ├── example_policy_invalid_characters.py │ ├── example_policy_invalid_characters.yml │ ├── example_policy_missing_policy_file.yml │ ├── example_policy_required_tests.py │ ├── example_policy_required_tests.yml │ ├── example_policy_set_duplicates.py │ ├── example_policy_set_duplicates.yml │ ├── example_rule.py │ ├── example_rule_bad_log_type.py │ ├── example_rule_bad_log_type.yml │ ├── example_rule_invalid_mocks.py │ ├── example_rule_invalid_mocks.yml │ ├── example_rule_invalid_test.py │ ├── example_rule_invalid_test.yml │ ├── example_rule_missing_field.yml │ ├── example_rule_set_duplicates.py │ ├── example_rule_set_duplicates.yml │ ├── example_strict_invalid_yaml.yml │ ├── example_unhandled_exception.py │ ├── example_unhandled_exception.yml │ ├── example_unhandled_exception_on_import.py │ └── valid_analysis │ │ ├── advanced_rules │ │ ├── example_rule_data_model.py │ │ └── example_rule_data_model.yml │ │ ├── data_models │ │ ├── GSuite.Events.DataModel.py │ │ ├── example_data_model.yml │ │ ├── example_data_model_disabled.yml │ │ └── example_data_model_python.yml │ │ ├── global_helpers │ │ ├── a_helper.py │ │ ├── a_helper.yml │ │ ├── b_helper.py │ │ ├── b_helper.yml │ │ ├── helpers.py │ │ └── helpers.yml │ │ ├── packs │ │ └── sample-pack.yml │ │ ├── policies │ │ ├── example_policy.py │ │ ├── example_policy.yml │ │ ├── example_policy_beta.py │ │ ├── example_policy_beta.yml │ │ ├── example_policy_extraneous_fields.py │ │ ├── example_policy_extraneous_fields.yml │ │ ├── example_policy_generated_functions.py │ │ └── example_policy_generated_functions.yml │ │ ├── queries │ │ ├── query_one.yml │ │ ├── query_three.yml │ │ └── query_two.yml │ │ ├── rules │ │ ├── example_rule.py │ │ ├── example_rule.yml │ │ ├── example_rule_extraneous_fields.py │ │ ├── example_rule_extraneous_fields.yml │ │ ├── example_rule_generated_functions.py │ │ ├── example_rule_generated_functions.yml │ │ ├── example_rule_global.py │ │ ├── example_rule_global.yml │ │ ├── example_rule_mocks.py │ │ └── example_rule_mocks.yml │ │ └── scheduled_rules │ │ ├── example_scheduled_rule.py │ │ └── example_scheduled_rule.yml ├── inline-filters │ ├── basic.python.rule.py │ ├── basic.python.rule.with.filters.py │ ├── basic.python.rule.with.filters.yml │ ├── basic.python.rule.yml │ ├── basic.python.scheduled_rule.py │ ├── basic.python.scheduled_rule.yml │ ├── basic.rule.with.filters.yml │ ├── basic.rule.yml │ └── basic.scheduled_rule.yml ├── lookup-tables │ ├── invalid │ │ └── lookup-table-1.yml │ └── valid │ │ ├── lookup-table-1.yml │ │ ├── lookup-table-2.yml │ │ └── sample_aws_accounts.csv ├── queries │ ├── invalid │ │ ├── example-scheduled-query-invalid-tablename-1.yml │ │ ├── example-scheduled-query-invalid-tablename-2.yml │ │ ├── example-scheduled-query-invalid-tablename-3.yml │ │ └── example-scheduled-query-invalid-tablename-4.yml │ └── valid │ │ ├── example-scheduled-query-cron.yml │ │ └── example-scheduled-query-rateminutes.yml ├── simple-detections │ ├── invalid │ │ ├── invalid_Test.MultiMatch.Key.yml │ │ ├── invalid_asana_team_privacy.yml │ │ └── invalid_gcp_gcs_public.yml │ └── valid │ │ ├── AWS.EC2.Traffic.Mirroring.yml │ │ ├── AWS.IAMUser.ReconAccessDenied.yml │ │ ├── AWS.Modify.Cloud.Compute.Infrastructure.yml │ │ ├── Amazon.EKS.Audit.Multiple403.yml │ │ ├── Amazon.EKS.Audit.SystemNamespaceFromPublicIP.yml │ │ ├── GitHub.Team.Modified.yml │ │ ├── Test.AbsoluteCondition.yml │ │ ├── Test.Combinators.yml │ │ ├── Test.Extra.Top.Level.Keys.yml │ │ ├── Test.ListComprehension.yml │ │ ├── Test.MultiMatch.Key.yml │ │ ├── Test.Numeric.Comparison.yml │ │ ├── asana_service_account_created.yml │ │ ├── asana_team_privacy_public.yml │ │ ├── asana_workspace_new_admin.yml │ │ ├── asana_workspace_saml_optional.yml │ │ ├── auth0_mfa_policy_disabled.yml │ │ ├── auth0_mfa_risk_assessment_disabled.yml │ │ ├── aws_authentication_from_crowdstrike_unmanaged_device.yml │ │ ├── aws_cloudtrail_account_discovery.yml │ │ ├── aws_cloudtrail_created.yml │ │ ├── aws_cloudtrail_unsuccessful_mfa_attempt.yml │ │ ├── aws_console_login_without_saml.yml │ │ ├── aws_ec2_monitoring.yml │ │ ├── aws_ec2_startup_script_change.yml │ │ ├── aws_guardduty_high_sev_findings.yml │ │ ├── aws_guardduty_low_sev_findings.yml │ │ ├── aws_s3_unauthenticated_access.yml │ │ ├── aws_vpc_inbound_traffic_port_allowlist.yml │ │ ├── aws_vpc_unapproved_outbound_dns.yml │ │ ├── box_user_downloads.yml │ │ ├── dropbox_linked_team_application_added.yml │ │ ├── duo_user_endpoint_failure_multi.yml │ │ ├── gcp_access_attempts_violating_vpc_service_controls.yml │ │ ├── gcp_gcs_public.yml │ │ ├── gcp_iam_org_folder_changes.yml │ │ ├── gcp_logging_settings_modified.yml │ │ ├── gcp_vpc_flow_logs_disabled.yml │ │ ├── google_workspace_apps_marketplace_allowlist.yml │ │ ├── gsuite_leaked_password.yml │ │ ├── gsuite_workspace_calendar_external_sharing.yml │ │ ├── okta_group_admin_role_assigned.yml │ │ ├── onelogin_high_risk_failed_login.yml │ │ ├── onelogin_password_accessed.yml │ │ ├── onelogin_user_account_locked.yml │ │ ├── snowflake_login_without_mfa.yml │ │ ├── teleport_scheduled_jobs.yml │ │ ├── test.enrichment.rule.yml │ │ ├── test.rule.with.dynamic.funcs.yml │ │ └── vpc_dns_tunneling.yml └── tests_can_be_inherited │ ├── base.py │ ├── base.yml │ └── derive.yml ├── unit ├── __init__.py └── panther_analysis_tool │ ├── __init__.py │ ├── backend │ ├── __init__.py │ └── test_lambda_client.py │ ├── command │ ├── __init__.py │ ├── test_benchmark.py │ └── test_bulk_delete.py │ ├── log_schemas │ ├── __init__.py │ └── test_user_defined.py │ ├── test_analysis_utils.py │ ├── test_check_packs.py │ ├── test_enriched_event.py │ ├── test_enriched_event_generator.py │ ├── test_exceptions.py │ ├── test_immutable.py │ ├── test_lookup_tables.py │ ├── test_main.py │ ├── test_schemas.py │ ├── test_testing.py │ ├── test_util.py │ ├── test_validation.py │ └── test_zip_chunker.py └── utils ├── __init__.py └── get_specs_for_test.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | 6 | * @panther-labs/threat-research 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### Steps to reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ### Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ### Environment 27 | 28 | How are you deploying or using Panther? 29 | 30 | - Panther version or commit: [e.g. v0.1.1] 31 | - OS: [e.g. MacOS 10.15.3] 32 | - Browser: [e.g. Chrome 80.0.3987.87, Safari Version 13.0.5 (15608.5.11)] 33 | 34 | ### Additional context 35 | 36 | Add any other context about the problem here. 37 | 38 | ### Screenshots 39 | 40 | If applicable, add screenshots to help explain your problem. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or other enhancement 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the ideal solution 10 | 11 | A clear and concise description of what you want to happen. 12 | 13 | ### Describe your use cases 14 | 15 | In order to properly evaluate a feature request, it is necessary to understand the use-cases for it. 16 | 17 | ### How frequently would you use such a feature 18 | 19 | ie. Daily, weekly, monthly 20 | 21 | ### Describe alternatives you have considered 22 | 23 | A clear and concise description of any alternative solutions or features you have considered. 24 | 25 | ### References 26 | 27 | Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above or to the suggested solution? If so, please create a list below that mentions each of them. 28 | 29 | ## - 30 | 31 | ### Additional context 32 | 33 | Add any other context or screenshots about the feature request here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Story 3 | about: Planned or ongoing development work 4 | title: '' 5 | labels: story 6 | assignees: '' 7 | --- 8 | 9 | ### Description 10 | 11 | Explain what you are trying to achieve and why this story needs to be implemented. What is the value that this will provide? 12 | 13 | ### Related Services 14 | 15 | Which backend services must change for this story to be completed? 16 | 17 | ### Designs 18 | 19 | Paste the link to your designs here 20 | 21 | ### Acceptance Criteria 22 | 23 | A concise list of specific user stories that qualify this story as done. 24 | 25 | This acts as a checklist and high-level context for anyone reading this issue to verify your implementation. 26 | 27 | For example: 28 | 29 | - We can collect anonymized frontend crash logs from user browsers 30 | - Users can opt in to send these logs to panther 31 | - The crash logs will contain the following fields : browser version 32 | - Users can opt-out from collection at any time 33 | - ... 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # Update the GitHub actions used in our CI/CD workflow 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | reviewers: 10 | - "@panther-labs/admin" 11 | assignees: 12 | - "@panther-labs/admin" 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Background 2 | 3 | 4 | 5 | ### Changes 6 | 7 | * 8 | 9 | ### Testing 10 | 11 | * 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | permissions: 4 | contents: read 5 | 6 | jobs: 7 | ci: 8 | if: ${{ github.actor != 'panther-bot-automation' }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 13 | - name: Setup Python 14 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b 15 | with: 16 | python-version: 3.11 17 | - name: Install poetry 18 | run: make install-poetry 19 | - name: Setup Virtual Environment 20 | run: make venv 21 | - name: Install Core Utilities 22 | run: make install 23 | - name: Install Dependencies 24 | run: make deps 25 | - name: Install Panther CLI 26 | run: poetry install 27 | - name: Run CLI Tests 28 | run: make ci 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main # Splitting out main here ensures we do not redundantly run this workflow on merge from a PR 5 | pull_request: 6 | branches: 7 | - "*" # Match all branches 8 | 9 | permissions: 10 | contents: write 11 | id-token: write 12 | 13 | jobs: 14 | fmt: 15 | if: ${{ github.actor != 'panther-bot-automation' }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 20 | - name: Setup Python 21 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b 22 | with: 23 | python-version: 3.11 24 | - name: Install poetry 25 | run: make install-poetry 26 | - name: Install 27 | run: make install 28 | - name: Format 29 | run: make fmt 30 | - name: Import GPG key 31 | uses: crazy-max/ghaction-import-gpg@v6 32 | with: 33 | gpg_private_key: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY }} 34 | passphrase: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY_PASSPHRASE }} 35 | git_user_signingkey: true 36 | git_commit_gpgsign: true 37 | - name: Commit formatting 38 | run: | 39 | git config --global user.name "panther-bot-automation" 40 | git config --global user.email "github-service-account-automation@panther.io" 41 | 42 | if [[ "${{ github.event_name }}" == "pull_request" ]]; then 43 | BRANCH=${{ github.head_ref }} 44 | git fetch; git checkout ${{ github.head_ref }} 45 | else 46 | BRANCH=${{ github.ref }} 47 | fi 48 | 49 | git add -A . 50 | 51 | REQUIRES_COMMIT=1 52 | git commit -S -m "Auto-format files" || REQUIRES_COMMIT=0 53 | 54 | if [[ $REQUIRES_COMMIT -eq 0 ]]; then 55 | echo "No auto-formatting needed" 56 | else 57 | echo "Committing auto-formatted files" 58 | git push origin HEAD:$BRANCH 59 | fi 60 | env: 61 | GH_TOKEN: ${{ secrets.PANTHER_BOT_AUTOMATION_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/test_release_publish.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test, Publish Github and PyPI Releases 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | publish_github_release_and_pypi: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out the repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b 21 | with: 22 | python-version: '3.11' 23 | 24 | - name: Install pip and poetry 25 | run: | 26 | python -m pip install --root-user-action=ignore --upgrade pip 27 | pip install --root-user-action=ignore poetry 28 | make venv 29 | 30 | - name: Build Release tar.gz 31 | run: | 32 | make build 33 | 34 | - name: Install Build and Run PAT Tests 35 | run: | 36 | poetry run pip install --root-user-action=ignore dist/panther_analysis_tool-*.tar.gz 37 | make test integration 38 | 39 | - name: Create Github Release 40 | run: | 41 | export NEW_VERSION=$(poetry version -s) 42 | git config user.name "dac-bot" 43 | git config user.email "dac-bot@panther.com" 44 | gh release create v$NEW_VERSION dist/* -t v$NEW_VERSION --draft 45 | env: 46 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Publish to PyPI 49 | run: | 50 | make release 51 | env: 52 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/version_bump_pr.yml: -------------------------------------------------------------------------------- 1 | name: Version Bump PR 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | bump_type: 7 | description: 'Version Bump Type (major, minor, patch)' 8 | required: true 9 | default: 'minor' 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | version_bump_pr: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check out the repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Setup Python 25 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b 26 | with: 27 | python-version: 3.11 28 | 29 | - name: Install poetry 30 | run: make install-poetry 31 | 32 | - name: Bump version 33 | id: bump_version 34 | run: | 35 | poetry version "${{ github.event.inputs.bump_type }}" 36 | 37 | - name: Import GPG key 38 | uses: crazy-max/ghaction-import-gpg@v6 39 | with: 40 | gpg_private_key: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY }} 41 | passphrase: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY_PASSPHRASE }} 42 | git_user_signingkey: true 43 | git_commit_gpgsign: true 44 | 45 | - name: Create Branch and Pull Request 46 | run: | 47 | NEW_VERSION="$(poetry version -s)" 48 | git config --global user.email "github-service-account-automation@panther.io" 49 | git config --global user.name "panther-bot-automation" 50 | git checkout -b "$NEW_VERSION" 51 | git commit -a -S -m "Bump version to $NEW_VERSION" 52 | git push --set-upstream origin "$NEW_VERSION" 53 | gh pr create -t "Version bump to v$NEW_VERSION" -b "Bumping Version to v$NEW_VERSION ahead of release." 54 | env: 55 | GH_TOKEN: ${{ secrets.PANTHER_BOT_AUTOMATION_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environments 2 | .env 3 | .venv 4 | env/ 5 | venv/ 6 | ENV/ 7 | env.bak/ 8 | venv.bak/ 9 | 10 | # macOS Files 11 | .DS_Store 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | .mypy_cache/ 18 | 19 | # Distribution / packaging 20 | *.zip 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Editor/IDE 65 | .vim/ 66 | .idea 67 | .vscode 68 | 69 | *.orig 70 | .panther_settings.yml 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please follow the [Code of Conduct](https://github.com/panther-labs/panther-analysis/blob/master/CODE_OF_CONDUCT.md) 4 | in all of your interactions with the project. 5 | 6 | Prior to contributing code, you will be required to sign our [Contributor License Agreement](https://cla-assistant.io/panther-labs/panther-analysis). 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure no new install or build artifacts are included in the request. 11 | 2. Update the `README.md` with details of changes to the interface, this includes new environment 12 | variables, exposed ports, useful file locations and container parameters. 13 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 14 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 15 | 4. You may merge the Pull Request in once you have the sign-off of other code owners. If you 16 | do not have permission to do that, you may request a reviewer to merge it for you. 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include panther_analysis_tool/log_schemas/__init__.py 3 | include panther_analysis_tool/log_schemas/user_defined.py 4 | include panther_analysis_tool/backend/graphql/*.graphql 5 | include panther_analysis_tool/detection_schemas/*.json 6 | include VERSION 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | bandit = "==1.7.5" 8 | black = "==23.7.0" 9 | coverage = "==7.3.1" 10 | isort = "==5.12.0" 11 | mypy = "==1.5.1" 12 | nose2 = ">=0.13.0" 13 | pyfakefs = "==5.4.1" 14 | pylint = "==2.17.5" 15 | PyYAML = "==6.0.1" 16 | responses = "*" 17 | twine = "*" 18 | typed-ast = "==1.5.5" 19 | types-python-dateutil = "*" 20 | types-requests = "*" 21 | 22 | [packages] 23 | "ruamel.yaml.clib" = "==0.2.7" 24 | "ruamel.yaml" = "==0.17.32" 25 | boto3 = "==1.28.44" 26 | botocore = "==1.31.44" 27 | certifi = "==2023.7.22" 28 | chardet = "==5.2.0" 29 | contextlib2 = "==21.6.0" 30 | decorator = "==5.1.1" 31 | dill = "==0.3.7" 32 | dynaconf = "==3.2.2" 33 | gql = { extras = ["aiohttp"], version = ">=3.4.1" } 34 | aiohttp = ">=3.9.4" 35 | graphql-core = "==3.2.3" 36 | idna = "==3.4" 37 | jmespath = "==1.0.1" 38 | jsonlines = "==4.0.0" 39 | jsonpath-ng = "==1.5.3" 40 | jsonschema = "4.19.0" 41 | nested-lookup = "==0.2.25" 42 | packaging = "==23.1" 43 | panther-core = "==0.11.2" 44 | ply = "==3.11" 45 | policyuniverse = "==1.5.1.20230817" 46 | python-dateutil = "==2.8.2" 47 | requests = "==2.31.0" 48 | s3transfer = "==0.6.2" 49 | schema = "==0.7.5" 50 | semver = "==2.13.0" 51 | six = "==1.16.0" 52 | sqlfluff = "==2.3.1" 53 | typing-extensions = "==4.7.1" 54 | urllib3 = "==1.26.18" 55 | wrapt = "==1.15.0" 56 | 57 | [requires] 58 | python_version = "3.11" 59 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a security vulnerability, please follow these steps: 6 | 7 | 1. Go to this repository's **Security** tab on GitHub. 8 | 2. Click on **Report a vulnerability**. 9 | 3. Provide a clear description of the vulnerability and its potential impact. Be as detailed as possible. 10 | 4. include steps or a PoC (Proof of Concept) to reproduce the vulnerability if applicable. 11 | 5. Submit the report. 12 | 13 | Once we receive the private report notification, we will promptly investigate and assess the reported vulnerability. 14 | 15 | Please do not disclose any potential vulnerabilities in public repositories, issue trackers, or forums until we have had a chance to review and address the issue. 16 | 17 | ## Scope 18 | 19 | This security policy applies to all the code and files within this repository and its dependencies, which Panther Labs actively maintain. 20 | 21 | If you encounter a security issue in a dependency we do not directly maintain, please follow responsible disclosure practices and report it to the respective project. 22 | 23 | Thank you for being so helpful in making this project more secure. 24 | -------------------------------------------------------------------------------- /bin/panther_analysis_tool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from panther_analysis_tool import main 4 | 5 | main.run() 6 | -------------------------------------------------------------------------------- /bin/pat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from panther_analysis_tool import main 4 | 5 | main.run() 6 | -------------------------------------------------------------------------------- /example_panther_config.yml: -------------------------------------------------------------------------------- 1 | # Example configuration file for panther_analysis_tool. 2 | # Copy this file into the root of your test repository and name it .panther_settings.yml 3 | # Note that options in this file will be overridden by options passed on the command line 4 | # 5 | # The API token to use when making changes to Panther 6 | # api_token: "token" 7 | # 8 | # The URL where Panther is hosted 9 | # api_host: "test.runpanther.xyz" 10 | # 11 | # The AWS profile to use when updating the AWS Panther deployment. 12 | # aws_profile: "aws_profile" 13 | # 14 | # The key id to use to sign the release asset 15 | # kms_key: "kms-key" 16 | # 17 | # The branch to base the release on 18 | # github_branch: "main" 19 | # 20 | # The github owner of the repository 21 | # github_owner: "Bob Ownerson" 22 | # 23 | # The github repository name 24 | # github_repository: "panther-labs/panther-analysis" 25 | # 26 | # The tag name for this release 27 | # github_tag: "v.1.0.0" 28 | # 29 | # Skip tests for disabled rules 30 | # skip_disabled_tests: True 31 | # 32 | # The relative path to Panther policies and rules. 33 | # path: "." 34 | # 35 | # The path to store output files. 36 | # out: "." 37 | # 38 | # The minimum number of tests in order for a detection to be considered passing. 39 | # If a number greater than 1 is specified, at least one True and one False test is required. 40 | # minimum_tests: 0 41 | # 42 | # Relative path to files in this project to be ignored by panther-analysis tool 43 | # ignore_files: 44 | # - "example.yml" 45 | # 46 | # A destination name that may be returned by the destinations function. 47 | # available_destination: "test_destination" 48 | # 49 | # Filter tests by RuleID and other parameters. This must be passed as a dict with lists as values. 50 | # filter: {"RuleID":["Standard.UnusualLogin"]} 51 | # 52 | # Allows skipping of table name validation from schema validation. Useful when querying 53 | # non-Panther or non-Snowflake tables 54 | # ignore-table-names: False 55 | # 56 | # Provide additional fully qualified table names that should be considered valid during schema validation 57 | # (in addition to standard Panther/Snowflake tables). Accepts '*' as wildcard character matching 0 or more characters. 58 | # valid_table_names: 59 | # - "foo.bar.baz" 60 | # - "bar.baz.*" 61 | # - "foo.*bar.baz" 62 | # - "baz.*" 63 | # - "*.foo.*" 64 | -------------------------------------------------------------------------------- /panther_analysis_tool/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | UPLOAD_IN_PROGRESS_SUBSTR = "another upload" 4 | 5 | 6 | def is_retryable_error(err: Optional[Dict[str, Any]]) -> bool: 7 | if err: 8 | if is_retryable_error_str(err.get("message", "")) or is_retryable_error_str( 9 | err.get("body", "") 10 | ): 11 | return True 12 | return False 13 | 14 | 15 | def is_retryable_error_str(err: str) -> bool: 16 | if not err: 17 | return False 18 | 19 | return ( 20 | UPLOAD_IN_PROGRESS_SUBSTR in err 21 | or err == "upload failed" 22 | or "unknown error occurred" in err 23 | or "ddb lock" in err 24 | or "pload does not exist" in err 25 | ) 26 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/async_bulk_upload.graphql: -------------------------------------------------------------------------------- 1 | mutation uploadDetectionEntitiesAsync($input: UploadDetectionEntitiesAsyncInput!) { 2 | uploadDetectionEntitiesAsync(input: $input) { 3 | receiptId 4 | } 5 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/async_bulk_upload_status.graphql: -------------------------------------------------------------------------------- 1 | query status($input: ID!) { 2 | detectionEntitiesUploadStatus(receiptId: $input) { 3 | status 4 | error 5 | result { 6 | dataModels { 7 | ...UploadStatisticDetails 8 | } 9 | globalHelpers { 10 | ...UploadStatisticDetails 11 | } 12 | lookupTables { 13 | ...UploadStatisticDetails 14 | } 15 | policies { 16 | ...UploadStatisticDetails 17 | } 18 | rules { 19 | ...UploadStatisticDetails 20 | } 21 | queries { 22 | ...UploadStatisticDetails 23 | } 24 | correlationRules { 25 | ...UploadStatisticDetails 26 | } 27 | } 28 | } 29 | } 30 | 31 | fragment UploadStatisticDetails on UploadStatistics { 32 | modified 33 | new 34 | total 35 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/bulk_upload.graphql: -------------------------------------------------------------------------------- 1 | mutation UploadDetections($input: UploadDetectionEntitiesInput!) { 2 | uploadDetectionEntities(input: $input) { 3 | dataModels { 4 | ...UploadStatisticDetails 5 | } 6 | globalHelpers { 7 | ...UploadStatisticDetails 8 | } 9 | lookupTables { 10 | ...UploadStatisticDetails 11 | } 12 | policies { 13 | ...UploadStatisticDetails 14 | } 15 | rules { 16 | ...UploadStatisticDetails 17 | } 18 | queries { 19 | ...UploadStatisticDetails 20 | } 21 | correlationRules { 22 | ...UploadStatisticDetails 23 | } 24 | } 25 | } 26 | 27 | fragment UploadStatisticDetails on UploadStatistics { 28 | modified 29 | new 30 | total 31 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/create_or_update_schema.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateOrUpdateSchema($input: CreateOrUpdateSchemaInput!) { 2 | createOrUpdateSchema(input: $input) { 3 | schema { 4 | createdAt 5 | description 6 | isManaged 7 | name 8 | referenceURL 9 | revision 10 | spec 11 | updatedAt 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/create_perf_test.graphql: -------------------------------------------------------------------------------- 1 | mutation CreatePerfTest ($input: CreatePerfTestInput!){ 2 | createPerfTest(input: $input) { 3 | replay { 4 | id 5 | state 6 | createdAt 7 | updatedAt 8 | completedAt 9 | detectionId 10 | replayType 11 | scope { 12 | logTypes 13 | dataWindow { 14 | sizeWindow { 15 | maxSizeInGB 16 | } 17 | timeWindow { 18 | endsAt 19 | startsAt 20 | } 21 | } 22 | } 23 | summary { 24 | totalAlerts 25 | completedAt 26 | ruleErrorCount 27 | ruleMatchCount 28 | evaluationProgress 29 | computationProgress 30 | logDataSizeEstimate 31 | matchesProcessedCount 32 | eventsProcessedCount 33 | eventsMatchedCount 34 | readTimeNanos 35 | processingTimeNanos 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/delete_detections.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteDetections($input: DeleteDetectionsInput!) { 2 | deleteDetections(input: $input) { 3 | ids 4 | savedQueryNames 5 | } 6 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/delete_saved_queries.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteSavedQueries($input: DeleteSavedQueriesByNameInput!) { 2 | deleteSavedQueriesByName(input: $input) { 3 | detectionIDs 4 | names 5 | } 6 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/feature_flags.graphql: -------------------------------------------------------------------------------- 1 | query GetFeatureFlags($input: GetFeatureFlagsInput!) { 2 | featureFlags(input: $input) { 3 | flags { 4 | flag 5 | treatment 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/generate_enriched_event.graphql: -------------------------------------------------------------------------------- 1 | query GenerateEnrichedEvent($input: GenerateEnrichedEventInput!) { 2 | generateEnrichedEvent(input: $input) { 3 | enrichedEvent 4 | } 5 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/get_rule_body.graphql: -------------------------------------------------------------------------------- 1 | query GetRuleBody($input: ID!) { 2 | rulePythonBody(input: $input) { 3 | pythonBody 4 | tests { 5 | expectedResult 6 | name 7 | resource 8 | mocks { 9 | objectName 10 | returnValue 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/get_version.graphql: -------------------------------------------------------------------------------- 1 | query GetVersion { 2 | generalSettings { 3 | pantherVersion 4 | } 5 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/introspection_query.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQuery { 2 | __schema { 3 | queryType { name } 4 | mutationType { name } 5 | subscriptionType { name } 6 | types { 7 | ...FullType 8 | } 9 | directives { 10 | name 11 | description 12 | locations 13 | args { 14 | ...InputValue 15 | } 16 | } 17 | } 18 | } 19 | fragment FullType on __Type { 20 | kind 21 | name 22 | description 23 | fields(includeDeprecated: true) { 24 | name 25 | description 26 | args { 27 | ...InputValue 28 | } 29 | type { 30 | ...TypeRef 31 | } 32 | isDeprecated 33 | deprecationReason 34 | } 35 | inputFields { 36 | ...InputValue 37 | } 38 | interfaces { 39 | ...TypeRef 40 | } 41 | enumValues(includeDeprecated: true) { 42 | name 43 | description 44 | isDeprecated 45 | deprecationReason 46 | } 47 | possibleTypes { 48 | ...TypeRef 49 | } 50 | } 51 | fragment InputValue on __InputValue { 52 | name 53 | description 54 | type { ...TypeRef } 55 | defaultValue 56 | } 57 | fragment TypeRef on __Type { 58 | kind 59 | name 60 | ofType { 61 | kind 62 | name 63 | ofType { 64 | kind 65 | name 66 | ofType { 67 | kind 68 | name 69 | ofType { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | ofType { 76 | kind 77 | name 78 | ofType { 79 | kind 80 | name 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/list_schemas.graphql: -------------------------------------------------------------------------------- 1 | query Schemas($input: SchemasInput!) { 2 | schemas(input: $input) { 3 | edges { 4 | node { 5 | createdAt 6 | description 7 | isManaged 8 | name 9 | referenceURL 10 | revision 11 | spec 12 | updatedAt 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/metrics.graphql: -------------------------------------------------------------------------------- 1 | query Metrics($input: MetricsInput!) { 2 | metrics(input: $input) { 3 | bytesProcessedPerSource { 4 | breakdown 5 | label 6 | value 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/replay.graphql: -------------------------------------------------------------------------------- 1 | query Replay($input: ID!) { 2 | replay(id: $input) { 3 | id 4 | state 5 | createdAt 6 | updatedAt 7 | completedAt 8 | detectionId 9 | replayType 10 | scope { 11 | logTypes 12 | dataWindow { 13 | sizeWindow { 14 | maxSizeInGB 15 | } 16 | timeWindow { 17 | endsAt 18 | startsAt 19 | } 20 | } 21 | } 22 | summary { 23 | totalAlerts 24 | completedAt 25 | ruleErrorCount 26 | ruleMatchCount 27 | evaluationProgress 28 | computationProgress 29 | logDataSizeEstimate 30 | matchesProcessedCount 31 | eventsProcessedCount 32 | eventsMatchedCount 33 | readTimeNanos 34 | processingTimeNanos 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/stop_replay.graphql: -------------------------------------------------------------------------------- 1 | mutation StopReplay($input: StopReplayInput!) { 2 | stopReplay(input: $input) { 3 | replay { 4 | ...ReplayFull 5 | __typename 6 | } 7 | __typename 8 | } 9 | } 10 | 11 | fragment ReplayFull on Replay { 12 | id 13 | state 14 | createdAt 15 | completedAt 16 | detectionId 17 | scope { 18 | dataWindow { 19 | timeWindow { 20 | startsAt 21 | endsAt 22 | __typename 23 | } 24 | sizeWindow { 25 | maxSizeInGB 26 | __typename 27 | } 28 | __typename 29 | } 30 | logTypes 31 | __typename 32 | } 33 | summary { 34 | completedAt 35 | totalAlerts 36 | ruleMatchCount 37 | ruleErrorCount 38 | evaluationProgress 39 | logDataSizeEstimate 40 | computationProgress 41 | matchesProcessedCount 42 | eventsProcessedCount 43 | eventsMatchedCount 44 | __typename 45 | } 46 | __typename 47 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/test_correlation_rule.graphql: -------------------------------------------------------------------------------- 1 | mutation TestCorrelationRule($input: TestCorrelationRuleYAMLInput!) { 2 | testCorrelationRuleYAML(input: $input) { 3 | results { 4 | error 5 | name 6 | passed 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/transpile_filters.graphql: -------------------------------------------------------------------------------- 1 | mutation TranspileFilters($input: TranspileFiltersInput!) { 2 | transpileFilters(input: $input) { 3 | transpiledFilters 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/transpile_sdl.graphql: -------------------------------------------------------------------------------- 1 | mutation TranspileSimpleDetectionsToPython($input: TranspileSimpleDetectionsToPythonInput!) { 2 | transpileSimpleDetectionsToPython(input: $input) { 3 | transpiledPython 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/validate_bulk_upload.graphql: -------------------------------------------------------------------------------- 1 | mutation ValidateBulkUpload($input: ValidateBulkUploadInput!) { 2 | validateBulkUpload(input: $input) { 3 | receiptId 4 | } 5 | } -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/validate_bulk_upload_status.graphql: -------------------------------------------------------------------------------- 1 | query ValidateBulkUploadStatus($input: ID!) { 2 | validateBulkUploadStatus(receiptId: $input) { 3 | status 4 | error 5 | result { 6 | issues { 7 | path 8 | errorMessage 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /panther_analysis_tool/cli_output.py: -------------------------------------------------------------------------------- 1 | from panther_analysis_tool.backend.client import BackendMultipartError 2 | 3 | 4 | class BColors: 5 | HEADER = "\033[95m" 6 | OKBLUE = "\033[94m" 7 | OKCYAN = "\033[96m" 8 | OKGREEN = "\033[92m" 9 | WARNING = "\033[93m" 10 | FAIL = "\033[91m" 11 | ENDC = "\033[0m" 12 | BOLD = "\033[1m" 13 | UNDERLINE = "\033[4m" 14 | 15 | @classmethod 16 | def wrap(cls, start: str, text: str) -> str: 17 | return f"{start}{text}{cls.ENDC}" 18 | 19 | 20 | def bold(text: str) -> str: 21 | return BColors.wrap(BColors.BOLD, text) 22 | 23 | 24 | def header(text: str) -> str: 25 | return BColors.wrap(BColors.HEADER, text) 26 | 27 | 28 | def blue(text: str) -> str: 29 | return BColors.wrap(BColors.OKBLUE, text) 30 | 31 | 32 | def cyan(text: str) -> str: 33 | return BColors.wrap(BColors.OKCYAN, text) 34 | 35 | 36 | def success(text: str) -> str: 37 | return BColors.wrap(BColors.OKGREEN, text) 38 | 39 | 40 | def warning(text: str) -> str: 41 | return BColors.wrap(BColors.WARNING, text) 42 | 43 | 44 | def underline(text: str) -> str: 45 | return BColors.wrap(BColors.UNDERLINE, text) 46 | 47 | 48 | def failed(text: str) -> str: 49 | return BColors.wrap(BColors.FAIL, text) 50 | 51 | 52 | def multipart_error_msg(result: BackendMultipartError, msg: str) -> str: 53 | return_str = "\n-----\n" 54 | 55 | if result.has_error(): 56 | return_str += f"{bold('Error')}: {result.get_error()}\n-----\n" 57 | 58 | for issue in result.get_issues(): 59 | if issue.path and issue.path != "": 60 | return_str += f"{bold('Path')}: {issue.path}\n" 61 | 62 | if issue.error_message and issue.error_message != "": 63 | return_str += f"{bold('Error')}: {issue.error_message}\n" 64 | 65 | return_str += "-----\n" 66 | 67 | return f"{return_str}\n{failed(msg)}" 68 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/check_connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import argparse 21 | import logging 22 | from typing import Tuple 23 | 24 | from panther_analysis_tool.backend.client import Client as BackendClient 25 | 26 | 27 | def run(backend: BackendClient, args: argparse.Namespace) -> Tuple[int, str]: 28 | logging.info("checking connection to %s...", args.api_host) 29 | result = backend.check() 30 | 31 | if not result.success: 32 | logging.info("connection failed") 33 | return 1, result.message 34 | 35 | logging.info("connection successful: %s", result.message) 36 | return 0, "" 37 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/standard_args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | API_DOCUMENTATION = "https://docs.panther.com/api-beta" 4 | 5 | 6 | def for_public_api(parser: argparse.ArgumentParser, required: bool) -> None: 7 | parser.add_argument( 8 | "--api-token", 9 | type=str, 10 | help="The Panther API token to use. See: " + API_DOCUMENTATION, 11 | required=required, 12 | ) 13 | 14 | parser.add_argument( 15 | "--api-host", 16 | type=str, 17 | help="The Panther API host to use. See: " + API_DOCUMENTATION, 18 | required=required, 19 | ) 20 | 21 | 22 | def using_aws_profile(parser: argparse.ArgumentParser) -> None: 23 | parser.add_argument( 24 | "--aws-profile", 25 | type=str, 26 | help="The AWS profile to use when updating the AWS Panther deployment.", 27 | required=False, 28 | ) 29 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/validate.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import io 3 | import logging 4 | import zipfile 5 | from typing import Tuple 6 | 7 | from panther_analysis_tool import cli_output 8 | from panther_analysis_tool.backend.client import ( 9 | BulkUploadParams, 10 | BulkUploadValidateStatusResponse, 11 | ) 12 | from panther_analysis_tool.backend.client import Client as BackendClient 13 | from panther_analysis_tool.backend.client import UnsupportedEndpointError 14 | from panther_analysis_tool.zip_chunker import ZipArgs, analysis_chunks 15 | 16 | 17 | def run(backend: BackendClient, args: argparse.Namespace) -> Tuple[int, str]: 18 | if backend is None or not backend.supports_bulk_validate(): 19 | return 1, "Invalid backend. `validate` is only supported via API token" 20 | 21 | typed_args = ZipArgs.from_args(args) 22 | chunks = analysis_chunks(typed_args) 23 | buffer = io.BytesIO() 24 | 25 | with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_out: 26 | for name in chunks[0].files: 27 | zip_out.write(name) 28 | 29 | buffer.seek(0, 0) 30 | params = BulkUploadParams(zip_bytes=buffer.read()) 31 | 32 | try: 33 | result = backend.bulk_validate(params) 34 | if result.is_valid(): 35 | return 0, f"{cli_output.success('Validation success')}" 36 | 37 | return 1, cli_output.multipart_error_msg(result, "Validation failed") 38 | except UnsupportedEndpointError as err: 39 | logging.debug(err) 40 | return 1, cli_output.warning("Your Panther instance does not support this feature") 41 | 42 | except BaseException as err: # pylint: disable=broad-except 43 | return 1, cli_output.multipart_error_msg( 44 | BulkUploadValidateStatusResponse.from_json({"error": str(err)}), "Validation failed" 45 | ) 46 | -------------------------------------------------------------------------------- /panther_analysis_tool/destination.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class FakeDestination: 6 | """Stub class as a replacement for the Destination class 7 | that wraps alert output metadata.""" 8 | 9 | destination_id: str 10 | destination_display_name: str 11 | -------------------------------------------------------------------------------- /panther_analysis_tool/detection_schemas/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | -------------------------------------------------------------------------------- /panther_analysis_tool/directory.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import os 3 | import shutil 4 | import signal 5 | import tempfile 6 | from typing import Any 7 | from uuid import uuid4 8 | 9 | 10 | def setup_temp() -> None: 11 | """ 12 | Creates a dedicated temporary directory for this process and cleans it up at exit 13 | """ 14 | temp_dir = os.path.join(tempfile.gettempdir(), f"tmp-PAT-{uuid4()}") 15 | os.mkdir(temp_dir) 16 | tempfile.tempdir = temp_dir 17 | 18 | def clean_me_up(*_: Any) -> None: 19 | try: 20 | shutil.rmtree(temp_dir) 21 | finally: 22 | pass 23 | 24 | atexit.register(clean_me_up) 25 | signal.signal(signal.SIGINT, clean_me_up) 26 | signal.signal(signal.SIGTERM, clean_me_up) 27 | -------------------------------------------------------------------------------- /panther_analysis_tool/log_schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/panther_analysis_tool/log_schemas/__init__.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/advanced_rules/example_rule_data_model.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | # filter events on unified data model field 3 | if event.udm("event_type") and event.udm("event_type") == "login_failure": 4 | return True 5 | # filter based on standard log type's fields 6 | if event.get("event_type_id", 0) == 6: 7 | return True 8 | # unknown event type 9 | return False 10 | 11 | 12 | def title(event): 13 | # use unified data model field in title 14 | return "User [{}] from IP [{}] has exceeded the failed logins threshold".format( 15 | event.udm("user"), event.udm("source_ip") 16 | ) 17 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/advanced_rules/example_rule_data_model.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_data_model.py 3 | RuleID: DataModel.BruteForceByIP 4 | DisplayName: Brute Force By IP 5 | Enabled: true 6 | LogTypes: 7 | - OneLogin.Events 8 | - GSuite.Reports 9 | Tags: 10 | - OneLogin 11 | - GSuite 12 | Severity: Medium 13 | Reports: 14 | MITRE ATT&CK: 15 | - Brute Force:Password Spraying 16 | Description: A single ip address was denied access to OneLogin more times than the configured threshold. 17 | Threshold: 10 18 | DedupPeriodMinutes: 10 19 | Reference: https://developers.onelogin.com/api-docs/1/events/event-resource 20 | Runbook: Analyze the IP they came from, and other actions taken before/after. Check if a user from this ip eventually authenticated successfully. 21 | SummaryAttributes: 22 | - p_any_ip_addresses 23 | Tests: 24 | - 25 | Name: Normal OneLogin Login Event 26 | ExpectedResult: false 27 | Log: 28 | { 29 | 'event_type_id': 8, 30 | 'actor_user_id': 123456, 31 | 'actor_user_name': 'Bob Cat', 32 | 'user_id': 123456, 33 | 'user_name': 'Bob Cat', 34 | 'ipaddr': '1.2.3.4', 35 | 'p_log_type': 'OneLogin.Events' 36 | } 37 | - 38 | Name: Failed OneLogin Login Event 39 | ExpectedResult: true 40 | Log: 41 | { 42 | 'event_type_id': 6, 43 | 'actor_user_id': 123456, 44 | 'actor_user_name': 'Bob Cat', 45 | 'user_id': 123456, 46 | 'user_name': 'Bob Cat', 47 | 'ipaddr': '1.2.3.4', 48 | 'p_log_type': 'OneLogin.Events' 49 | } 50 | - 51 | Name: GSuite Normal Login Event 52 | ExpectedResult: false 53 | Log: 54 | { 55 | 'id': {'applicationName': 'login'}, 56 | 'ipAddress': '4.3.2.1', 57 | 'events': [ 58 | { 59 | 'type': 'login', 60 | 'name': 'login_success' 61 | } 62 | ], 63 | 'p_log_type': 'GSuite.Reports' 64 | } 65 | - 66 | Name: GSuite Failed Login Event 67 | ExpectedResult: true 68 | Log: 69 | { 70 | 'actor' : { 71 | 'email': 'bob@example.com' 72 | }, 73 | 'id': {'applicationName': 'login'}, 74 | 'ipAddress': '4.3.2.1', 75 | 'events': [ 76 | { 77 | 'type': 'login', 78 | 'name': 'login_failure' 79 | } 80 | ], 81 | 'p_log_type': 'GSuite.Reports' 82 | } 83 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/correlation_rules/aws_cloudtrail_iaas.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.CloudTrail.IaaS' 3 | DisplayName: 'AWS CloudTrail IaaS' 4 | Enabled: true 5 | LogTypes: 6 | - AWS.CloudTrail 7 | Severity: Info 8 | CreateAlert: false 9 | Detection: 10 | - KeyPath: userIdentity.arn 11 | Condition: IsIn 12 | Values: 13 | - DeploymentUpdateGitHubRole 14 | - KeyPath: eventName 15 | Condition: IsIn 16 | Values: 17 | - StartSession 18 | - ListResources 19 | - UpdateResource 20 | - DescribeResource 21 | - WriteLog -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/correlation_rules/discovering_exfiltrated_credentials.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: correlation_rule 2 | RuleID: 'Discovering.Exfiltrated.Credentials' 3 | DisplayName: 'Discovering.Exfiltrated.Credentials' 4 | Enabled: true 5 | Severity: High 6 | Description: > 7 | There was at least one IaaS activity match not followed 8 | by a CI/CD activity within 10 minutes. 9 | Detection: 10 | - Sequence: 11 | - ID: IaaS Activity 12 | RuleID: AWS.CloudTrail.IaaS 13 | - ID: CICD Activity 14 | RuleID: GitHub.CICD 15 | Absence: true 16 | Transitions: 17 | - From: IaaS Activity 18 | To: CICD Activity 19 | WithinTimeFrameMinutes: 10 20 | Schedule: 21 | RateMinutes: 10 22 | TimeoutMinutes: 3 23 | Tests: 24 | - Name: IaaS Activity without CICD Activity 25 | ExpectedResult: true 26 | RuleOutputs: 27 | - ID: IaaS Activity 28 | Matches: 29 | username: 30 | my_username: [1] 31 | - Name: IaaS Activity with CICD Activity 32 | ExpectedResult: false 33 | RuleOutputs: 34 | - ID: IaaS Activity 35 | Matches: 36 | username: 37 | my_username: [1] 38 | - ID: CICD Activity 39 | Matches: 40 | username: 41 | my_username: [2] -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/correlation_rules/github_cicd.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'GitHub.CICD' 3 | DisplayName: 'GitHub CICD' 4 | Enabled: true 5 | LogTypes: 6 | - GitHub.Audit 7 | Severity: Info 8 | CreateAlert: false 9 | Detection: 10 | - KeyPath: repository 11 | Condition: Equals 12 | Value: panther-labs/example-repo 13 | - KeyPath: action 14 | Condition: Equals 15 | Value: workflows.created_workflow_run 16 | - KeyPath: name 17 | Condition: Equals 18 | Value: CI -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/data_models/aws_cloudtrail_data_model.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from panther import deep_get, event_type 4 | 5 | 6 | def get_event_type(event): 7 | # currently, only tracking a few event types 8 | if ( 9 | event.get("eventName") == "ConsoleLogin" 10 | and deep_get(event, "userIdentity", "type") == "IAMUser" 11 | ): 12 | if deep_get(event, "responseElements", "ConsoleLogin") == "Failure": 13 | return event_type.FAILED_LOGIN 14 | if deep_get(event, "responseElements", "ConsoleLogin") == "Success": 15 | return event_type.SUCCESSFUL_LOGIN 16 | if event.get("eventName") == "CreateUser": 17 | return event_type.USER_ACCOUNT_CREATED 18 | if event.get("eventName") == "CreateAccountResult": 19 | return event_type.ACCOUNT_CREATED 20 | return None 21 | 22 | 23 | def load_ip_address(event): 24 | """ 25 | CloudTrail occasionally sets non-IPs in the sourceIPAddress field. 26 | This method ensures that either an IPv4 or IPv6 address is always returned. 27 | """ 28 | source_ip = event.get("sourceIPAddress") 29 | if not source_ip: 30 | return None 31 | try: 32 | ipaddress.IPv4Address(source_ip) 33 | except ipaddress.AddressValueError: 34 | try: 35 | ipaddress.IPv6Address(source_ip) 36 | except ipaddress.AddressValueError: 37 | return None 38 | return source_ip 39 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/data_models/aws_cloudtrail_data_model.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: datamodel 2 | LogTypes: 3 | - AWS.CloudTrail 4 | DataModelID: "Standard.AWS.CloudTrail" 5 | DisplayName: "AWS CloudTrail" 6 | Filename: aws_cloudtrail_data_model.py 7 | Enabled: true 8 | Mappings: 9 | - Name: actor_user 10 | Path: $.userIdentity..userName 11 | - Name: event_type 12 | Method: get_event_type 13 | - Name: source_ip 14 | Method: load_ip_address 15 | - Name: user_agent 16 | Path: userAgent 17 | - Name: user 18 | Path: $.responseElements.user.userName 19 | - Name: user_account_id 20 | Path: $.responseElements.user.userId -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/a_helper.py: -------------------------------------------------------------------------------- 1 | from b_helper import b_says_hello 2 | 3 | 4 | def a_says_hello(): 5 | return f"{b_says_hello()} before a says hello" 6 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/a_helper.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: a_helper 3 | Filename: a_helper.py 4 | Description: > 5 | Used to test global importing other globals 6 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/b_helper.py: -------------------------------------------------------------------------------- 1 | def b_says_hello(): 2 | return "hello from b" 3 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/b_helper.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: b_helper 3 | Filename: b_helper.py 4 | Description: > 5 | Used to test global importing other globals 6 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/helpers.py: -------------------------------------------------------------------------------- 1 | def test_helper(): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/helpers.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: panther 3 | Filename: helpers.py 4 | Tags: 5 | - AWS 6 | Description: > 7 | Used to define global helpers and variables. 8 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/packs/missing_datamodel.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | Description: This pack groups amazing rules and policies 3 | DisplayName: Sample Pack Data 4 | PackID: Missing.DataModel 5 | PackDefinition: 6 | IDs: 7 | - AWS.IAM.MFAEnabled 8 | - AWS.IAM.BetaTest 9 | - AWS.CloudTrail.MFAEnabled 10 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/packs/missing_global.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | Description: This pack groups amazing rules and policies 3 | DisplayName: Sample Pack Data 4 | PackID: Missing.Global 5 | PackDefinition: 6 | IDs: 7 | - Test.Global.Global # This rule imports a_helper 8 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/packs/missing_query.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | Description: This pack groups amazing rules and policies 3 | DisplayName: Sample Pack Data 4 | PackID: Missing.Query 5 | PackDefinition: 6 | IDs: 7 | - AWS.CloudTrail.Created.Scheduled # Scheduled rule, but no query 8 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/packs/missing_subrules.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | Description: This pack groups amazing rules and policies 3 | DisplayName: Sample Pack Data 4 | PackID: Missing.SubRules 5 | PackDefinition: 6 | IDs: 7 | - Discovering.Exfiltrated.Credentials # Correlation rule included without subrules 8 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy.py 3 | DisplayName: "[MFA Is Enabled For User]" 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: Critical 6 | PolicyID: AWS.IAM.MFAEnabled 7 | OutputIds: 8 | - 00000-01-00000 9 | Enabled: true 10 | ResourceTypes: 11 | - AWS.IAM.RootUser 12 | - AWS.IAM.User 13 | Tags: 14 | - AWS Managed Rules - Security, Identity & Compliance 15 | - AWS 16 | - CIS 17 | - SOC2 18 | Reports: 19 | CIS: 20 | - 1.1 21 | MITRE: 22 | - Extraction:Data Parsing 23 | Runbook: > 24 | Find out who disabled MFA on the account. 25 | Reference: https://www.link-to-info.io 26 | Tests: 27 | - 28 | Name: Root MFA not enabled fails compliance 29 | ExpectedResult: false 30 | Resource: 31 | Arn: arn:aws:iam::123456789012:user/root 32 | CreateDate: 2019-01-01T00:00:00Z 33 | CredentialReport: 34 | MfaActive: false 35 | PasswordEnabled: true 36 | UserName: root 37 | - 38 | Name: User MFA not enabled fails compliance 39 | ExpectedResult: false 40 | Resource: 41 | { 42 | "Arn": "arn:aws:iam::123456789012:user/test", 43 | "CreateDate": "2019-01-01T00:00:00", 44 | "CredentialReport": { 45 | "MfaActive": false, 46 | "PasswordEnabled": true 47 | }, 48 | "UserName": "test" 49 | } 50 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_beta.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return False 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_beta.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_beta.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: Another valid policy. 5 | Severity: High 6 | PolicyID: AWS.IAM.BetaTest 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | ExpectedResult: false 22 | Resource: 23 | Arn: arn:aws:iam::123456789012:user/root 24 | CreateDate: 2019-01-01T00:00:00Z 25 | CredentialReport: 26 | MfaActive: false 27 | PasswordEnabled: true 28 | UserName: root 29 | - 30 | Name: User MFA not enabled fails compliance 31 | ExpectedResult: false 32 | Resource: 33 | { 34 | "Arn": "arn:aws:iam::123456789012:user/test", 35 | "CreateDate": "2019-01-01T00:00:00", 36 | "CredentialReport": { 37 | "MfaActive": false, 38 | "PasswordEnabled": true 39 | }, 40 | "UserName": "test" 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_extraneous_fields.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_extraneous_fields.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_extraneous_fields.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: IAM.MFAEnabled Extra Fields 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | ResourceType: AWS.IAM.User.Snapshot (extraneous field) 27 | Resource: 28 | Arn: arn:aws:iam::123456789012:user/root 29 | CreateDate: 2019-01-01T00:00:00Z 30 | CredentialReport: 31 | MfaActive: false 32 | PasswordEnabled: true 33 | UserName: root 34 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_generated_functions.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return False 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | 14 | 15 | def title(resource): 16 | return "THIS IS AN EXAMPLE TITLE" 17 | 18 | 19 | def alert_context(resource): 20 | return {"ip": "1.1.1.1"} 21 | 22 | 23 | def description(resource): 24 | return "THIS IS AN EXAMPLE DESCRIPTION." 25 | 26 | 27 | def destinations(resource): 28 | return ["ExampleDestinationName"] 29 | 30 | 31 | def runbook(resource): 32 | return "THIS IS AN EXAMPLE RUNBOOK VALUE." 33 | 34 | 35 | def reference(resource): 36 | return "THIS IS AN EXAMPLE REFERENCE." 37 | 38 | 39 | def severity(resource): 40 | return "CrItIcAl" 41 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/policies/example_policy_generated_functions.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_generated_functions.py 3 | DisplayName: MFA Is Enabled For User Generated Fields 4 | Description: Another valid policy. 5 | Severity: High 6 | PolicyID: AWS.IAM.MFAEnabledGenerated 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | ExpectedResult: false 22 | Resource: 23 | Arn: arn:aws:iam::123456789012:user/root 24 | CreateDate: 2019-01-01T00:00:00Z 25 | CredentialReport: 26 | MfaActive: false 27 | PasswordEnabled: true 28 | UserName: root 29 | - 30 | Name: User MFA not enabled fails compliance 31 | ExpectedResult: false 32 | Resource: 33 | { 34 | "Arn": "arn:aws:iam::123456789012:user/test", 35 | "CreateDate": "2019-01-01T00:00:00", 36 | "CredentialReport": { 37 | "MfaActive": false, 38 | "PasswordEnabled": true 39 | }, 40 | "UserName": "test" 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/queries/query_one.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: A Test Query 3 | Enabled: true 4 | Query: 'SELECT * FROM snowflake.account_usage.login_history LIMIT 10' 5 | Description: Some meme query 6 | Tags: 7 | - some 8 | - meme 9 | Schedule: 10 | CronExpression: '0 8 * * *' 11 | TimeoutMinutes: 1 12 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/queries/query_three.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: saved_query 2 | QueryName: A Third Test Query 3 | AthenaQuery: 'SELECT * FROM panther_logs.aws_cloudtrail LIMIT 10' 4 | SnowflakeQuery: 'SELECT * FROM panther_logs.public.aws_cloudtrail LIMIT 10' 5 | Description: Some saved (not scheduled) query 6 | Tags: 7 | - some 8 | - meme 9 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/queries/query_two.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: Another Test Query 3 | Enabled: true 4 | Query: > 5 | SELECT * 6 | FROM panther_logs.public.s3_access 7 | WHERE eventName='abc123' 8 | LIMIT 10 9 | Description: Some meme query 10 | Tags: 11 | - some 12 | - meme 13 | Schedule: 14 | RateMinutes: 10 15 | TimeoutMinutes: 10 16 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule.py: -------------------------------------------------------------------------------- 1 | from panther import test_helper # pylint: disable=import-error 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def rule(event): 7 | if event["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | if "CredentialReport" not in event: 11 | return False 12 | 13 | cred_report = event.get("CredentialReport", {}) 14 | if not cred_report: 15 | return True 16 | 17 | return ( 18 | test_helper() 19 | and cred_report.get("PasswordEnabled", False) 20 | and cred_report.get("MfaActive", False) 21 | ) 22 | 23 | 24 | def dedup(event): 25 | return event["UserName"] 26 | 27 | 28 | def title(event): 29 | return "{} does not have MFA enabled".format(event["UserName"]) 30 | 31 | 32 | def alert_context(event): 33 | # test/validate that we can return the event as alert_context 34 | return event 35 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabled 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Root MFA not enabled fails compliance 25 | ExpectedResult: false 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled fails compliance 35 | ExpectedResult: false 36 | Log: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": 2019-01-01, 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled passes compliance. 48 | ExpectedResult: true 49 | Log: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": 2019-01-01, 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_extraneous_fields.py: -------------------------------------------------------------------------------- 1 | from panther import test_helper # pylint: disable=import-error 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def rule(event): 7 | if event["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = event.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return ( 15 | test_helper() 16 | and cred_report.get("PasswordEnabled", False) 17 | and cred_report.get("MfaActive", False) 18 | ) 19 | 20 | 21 | def dedup(event): 22 | return event["UserName"] 23 | 24 | 25 | def title(event): 26 | return "{} does not have MFA enabled".format(event["UserName"]) 27 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_extraneous_fields.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_extraneous_fields.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | RuleID: AWS.CloudTrail.MFAEnabled.Extra.Fields 7 | Enabled: true 8 | LogTypes: 9 | - AWS.CloudTrail 10 | Tags: 11 | - AWS Managed Rules - Security, Identity & Compliance 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | LogType: AWS.CloudTrail (extraneous Field) 22 | ExpectedResult: false 23 | Log: 24 | Arn: arn:aws:iam::123456789012:user/root 25 | CreateDate: 2019-01-01T00:00:00Z 26 | CredentialReport: 27 | MfaActive: false 28 | PasswordEnabled: true 29 | UserName: root 30 | - 31 | Name: User MFA not enabled fails compliance 32 | LogType: AWS.CloudTrail (extraneous Field) 33 | ExpectedResult: false 34 | Log: 35 | { 36 | "Arn": "arn:aws:iam::123456789012:user/test", 37 | "CreateDate": "2019-01-01T00:00:00", 38 | "CredentialReport": { 39 | "MfaActive": false, 40 | "PasswordEnabled": true 41 | }, 42 | "UserName": "test" 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_generated_functions.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def rule(event): 5 | return True 6 | 7 | 8 | def title(event): 9 | return "THIS IS AN EXAMPLE TITLE" 10 | 11 | 12 | def alert_context(event): 13 | return {"ip": "1.1.1.1"} 14 | 15 | 16 | def description(event): 17 | return "THIS IS AN EXAMPLE DESCRIPTION." 18 | 19 | 20 | def destinations(event): 21 | return ["ExampleDestinationName"] 22 | 23 | 24 | def runbook(event): 25 | return "THIS IS AN EXAMPLE RUNBOOK VALUE." 26 | 27 | 28 | def reference(event): 29 | return "THIS IS AN EXAMPLE REFERENCE." 30 | 31 | 32 | def severity(event): 33 | return "CRITICAL" 34 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_generated_functions.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_generated_functions.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabledGenerated 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Test 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_global.py: -------------------------------------------------------------------------------- 1 | from a_helper import a_says_hello 2 | 3 | 4 | def rule(_): 5 | output = a_says_hello() 6 | return output == "hello from b before a says hello" 7 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_global.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_global.py 3 | DisplayName: Rule with global importing global 4 | Description: we should be able to import a global in another global. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: Test.Global.Global 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Simple return true 14 | ExpectedResult: true 15 | Log: 16 | {} 17 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_mocks.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from unittest.mock import MagicMock 3 | 4 | import boto3 5 | 6 | IGNORED_USERS = {} 7 | 8 | 9 | def rule(event): 10 | return all(isinstance(x, MagicMock) for x in [boto3, boto3.client, date]) 11 | 12 | 13 | def title(event): 14 | return ( 15 | f"BOTO3: {isinstance(boto3, MagicMock)} - " 16 | f"BOTO3.CLIENT: {isinstance(boto3.client, MagicMock)} - " 17 | f"DATE: {isinstance(date, MagicMock)}" 18 | ) 19 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/rules/example_rule_mocks.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_mocks.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabledMocks 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Mocking Test 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | Mocks: 34 | [ 35 | { 36 | "objectName": "boto3", 37 | "returnValue": "example_boto3_return_value" 38 | }, 39 | { 40 | "objectName": "date", 41 | "returnValue": "['example_date_return_value']" 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/scheduled_rules/example_scheduled_rule.py: -------------------------------------------------------------------------------- 1 | # API calls that are indicative of CloudTrail changes 2 | CLOUDTRAIL_CREATE_UPDATE = { 3 | "CreateTrail", 4 | "UpdateTrail", 5 | "StartLogging", 6 | } 7 | 8 | 9 | def rule(event): 10 | return event["eventName"] in CLOUDTRAIL_CREATE_UPDATE 11 | 12 | 13 | def title(event): 14 | return "CloudTrail [{}] was created/updated".format(event["requestParameters"].get("name")) 15 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/scheduled_rules/example_scheduled_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | Filename: example_scheduled_rule.py 3 | RuleID: AWS.CloudTrail.Created.Scheduled 4 | DisplayName: A CloudTrail Was Created or Updated 5 | Enabled: true 6 | ScheduledQueries: 7 | - A Test Query 8 | Tags: 9 | - AWS 10 | - Security Control 11 | Reports: 12 | CIS: 13 | - 3.5 14 | Severity: Info 15 | Description: > 16 | A CloudTrail Trail was created, updated, or enabled. 17 | Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified 18 | Reference: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html 19 | SummaryAttributes: 20 | - eventName 21 | - userAgent 22 | - sourceIpAddress 23 | - recipientAccountId 24 | - p_any_aws_arns 25 | Tests: 26 | - 27 | Name: Blank Test 28 | ExpectedResult: false 29 | Log: 30 | EventName: hello 31 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/packless-rule/packs/test.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | PackID: PantherManaged.Test 3 | Description: Group of all Test detections 4 | PackDefinition: 5 | IDs: 6 | - Test.Included 7 | DisplayName: "Panther Test Pack" 8 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/packless-rule/rules/test_rules/test_deprecated.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: test description 3 | DisplayName: "Test" 4 | Enabled: true 5 | Severity: Medium 6 | DedupPeriodMinutes: 60 7 | Detection: 8 | - All: 9 | - Condition: Equals 10 | KeyPath: IntegrityLevel 11 | Value: System 12 | LogTypes: 13 | - Asana.Audit 14 | RuleID: "Test.Deprecated" 15 | Threshold: 1 16 | Tags: 17 | - Deprecated -------------------------------------------------------------------------------- /tests/fixtures/check-packs/packless-rule/rules/test_rules/test_included.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: test description 3 | DisplayName: "Test" 4 | Enabled: true 5 | Severity: Medium 6 | DedupPeriodMinutes: 60 7 | Detection: 8 | - All: 9 | - Condition: Equals 10 | KeyPath: IntegrityLevel 11 | Value: System 12 | LogTypes: 13 | - Asana.Audit 14 | RuleID: "Test.Included" 15 | Threshold: 1 16 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/packless-rule/rules/test_rules/test_missing.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: test description 3 | DisplayName: "Test" 4 | Enabled: true 5 | Severity: Medium 6 | DedupPeriodMinutes: 60 7 | Detection: 8 | - All: 9 | - Condition: Equals 10 | KeyPath: IntegrityLevel 11 | Value: System 12 | LogTypes: 13 | - Asana.Audit 14 | RuleID: "Test.Missing" 15 | Threshold: 1 16 | -------------------------------------------------------------------------------- /tests/fixtures/correlation-unit-tests/fails/fails1.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: correlation_rule 2 | DisplayName: Example Correlation Rule 3 | Enabled: true 4 | RuleID: My.Failing.Correlation.Rule 5 | Severity: High 6 | Detection: 7 | - Sequence: 8 | - ID: First 9 | RuleID: Okta.Global.MFA.Disabled 10 | MinMatchCount: 7 11 | - ID: Second 12 | RuleID: Okta.Support.Access 13 | MinMatchCount: 1 14 | LookbackWindowMinutes: 15 15 | Schedule: 16 | RateMinutes: 5 17 | TimeoutMinutes: 3 18 | Tests: 19 | - Name: "something" 20 | ExpectedResult: true 21 | RuleOutputs: 22 | - ID: First 23 | Matches: 24 | p_actor: 25 | jane.smith: [1,2] 26 | - ID: Second 27 | Matches: 28 | p_enrichment.endpoint_mapping.aid.assigned_user: 29 | jane.smith: [6] 30 | -------------------------------------------------------------------------------- /tests/fixtures/correlation-unit-tests/passes/pass1.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: correlation_rule 2 | DisplayName: Example Correlation Rule 3 | Enabled: true 4 | RuleID: My.Correlation.Rule 5 | Severity: High 6 | Detection: 7 | - Sequence: 8 | - ID: First 9 | RuleID: Okta.Global.MFA.Disabled 10 | MinMatchCount: 7 11 | - ID: Second 12 | RuleID: Okta.Support.Access 13 | MinMatchCount: 1 14 | LookbackWindowMinutes: 15 15 | Schedule: 16 | RateMinutes: 5 17 | TimeoutMinutes: 3 18 | Tests: 19 | - Name: "something" 20 | ExpectedResult: true 21 | RuleOutputs: 22 | - ID: First 23 | Matches: 24 | p_actor: 25 | jane.smith: [1,2,3,4,4,4,5] 26 | - ID: Second 27 | Matches: 28 | p_enrichment.endpoint_mapping.aid.assigned_user: 29 | jane.smith: [6] 30 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/invalid/schema-1.yml: -------------------------------------------------------------------------------- 1 | # NOTE: this is a valid schema in order to test the behavior 2 | # when both valid and invalid schemas are present. 3 | schema: Custom.SampleSchema1 4 | description: "Sample Schema 1" 5 | referenceURL: "https://runpanther.io" 6 | version: 0 7 | fields: 8 | - name: time 9 | description: Event timestamp 10 | required: true 11 | type: timestamp 12 | timeFormat: rfc3339 13 | isEventTime: true 14 | - name: method 15 | description: The HTTP method used for the request 16 | type: string 17 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/invalid/schema-2.yaml: -------------------------------------------------------------------------------- 1 | schema: Custom.SampleSchema2 2 | description: "Sample Schema 2" 3 | referenceURL: "https://runpanther.io" 4 | version: 0 5 | fields: 6 | - name: time 7 | description: Event timestamp: 8 | required: true 9 | type: timestamp 10 | timeFormat: rfc3339 11 | isEventTime: true 12 | - name: ip 13 | description: The IP address used for the request 14 | type: string 15 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/valid/lookup-table-schema-1.yml: -------------------------------------------------------------------------------- 1 | schema: Custom.AWSAccountIDs 2 | description: "Sample Lookup Table Schema 1" 3 | referenceURL: "https://runpanther.io" 4 | version: 0 5 | parser: 6 | csv: 7 | delimiter: ',' 8 | hasHeader: true 9 | fields: 10 | - name: email 11 | required: true 12 | type: string 13 | - name: awsacctid 14 | required: true 15 | type: string 16 | - name: isProduction 17 | required: true 18 | type: string 19 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/valid/schema-1.yml: -------------------------------------------------------------------------------- 1 | schema: Custom.SampleSchema1 2 | description: "Sample Schema 1" 3 | referenceURL: "https://runpanther.io" 4 | fieldDiscoveryEnabled: false 5 | version: 0 6 | fields: 7 | - name: time 8 | description: Event timestamp 9 | required: true 10 | type: timestamp 11 | timeFormats: 12 | - rfc3339 13 | isEventTime: true 14 | - name: method 15 | description: The HTTP method used for the request 16 | type: string 17 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/valid/schema-2.yaml: -------------------------------------------------------------------------------- 1 | schema: Custom.SampleSchema2 2 | description: "Sample Schema 2" 3 | referenceURL: "https://runpanther.io" 4 | version: 0 5 | fields: 6 | - name: time 7 | description: Event timestamp 8 | required: true 9 | type: timestamp 10 | timeFormat: rfc3339 11 | isEventTime: true 12 | - name: ip 13 | description: The IP address used for the request 14 | type: string -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/valid/schema-3.yml: -------------------------------------------------------------------------------- 1 | schema: Custom.Sample.Schema3 2 | description: "Sample Schema 3" 3 | referenceURL: "https://runpanther.io" 4 | fieldDiscoveryEnabled: true 5 | version: 0 6 | fields: 7 | - name: time 8 | description: Event timestamp 9 | required: true 10 | type: timestamp 11 | timeFormat: rfc3339 12 | isEventTime: true 13 | - name: method 14 | description: The HTTP method used for the request 15 | type: string 16 | -------------------------------------------------------------------------------- /tests/fixtures/custom-schemas/valid/schema_1_tests.yml: -------------------------------------------------------------------------------- 1 | name: test1 2 | logType: Custom.Schema1 3 | input: | 4 | { 5 | "ip": "10.0.0.1", 6 | "timestamp":"2021-03-26T21:40:41Z" 7 | } 8 | result: | 9 | { 10 | "ip": "10.0.0.1", 11 | "timestamp":"2021-03-26T21:40:41Z", 12 | "p_log_type":"Custom.Schema1", 13 | "p_event_time":"2021-03-26T21:40:41Z", 14 | "p_any_ip_addresses":["10.0.0.1"] 15 | } 16 | --- 17 | name: test2 18 | logType: Custom.Schema1 19 | input: | 20 | { 21 | "ip": "10.0.0.2", 22 | "timestamp":"2021-03-26T21:40:41Z" 23 | } 24 | result: | 25 | { 26 | "ip": "10.0.0.2", 27 | "timestamp":"2021-03-26T21:40:41Z", 28 | "p_log_type":"Custom.Schema1", 29 | "p_event_time":"2021-03-26T21:40:41Z", 30 | "p_any_ip_addresses":["10.0.0.1"] 31 | } 32 | -------------------------------------------------------------------------------- /tests/fixtures/derived_without_base/derived.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Sus.Login.Derived' 3 | BaseDetection: 'Sus.Login.Base' 4 | Severity: High 5 | Enabled: true 6 | Tests: 7 | - ExpectedResult: false 8 | Log: {} 9 | Name: t1 -------------------------------------------------------------------------------- /tests/fixtures/detections/.panther_settings.yml: -------------------------------------------------------------------------------- 1 | ignored_files: 2 | - "example_ignored.yml" 3 | - "example_ignored_multi" 4 | -------------------------------------------------------------------------------- /tests/fixtures/detections/aws_globals.py: -------------------------------------------------------------------------------- 1 | # This file exists to define global variables for use by other policies. 2 | GLOBAL_TRUE = True 3 | 4 | 5 | # This policy is a no-op 6 | def policy(_): 7 | return True 8 | -------------------------------------------------------------------------------- /tests/fixtures/detections/aws_globals.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: aws_globals.py 3 | PolicyID: aws_globals 4 | Enabled: true 5 | ResourceTypes: 6 | - AWS.CloudTrail 7 | Tags: 8 | - AWS 9 | Severity: Info 10 | Description: > 11 | Used to define global helpers and variables. 12 | Tests: 13 | - 14 | Name: Dummy Test 15 | ExpectedResult: true 16 | Resource: {} 17 | -------------------------------------------------------------------------------- /tests/fixtures/detections/destinations/example_available_destination_name.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | 4 | 5 | def destinations(event): 6 | return ["Pagerduty"] 7 | -------------------------------------------------------------------------------- /tests/fixtures/detections/destinations/example_available_destination_name.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Enabled: true 3 | Filename: example_available_destination_name.py 4 | RuleID: Example.Rule.Available.Destination 5 | LogTypes: 6 | - AWS.CloudTrail 7 | Severity: Low 8 | DisplayName: Example Rule to Check the Format of the Spec 9 | Tags: 10 | - Tag1 11 | Runbook: Find out who changed the spec format. 12 | Reference: https://www.link-to-info.io 13 | Tests: 14 | - 15 | Name: Name to describe our first test 16 | ExpectedResult: true 17 | Log: 18 | { 19 | "field1": "value1", 20 | } -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_disabled_rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | raise 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_disabled_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_disabled_rule.py 3 | DisplayName: Disabled Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: Example.DisabledRule 8 | Enabled: false 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Test 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule.py 3 | DisplayName: Example Rule 4 | Description: Example 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabled 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Test 25 | ExpectedResult: true 26 | Log: 27 | foo: bar 28 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_data_model_conflict.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: datamodel 2 | DataModelID: onelogin.DataModel.Conflict 3 | Enabled: true 4 | LogTypes: 5 | - OneLogin.Events 6 | Mappings: 7 | - Name: source_ip 8 | Path: ipAddress 9 | - Name: user 10 | Path: $.actor_user_name 11 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_ignored.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy.py 3 | DisplayName: Ignored 4 | Description: This policy should be ignored 5 | Severity: High 6 | PolicyID: IAM.MFAEnabledIgnored 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | Resource: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled triggers a violation. 35 | ExpectedResult: false 36 | Resource: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": "2019-01-01T00:00:00", 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled. 48 | ExpectedResult: false 49 | Resource: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": "2019-01-01T00:00:00", 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_ignored_multi.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy.py 3 | DisplayName: Ignored Again 4 | Description: This policy should be ignored 5 | Severity: High 6 | PolicyID: IAM.MFAEnabledIgnoredAgain 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | Resource: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled triggers a violation. 35 | ExpectedResult: false 36 | Resource: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": "2019-01-01T00:00:00", 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled. 48 | ExpectedResult: false 49 | Resource: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": "2019-01-01T00:00:00", 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_invalid_pack.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | PackID: Contain.Unknown.ID 3 | PackDefinition: 4 | IDs: 5 | - AWS.IAM.BetaTest 6 | - does.not.exist 7 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_malformed_policy.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | DisplayName: MFA Is Not Enabled For User 3 | PolicyID: AWS.IAM.MFANotEnabled 4 | ResourceTypes: 5 | - AWS.IAM.RootUser 6 | - AWS.IAM.User 7 | Tags: 8 | - AWS Managed Rules - Security, Identity & Compliance 9 | - AWS 10 | - CIS 11 | - SOC2 12 | Runbook: > 13 | Find out who disabled MFA on the account. 14 | Reference: https://www.link-to-info.io 15 | Tests: 16 | - 17 | Name: Root MFA not enabled triggers a violation. 18 | ExpectedResult: false 19 | Resource: 20 | Arn: arn:aws:iam::123456789012:user/root 21 | CreateDate: 2019-01-01T00:00:00Z 22 | CredentialReport: 23 | MfaActive: false 24 | PasswordEnabled: true 25 | UserName: root 26 | - 27 | Name: User MFA not enabled triggers a violation. 28 | ExpectedResult: false 29 | Resource: 30 | { 31 | "Arn": "arn:aws:iam::123456789012:user/test", 32 | "CreateDate": "2019-01-01T00:00:00", 33 | "CredentialReport": { 34 | "MfaActive": false, 35 | "PasswordEnabled": true 36 | }, 37 | "UserName": "test" 38 | } 39 | - 40 | Name: User with no password enabled does not trigger a policy violation. 41 | ExpectedResult: true 42 | DOG: lol 43 | Resource: 44 | Arn: arn:aws:iam::123456789012:user/non-ui-user 45 | CreateDate: 2019-01-01T00:00:00Z 46 | UserName: non-ui-user 47 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_malformed_yaml.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: Critical 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabled.Malformed.Yaml 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: User MFA enabled passes compliance but fails dedup check. 25 | ExpectedResult: true 26 | Log: 27 | User:{ "test": "one" } 28 | Arn: arn:aws:iam::123456789012:user/test 29 | CreateDate: '2019-01-01T00:00:00' 30 | CredentialReport: 31 | MfaActive: true 32 | PasswordEnabled: true 33 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnalysisType": "policy", 3 | "Filename": "example_policy.py", 4 | "DisplayName": "MFA Enabled For User (JSON example)", 5 | "Description": "MFA is a security best practice that adds an extra layer of protection for your AWS account logins.", 6 | "Severity": "High", 7 | "PolicyID": "AWS.IAM.MFAEnabled.2", 8 | "Enabled": true, 9 | "ResourceTypes": [ 10 | "AWS.IAM.RootUser", 11 | "AWS.IAM.User" 12 | ], 13 | "Tags": [ 14 | "AWS Managed Rules - Security, Identity & Compliance", 15 | "AWS", 16 | "CIS", 17 | "SOC2" 18 | ], 19 | "Runbook": "Find out who disabled MFA on the account.\n", 20 | "Reference": "https://www.link-to-info.io", 21 | "Tests": [ 22 | { 23 | "Name": "Root MFA not enabled triggers a violation.", 24 | "ResourceType": "AWS.IAM.RootUser.Snapshot", 25 | "ExpectedResult": true, 26 | "Resource": { 27 | "Arn": "arn:aws:iam::123456789012:user/root", 28 | "CreateDate": "2019-01-01T00:00:00.000Z", 29 | "CredentialReport": { 30 | "MfaActive": false, 31 | "PasswordEnabled": true 32 | }, 33 | "UserName": "root" 34 | } 35 | }, 36 | { 37 | "Name": "User with no password enabled does not trigger a policy violation.", 38 | "ResourceType": "AWS.IAM.User.Snapshot", 39 | "ExpectedResult": true, 40 | "Resource": { 41 | "Arn": "arn:aws:iam::123456789012:user/non-ui-user", 42 | "CreateDate": "2019-01-01T00:00:00.000Z", 43 | "UserName": "non-ui-user" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return True 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: IAM.MFAEnabled 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | Resource: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled triggers a violation. 35 | ExpectedResult: false 36 | Resource: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": "2019-01-01T00:00:00", 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled. 48 | ExpectedResult: false 49 | Resource: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": "2019-01-01T00:00:00", 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_bad_resource_type.py: -------------------------------------------------------------------------------- 1 | def policy(resource): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_bad_resource_type.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_bad_resource_type.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: Example.Bad.Resource.Type 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUserz 10 | - AWS.IAM.Userz 11 | Tags: 12 | - bad_resource_type 13 | Runbook: > 14 | Find out who disabled MFA on the account. 15 | Reference: https://www.link-to-info.io 16 | Suppressions: 17 | - aws:resource:1 18 | - aws:.*:other-resource 19 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_import.py: -------------------------------------------------------------------------------- 1 | import aws_globals 2 | 3 | 4 | def policy(resource): 5 | return aws_globals.GLOBAL_TRUE 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_import.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_import.py 3 | DisplayName: Example with Import 4 | Severity: Info 5 | PolicyID: AWS.Import 6 | Enabled: true 7 | ResourceTypes: 8 | - AWS.CloudTrail 9 | Tests: 10 | - 11 | Name: Always Returns True 12 | ExpectedResult: true 13 | Resource: 14 | Key: value 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_invalid_characters.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return True 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_invalid_characters.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_invalid_characters.py 3 | DisplayName: MFA Is Enabled For User & 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: AWS.IAM.MFAEnabled& 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | Resource: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled triggers a violation. 35 | ExpectedResult: false 36 | Resource: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": "2019-01-01T00:00:00", 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled. 48 | ExpectedResult: false 49 | Resource: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": "2019-01-01T00:00:00", 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_missing_policy_file.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: non_existent_policy.py 3 | Severity: High 4 | PolicyID: Policy.Does.Not.Exist 5 | Enabled: true 6 | ResourceTypes: 7 | - AWS.IAM.RootUser 8 | - AWS.IAM.User 9 | Tags: 10 | - AWS Managed Rules - Security, Identity & Compliance 11 | - AWS 12 | - CIS 13 | - SOC2 14 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_required_tests.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_required_tests.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_required_tests.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: IAM.MFAEnabled.Required.Tests 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | ResourceType: AWS.IAM.User.Snapshot (extraneous field) 27 | Resource: 28 | Arn: arn:aws:iam::123456789012:user/root 29 | CreateDate: 2019-01-01T00:00:00Z 30 | CredentialReport: 31 | MfaActive: false 32 | PasswordEnabled: true 33 | UserName: root 34 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_set_duplicates.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_set_duplicates.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_set_duplicates.py 3 | DisplayName: "[MFA Is Enabled For User]" 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: Critical 6 | PolicyID: Example.Policy.Set.Duplicates 7 | OutputIds: 8 | - 00000-01-00000 9 | Enabled: true 10 | ResourceTypes: 11 | - AWS.IAM.RootUser 12 | - AWS.IAM.User 13 | Tags: 14 | - AWS Managed Rules - Security, Identity & Compliance 15 | - AWS 16 | - CIS 17 | - SOC2 18 | Reports: 19 | CIS: 20 | - 1.1 21 | MITRE: 22 | - Extraction:Data Parsing 23 | Runbook: > 24 | Find out who disabled MFA on the account. 25 | Reference: https://www.link-to-info.io 26 | Suppressions: 27 | - asdf 28 | - asdf 29 | Tests: 30 | - 31 | Name: Root MFA not enabled fails compliance 32 | ExpectedResult: false 33 | Resource: 34 | Arn: arn:aws:iam::123456789012:user/root 35 | CreateDate: 2019-01-01T00:00:00Z 36 | CredentialReport: 37 | MfaActive: false 38 | PasswordEnabled: true 39 | UserName: root 40 | - 41 | Name: User MFA not enabled fails compliance 42 | ExpectedResult: false 43 | Resource: 44 | { 45 | "Arn": "arn:aws:iam::123456789012:user/test", 46 | "CreateDate": "2019-01-01T00:00:00", 47 | "CredentialReport": { 48 | "MfaActive": false, 49 | "PasswordEnabled": true 50 | }, 51 | "UserName": "test" 52 | } 53 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule.py: -------------------------------------------------------------------------------- 1 | from panther import test_helper # pylint: disable=import-error 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def rule(event): 7 | if event["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = event.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return ( 15 | test_helper() 16 | and cred_report.get("PasswordEnabled", False) 17 | and cred_report.get("MfaActive", False) 18 | ) 19 | 20 | 21 | def dedup(event): 22 | return None 23 | 24 | 25 | def title(event): 26 | return "{} does not have MFA enabled".format(event["UserName"]) 27 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_bad_log_type.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_bad_log_type.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_bad_log_type.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: Example.Bad.Log.Type 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrailz 14 | Tags: 15 | - bad_log_type 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_invalid_mocks.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import boto3 4 | 5 | IGNORED_USERS = {} 6 | 7 | 8 | def rule(event): 9 | return True 10 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_invalid_mocks.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_invalid_mocks.py 3 | DisplayName: Example Rule Invalid Mock 4 | Severity: Critical 5 | RuleID: Example.Rule.Invalid.Mock 6 | Enabled: true 7 | LogTypes: 8 | - AWS.CloudTrail 9 | Reference: https://www.link-to-info.io 10 | Tests: 11 | - 12 | Name: Example Mocking Test 13 | ExpectedResult: true 14 | Log: 15 | { 16 | "field1": "value1", 17 | } 18 | Mocks: 19 | [ 20 | { 21 | "objectName": "boto3", 22 | "returnValue": "example_boto3_return_value" 23 | }, 24 | { 25 | "objectName": "date", 26 | "returnValue": "['example_date_return_value']" 27 | }, 28 | { # This is an invalid mock as `badmock` is not an object present in the python module 29 | "objectName": "badmock", 30 | "returnValue": "badmockdata" 31 | }, 32 | ] 33 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_invalid_test.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.udm("any_field") 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_invalid_test.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Enabled: true 3 | Filename: example_rule_invalid_test.py 4 | RuleID: Example.Rule.Invalid.Test 5 | LogTypes: 6 | - AWS.CloudTrail 7 | Severity: Low 8 | DisplayName: Example Rule to Check the Format of the Spec 9 | Tags: 10 | - Tags 11 | Runbook: Find out who changed the spec format. 12 | Reference: https://www.link-to-info.io 13 | Tests: 14 | - 15 | Name: Test case missing required field [p_log_type] 16 | ExpectedResult: true 17 | Log: 18 | { 19 | "field1": "value1", 20 | } 21 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_missing_field.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: Critical 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabled.Missing.Field 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: User MFA enabled passes compliance but fails dedup check. 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/test 28 | CreateDate: '2019-01-01T00:00:00' 29 | CredentialReport: 30 | MfaActive: true 31 | PasswordEnabled: true 32 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_set_duplicates.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_set_duplicates.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Enabled: true 3 | Filename: example_rule_set_duplicates.py 4 | RuleID: Example.Rule.Set.Duplicates 5 | LogTypes: 6 | - AWS.CloudTrail 7 | - AWS.CloudTrail 8 | Severity: Low 9 | DisplayName: Example Rule to Check the Format of the Spec 10 | Tags: 11 | - Tags 12 | - Tags 13 | - tags 14 | Runbook: Find out who changed the spec format. 15 | Reference: https://www.link-to-info.io 16 | SummaryAttributes: 17 | - example_dupe 18 | - example_dupe 19 | OutputIds: 20 | - "1234" 21 | - "1234" 22 | Tests: 23 | - 24 | Name: Name to describe our first test. 25 | ExpectedResult: true 26 | Log: 27 | { 28 | "field1": "value1", 29 | } -------------------------------------------------------------------------------- /tests/fixtures/detections/example_strict_invalid_yaml.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_strict_invalid_yaml.py 3 | DisplayName: Sample Rule - Strict Invalid YAML Check 4 | Severity: Info 5 | RuleID: Strict.Invalid.YAML.Check 6 | Enabled: true 7 | SummaryAttributes: 8 | - p_log_type 9 | - p_any_ip_addresses 10 | LogTypes: 11 | - Sample.Log.Type 12 | Tags: 13 | - Test 14 | Runbook: Sample Runbook 15 | Reference: https://www.link-to-info.io 16 | Tests: 17 | - 18 | Name: Testing invalid yaml 19 | ExpectedResult: false 20 | Log: 21 | { 22 | UserName:{ 23 | test: test 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_unhandled_exception.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | raise Exception 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_unhandled_exception.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Enabled: true 3 | Filename: example_unhandled_exception.py 4 | RuleID: Example.Rule.Unknown.Exception 5 | LogTypes: 6 | - AWS.CloudTrail 7 | Severity: Low 8 | DisplayName: Example Rule to Check the Format of the Spec 9 | Tags: 10 | - Tags 11 | Runbook: Find out who changed the spec format. 12 | Reference: https://www.link-to-info.io 13 | Tests: 14 | - 15 | Name: Name to describe our first test. 16 | ExpectedResult: true 17 | Log: 18 | { 19 | "field1": "value1", 20 | } -------------------------------------------------------------------------------- /tests/fixtures/detections/example_unhandled_exception_on_import.py: -------------------------------------------------------------------------------- 1 | import unknown_module 2 | 3 | 4 | def rule(event): 5 | return True 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/advanced_rules/example_rule_data_model.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | # filter events on unified data model field 3 | if event.udm("event_type") and event.udm("event_type") == "login_failure": 4 | return True 5 | # filter based on standard log type's fields 6 | if event.get("event_type_id", 0) == 6: 7 | return True 8 | # unknown event type 9 | return False 10 | 11 | 12 | def title(event): 13 | # use unified data model field in title 14 | return "User [{}] from IP [{}] has exceeded the failed logins threshold".format( 15 | event.udm("user"), event.udm("source_ip") 16 | ) 17 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/advanced_rules/example_rule_data_model.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_data_model.py 3 | RuleID: DataModel.BruteForceByIP 4 | DisplayName: Brute Force By IP 5 | Enabled: true 6 | LogTypes: 7 | - OneLogin.Events 8 | - GSuite.Reports 9 | Tags: 10 | - OneLogin 11 | - GSuite 12 | Severity: Medium 13 | Reports: 14 | MITRE ATT&CK: 15 | - Brute Force:Password Spraying 16 | Description: A single ip address was denied access to OneLogin more times than the configured threshold. 17 | Threshold: 10 18 | DedupPeriodMinutes: 10 19 | Reference: https://developers.onelogin.com/api-docs/1/events/event-resource 20 | Runbook: Analyze the IP they came from, and other actions taken before/after. Check if a user from this ip eventually authenticated successfully. 21 | SummaryAttributes: 22 | - p_any_ip_addresses 23 | Tests: 24 | - 25 | Name: Normal OneLogin Login Event 26 | ExpectedResult: false 27 | Log: 28 | { 29 | 'event_type_id': 8, 30 | 'actor_user_id': 123456, 31 | 'actor_user_name': 'Bob Cat', 32 | 'user_id': 123456, 33 | 'user_name': 'Bob Cat', 34 | 'ipaddr': '1.2.3.4', 35 | 'p_log_type': 'OneLogin.Events' 36 | } 37 | - 38 | Name: Failed OneLogin Login Event 39 | ExpectedResult: true 40 | Log: 41 | { 42 | 'event_type_id': 6, 43 | 'actor_user_id': 123456, 44 | 'actor_user_name': 'Bob Cat', 45 | 'user_id': 123456, 46 | 'user_name': 'Bob Cat', 47 | 'ipaddr': '1.2.3.4', 48 | 'p_log_type': 'OneLogin.Events' 49 | } 50 | - 51 | Name: GSuite Normal Login Event 52 | ExpectedResult: false 53 | Log: 54 | { 55 | 'id': {'applicationName': 'login'}, 56 | 'ipAddress': '4.3.2.1', 57 | 'events': [ 58 | { 59 | 'type': 'login', 60 | 'name': 'login_success' 61 | } 62 | ], 63 | 'p_log_type': 'GSuite.Reports' 64 | } 65 | - 66 | Name: GSuite Failed Login Event 67 | ExpectedResult: true 68 | Log: 69 | { 70 | 'actor' : { 71 | 'email': 'bob@example.com' 72 | }, 73 | 'id': {'applicationName': 'login'}, 74 | 'ipAddress': '4.3.2.1', 75 | 'events': [ 76 | { 77 | 'type': 'login', 78 | 'name': 'login_failure' 79 | } 80 | ], 81 | 'p_log_type': 'GSuite.Reports' 82 | } 83 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/data_models/GSuite.Events.DataModel.py: -------------------------------------------------------------------------------- 1 | def get_event_type(event): 2 | for details in event.get("events", []): 3 | if details.get("type", "") == "login" and details.get("name", "") == "login_failure": 4 | return "login_failure" 5 | return "other_event" 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/data_models/example_data_model.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: datamodel 2 | DataModelID: onelogin.DataModel 3 | Enabled: true 4 | LogTypes: 5 | - OneLogin.Events 6 | Mappings: 7 | - Name: source_ip 8 | Path: ipaddr 9 | - Name: user 10 | Path: $.actor_user_name 11 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/data_models/example_data_model_disabled.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: datamodel 2 | DataModelID: onelogin.DataModel.Disabled 3 | Enabled: false 4 | LogTypes: 5 | - OneLogin.Events 6 | Mappings: 7 | - Name: source_ip 8 | Path: ipAddress 9 | - Name: user 10 | Path: $.actor_user_name 11 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/data_models/example_data_model_python.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: datamodel 2 | DataModelID: GSuite.Events.DataModel 3 | Enabled: true 4 | Filename: GSuite.Events.DataModel.py 5 | LogTypes: 6 | - GSuite.Reports 7 | Mappings: 8 | - Name: source_ip 9 | Path: ipAddress 10 | - Name: event_type 11 | Method: get_event_type 12 | - Name: user 13 | Path: $.actor.email 14 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/a_helper.py: -------------------------------------------------------------------------------- 1 | from b_helper import b_says_hello 2 | 3 | 4 | def a_says_hello(): 5 | return f"{b_says_hello()} before a says hello" 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/a_helper.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: a_helper 3 | Filename: a_helper.py 4 | Description: > 5 | Used to test global importing other globals 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/b_helper.py: -------------------------------------------------------------------------------- 1 | def b_says_hello(): 2 | return "hello from b" 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/b_helper.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: b_helper 3 | Filename: b_helper.py 4 | Description: > 5 | Used to test global importing other globals 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/helpers.py: -------------------------------------------------------------------------------- 1 | def test_helper(): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/helpers.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: panther 3 | Filename: helpers.py 4 | Tags: 5 | - AWS 6 | Description: > 7 | Used to define global helpers and variables. 8 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/packs/sample-pack.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: pack 2 | Description: This pack groups amazing rules and policies 3 | DisplayName: Sample Pack Data 4 | PackID: Sample.Pack 5 | PackDefinition: 6 | IDs: 7 | - AWS.IAM.MFAEnabled 8 | - AWS.IAM.BetaTest 9 | - AWS.CloudTrail.MFAEnabled 10 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy.py 3 | DisplayName: "[MFA Is Enabled For User]" 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: Critical 6 | PolicyID: AWS.IAM.MFAEnabled 7 | OutputIds: 8 | - 00000-01-00000 9 | Enabled: true 10 | ResourceTypes: 11 | - AWS.IAM.RootUser 12 | - AWS.IAM.User 13 | Tags: 14 | - AWS Managed Rules - Security, Identity & Compliance 15 | - AWS 16 | - CIS 17 | - SOC2 18 | Reports: 19 | CIS: 20 | - 1.1 21 | MITRE: 22 | - Extraction:Data Parsing 23 | Runbook: > 24 | Find out who disabled MFA on the account. 25 | Reference: https://www.link-to-info.io 26 | Tests: 27 | - 28 | Name: Root MFA not enabled fails compliance 29 | ExpectedResult: false 30 | Resource: 31 | Arn: arn:aws:iam::123456789012:user/root 32 | CreateDate: 2019-01-01T00:00:00Z 33 | CredentialReport: 34 | MfaActive: false 35 | PasswordEnabled: true 36 | UserName: root 37 | - 38 | Name: User MFA not enabled fails compliance 39 | ExpectedResult: false 40 | Resource: 41 | { 42 | "Arn": "arn:aws:iam::123456789012:user/test", 43 | "CreateDate": "2019-01-01T00:00:00", 44 | "CredentialReport": { 45 | "MfaActive": false, 46 | "PasswordEnabled": true 47 | }, 48 | "UserName": "test" 49 | } 50 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_beta.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return False 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_beta.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_beta.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: Another valid policy. 5 | Severity: High 6 | PolicyID: AWS.IAM.BetaTest 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | ExpectedResult: false 22 | Resource: 23 | Arn: arn:aws:iam::123456789012:user/root 24 | CreateDate: 2019-01-01T00:00:00Z 25 | CredentialReport: 26 | MfaActive: false 27 | PasswordEnabled: true 28 | UserName: root 29 | - 30 | Name: User MFA not enabled fails compliance 31 | ExpectedResult: false 32 | Resource: 33 | { 34 | "Arn": "arn:aws:iam::123456789012:user/test", 35 | "CreateDate": "2019-01-01T00:00:00", 36 | "CredentialReport": { 37 | "MfaActive": false, 38 | "PasswordEnabled": true 39 | }, 40 | "UserName": "test" 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_extraneous_fields.py: -------------------------------------------------------------------------------- 1 | import panther 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def policy(resource): 7 | if resource["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = resource.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_extraneous_fields.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_extraneous_fields.py 3 | DisplayName: MFA Is Enabled For User 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | PolicyID: IAM.MFAEnabled Extra Fields 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS Managed Rules - Security, Identity & Compliance 13 | - AWS 14 | - CIS 15 | - SOC2 16 | Runbook: > 17 | Find out who disabled MFA on the account. 18 | Reference: https://www.link-to-info.io 19 | Suppressions: 20 | - aws:resource:1 21 | - aws:.*:other-resource 22 | Tests: 23 | - 24 | Name: Root MFA not enabled triggers a violation. 25 | ExpectedResult: false 26 | ResourceType: AWS.IAM.User.Snapshot (extraneous field) 27 | Resource: 28 | Arn: arn:aws:iam::123456789012:user/root 29 | CreateDate: 2019-01-01T00:00:00Z 30 | CredentialReport: 31 | MfaActive: false 32 | PasswordEnabled: true 33 | UserName: root 34 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_generated_functions.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def policy(resource): 5 | if resource["UserName"] in IGNORED_USERS: 6 | return False 7 | 8 | cred_report = resource.get("CredentialReport", {}) 9 | if not cred_report: 10 | return True 11 | 12 | return cred_report.get("PasswordEnabled", False) and cred_report.get("MfaActive", False) 13 | 14 | 15 | def title(resource): 16 | return "THIS IS AN EXAMPLE TITLE" 17 | 18 | 19 | def alert_context(resource): 20 | return {"ip": "1.1.1.1"} 21 | 22 | 23 | def description(resource): 24 | return "THIS IS AN EXAMPLE DESCRIPTION." 25 | 26 | 27 | def destinations(resource): 28 | return ["ExampleDestinationName"] 29 | 30 | 31 | def runbook(resource): 32 | return "THIS IS AN EXAMPLE RUNBOOK VALUE." 33 | 34 | 35 | def reference(resource): 36 | return "THIS IS AN EXAMPLE REFERENCE." 37 | 38 | 39 | def severity(resource): 40 | return "CrItIcAl" 41 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/policies/example_policy_generated_functions.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: policy 2 | Filename: example_policy_generated_functions.py 3 | DisplayName: MFA Is Enabled For User Generated Fields 4 | Description: Another valid policy. 5 | Severity: High 6 | PolicyID: AWS.IAM.MFAEnabledGenerated 7 | Enabled: true 8 | ResourceTypes: 9 | - AWS.IAM.RootUser 10 | - AWS.IAM.User 11 | Tags: 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | ExpectedResult: false 22 | Resource: 23 | Arn: arn:aws:iam::123456789012:user/root 24 | CreateDate: 2019-01-01T00:00:00Z 25 | CredentialReport: 26 | MfaActive: false 27 | PasswordEnabled: true 28 | UserName: root 29 | - 30 | Name: User MFA not enabled fails compliance 31 | ExpectedResult: false 32 | Resource: 33 | { 34 | "Arn": "arn:aws:iam::123456789012:user/test", 35 | "CreateDate": "2019-01-01T00:00:00", 36 | "CredentialReport": { 37 | "MfaActive": false, 38 | "PasswordEnabled": true 39 | }, 40 | "UserName": "test" 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/queries/query_one.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: A Test Query 3 | Enabled: true 4 | Query: 'SELECT * FROM snowflake.account_usage.login_history LIMIT 10' 5 | Description: Some meme query 6 | Tags: 7 | - some 8 | - meme 9 | Schedule: 10 | CronExpression: '0 8 * * *' 11 | TimeoutMinutes: 1 12 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/queries/query_three.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: saved_query 2 | QueryName: A Third Test Query 3 | AthenaQuery: 'SELECT * FROM panther_logs.aws_cloudtrail LIMIT 10' 4 | SnowflakeQuery: 'SELECT * FROM panther_logs.public.aws_cloudtrail LIMIT 10' 5 | Description: Some saved (not scheduled) query 6 | Tags: 7 | - some 8 | - meme 9 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/queries/query_two.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: Another Test Query 3 | Enabled: true 4 | Query: > 5 | SELECT * 6 | FROM panther_logs.public.s3_access 7 | WHERE eventName='abc123' 8 | LIMIT 10 9 | Description: Some meme query 10 | Tags: 11 | - some 12 | - meme 13 | Schedule: 14 | RateMinutes: 10 15 | TimeoutMinutes: 10 16 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule.py: -------------------------------------------------------------------------------- 1 | from panther import test_helper # pylint: disable=import-error 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def rule(event): 7 | if event["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | if "CredentialReport" not in event: 11 | return False 12 | 13 | cred_report = event.get("CredentialReport", {}) 14 | if not cred_report: 15 | return True 16 | 17 | return ( 18 | test_helper() 19 | and cred_report.get("PasswordEnabled", False) 20 | and cred_report.get("MfaActive", False) 21 | ) 22 | 23 | 24 | def dedup(event): 25 | return event["UserName"] 26 | 27 | 28 | def title(event): 29 | return "{} does not have MFA enabled".format(event["UserName"]) 30 | 31 | 32 | def alert_context(event): 33 | # test/validate that we can return the event as alert_context 34 | return event 35 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabled 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Root MFA not enabled fails compliance 25 | ExpectedResult: false 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | - 34 | Name: User MFA not enabled fails compliance 35 | ExpectedResult: false 36 | Log: 37 | { 38 | "Arn": "arn:aws:iam::123456789012:user/test", 39 | "CreateDate": 2019-01-01, 40 | "CredentialReport": { 41 | "MfaActive": false, 42 | "PasswordEnabled": true 43 | }, 44 | "UserName": "test" 45 | } 46 | - 47 | Name: User MFA enabled passes compliance. 48 | ExpectedResult: true 49 | Log: 50 | { 51 | "Arn": "arn:aws:iam::123456789012:user/test", 52 | "CreateDate": 2019-01-01, 53 | "CredentialReport": { 54 | "MfaActive": true, 55 | "PasswordEnabled": true 56 | }, 57 | "UserName": "test" 58 | } 59 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_extraneous_fields.py: -------------------------------------------------------------------------------- 1 | from panther import test_helper # pylint: disable=import-error 2 | 3 | IGNORED_USERS = {} 4 | 5 | 6 | def rule(event): 7 | if event["UserName"] in IGNORED_USERS: 8 | return False 9 | 10 | cred_report = event.get("CredentialReport", {}) 11 | if not cred_report: 12 | return True 13 | 14 | return ( 15 | test_helper() 16 | and cred_report.get("PasswordEnabled", False) 17 | and cred_report.get("MfaActive", False) 18 | ) 19 | 20 | 21 | def dedup(event): 22 | return event["UserName"] 23 | 24 | 25 | def title(event): 26 | return "{} does not have MFA enabled".format(event["UserName"]) 27 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_extraneous_fields.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_extraneous_fields.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | RuleID: AWS.CloudTrail.MFAEnabled.Extra.Fields 7 | Enabled: true 8 | LogTypes: 9 | - AWS.CloudTrail 10 | Tags: 11 | - AWS Managed Rules - Security, Identity & Compliance 12 | - AWS 13 | - CIS 14 | - SOC2 15 | Runbook: > 16 | Find out who disabled MFA on the account. 17 | Reference: https://www.link-to-info.io 18 | Tests: 19 | - 20 | Name: Root MFA not enabled fails compliance 21 | LogType: AWS.CloudTrail (extraneous Field) 22 | ExpectedResult: false 23 | Log: 24 | Arn: arn:aws:iam::123456789012:user/root 25 | CreateDate: 2019-01-01T00:00:00Z 26 | CredentialReport: 27 | MfaActive: false 28 | PasswordEnabled: true 29 | UserName: root 30 | - 31 | Name: User MFA not enabled fails compliance 32 | LogType: AWS.CloudTrail (extraneous Field) 33 | ExpectedResult: false 34 | Log: 35 | { 36 | "Arn": "arn:aws:iam::123456789012:user/test", 37 | "CreateDate": "2019-01-01T00:00:00", 38 | "CredentialReport": { 39 | "MfaActive": false, 40 | "PasswordEnabled": true 41 | }, 42 | "UserName": "test" 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_generated_functions.py: -------------------------------------------------------------------------------- 1 | IGNORED_USERS = {} 2 | 3 | 4 | def rule(event): 5 | return True 6 | 7 | 8 | def title(event): 9 | return "THIS IS AN EXAMPLE TITLE" 10 | 11 | 12 | def alert_context(event): 13 | return {"ip": "1.1.1.1"} 14 | 15 | 16 | def description(event): 17 | return "THIS IS AN EXAMPLE DESCRIPTION." 18 | 19 | 20 | def destinations(event): 21 | return ["ExampleDestinationName"] 22 | 23 | 24 | def runbook(event): 25 | return "THIS IS AN EXAMPLE RUNBOOK VALUE." 26 | 27 | 28 | def reference(event): 29 | return "THIS IS AN EXAMPLE REFERENCE." 30 | 31 | 32 | def severity(event): 33 | return "CRITICAL" 34 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_generated_functions.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_generated_functions.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabledGenerated 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Test 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_global.py: -------------------------------------------------------------------------------- 1 | from a_helper import a_says_hello 2 | 3 | 4 | def rule(_): 5 | output = a_says_hello() 6 | return output == "hello from b before a says hello" 7 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_global.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_global.py 3 | DisplayName: Rule with global importing global 4 | Description: we should be able to import a global in another global. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: Test.Global.Global 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Simple return true 14 | ExpectedResult: true 15 | Log: 16 | {} 17 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_mocks.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from unittest.mock import MagicMock 3 | 4 | import boto3 5 | 6 | IGNORED_USERS = {} 7 | 8 | 9 | def rule(event): 10 | return all(isinstance(x, MagicMock) for x in [boto3, boto3.client, date]) 11 | 12 | 13 | def title(event): 14 | return ( 15 | f"BOTO3: {isinstance(boto3, MagicMock)} - " 16 | f"BOTO3.CLIENT: {isinstance(boto3.client, MagicMock)} - " 17 | f"DATE: {isinstance(date, MagicMock)}" 18 | ) 19 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/rules/example_rule_mocks.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: example_rule_mocks.py 3 | DisplayName: MFA Rule 4 | Description: MFA is a security best practice that adds an extra layer of protection for your AWS account logins. 5 | Severity: High 6 | Threshold: 5 7 | RuleID: AWS.CloudTrail.MFAEnabledMocks 8 | Enabled: true 9 | SummaryAttributes: 10 | - p_log_type 11 | - p_any_ip_addresses 12 | LogTypes: 13 | - AWS.CloudTrail 14 | Tags: 15 | - AWS Managed Rules - Security, Identity & Compliance 16 | - AWS 17 | - CIS 18 | - SOC2 19 | Runbook: > 20 | Find out who disabled MFA on the account. 21 | Reference: https://www.link-to-info.io 22 | Tests: 23 | - 24 | Name: Example Mocking Test 25 | ExpectedResult: true 26 | Log: 27 | Arn: arn:aws:iam::123456789012:user/root 28 | CreateDate: 2019-01-01T00:00:00Z 29 | CredentialReport: 30 | MfaActive: false 31 | PasswordEnabled: true 32 | UserName: root 33 | Mocks: 34 | [ 35 | { 36 | "objectName": "boto3", 37 | "returnValue": "example_boto3_return_value" 38 | }, 39 | { 40 | "objectName": "date", 41 | "returnValue": "['example_date_return_value']" 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/scheduled_rules/example_scheduled_rule.py: -------------------------------------------------------------------------------- 1 | # API calls that are indicative of CloudTrail changes 2 | CLOUDTRAIL_CREATE_UPDATE = { 3 | "CreateTrail", 4 | "UpdateTrail", 5 | "StartLogging", 6 | } 7 | 8 | 9 | def rule(event): 10 | return event["eventName"] in CLOUDTRAIL_CREATE_UPDATE 11 | 12 | 13 | def title(event): 14 | return "CloudTrail [{}] was created/updated".format(event["requestParameters"].get("name")) 15 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/scheduled_rules/example_scheduled_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | Filename: example_scheduled_rule.py 3 | RuleID: AWS.CloudTrail.Created.Scheduled 4 | DisplayName: A CloudTrail Was Created or Updated 5 | Enabled: true 6 | ScheduledQueries: 7 | - A Test Query 8 | Tags: 9 | - AWS 10 | - Security Control 11 | Reports: 12 | CIS: 13 | - 3.5 14 | Severity: Info 15 | Description: > 16 | A CloudTrail Trail was created, updated, or enabled. 17 | Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified 18 | Reference: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html 19 | SummaryAttributes: 20 | - eventName 21 | - userAgent 22 | - sourceIpAddress 23 | - recipientAccountId 24 | - p_any_aws_arns 25 | Tests: 26 | - 27 | Name: Blank Test 28 | ExpectedResult: false 29 | Log: 30 | EventName: hello 31 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.get("userAgent") == "Max" 3 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.rule.with.filters.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.get("userAgent") == "Max" 3 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.rule.with.filters.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'basic.python.rule.with.filters' 3 | Filename: basic.python.rule.with.filters.py 4 | InlineFilters: 5 | - KeyPath: actionName 6 | Condition: Equals 7 | Value: Beans 8 | DedupPeriodMinutes: 60 9 | DisplayName: 'basic.python.rule.with.filters' 10 | Enabled: true 11 | LogTypes: 12 | - Panther.Audit 13 | Severity: Medium 14 | Tests: 15 | - Name: wrong userAgent 16 | ExpectedResult: false 17 | Log: { 'userAgent': 'John', 'actionName': 'Beans' } 18 | - Name: wrong actionName 19 | ExpectedResult: false 20 | Log: { 'userAgent': 'Max', 'actionName': 'bananas' } 21 | - Name: alerts 22 | ExpectedResult: true 23 | Log: { 'userAgent': 'Max', 'actionName': 'Beans' } 24 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'basic.python.rule' 3 | Filename: basic.python.rule.py 4 | DedupPeriodMinutes: 60 5 | DisplayName: 'basic.python.rule' 6 | Enabled: true 7 | LogTypes: 8 | - Panther.Audit 9 | Severity: Medium 10 | Tests: 11 | - Name: wrong userAgent 12 | ExpectedResult: false 13 | Log: { 'userAgent': 'John' } 14 | - Name: alerts 15 | ExpectedResult: true 16 | Log: { 'userAgent': 'Max' } 17 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.scheduled_rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.get("userAgent") == "Max" 3 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.scheduled_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | RuleID: 'basic.python.scheduled_rule' 3 | Filename: basic.python.scheduled_rule.py 4 | DedupPeriodMinutes: 60 5 | DisplayName: 'basic.python.scheduled_rule' 6 | Enabled: true 7 | ScheduledQueries: 8 | - MyQuery 9 | Severity: Medium 10 | Tests: 11 | - Name: alerts 12 | ExpectedResult: true 13 | Log: { 'userAgent': 'Max' } 14 | - Name: no alerts - wrong userAgent 15 | ExpectedResult: false 16 | Log: { 'userAgent': 'John' } 17 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.rule.with.filters.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'basic.rule.with.filters' 3 | Detection: 4 | - Key: userAgent 5 | Condition: Equals 6 | Value: Max 7 | InlineFilters: 8 | - KeyPath: actionName 9 | Condition: Equals 10 | Value: Beans 11 | DedupPeriodMinutes: 60 12 | DisplayName: 'basic.rule.with.filters' 13 | Enabled: true 14 | LogTypes: 15 | - Panther.Audit 16 | Severity: Medium 17 | Tests: 18 | - Name: wrong userAgent 19 | ExpectedResult: false 20 | Log: { 'userAgent': 'John', 'actionName': 'Beans' } 21 | - Name: wrong actionName 22 | ExpectedResult: false 23 | Log: { 'userAgent': 'Max', 'actionName': 'bananas' } 24 | - Name: alerts 25 | ExpectedResult: true 26 | Log: { 'userAgent': 'Max', 'actionName': 'Beans' } 27 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'basic.rule' 3 | Detection: 4 | - Key: userAgent 5 | Condition: Equals 6 | Value: Max 7 | DedupPeriodMinutes: 60 8 | DisplayName: 'basic.rule' 9 | Enabled: true 10 | LogTypes: 11 | - Panther.Audit 12 | Severity: Medium 13 | Tests: 14 | - Name: alerts 15 | ExpectedResult: true 16 | Log: { 'userAgent': 'Max' } 17 | - Name: no alerts - wrong userAgent 18 | ExpectedResult: false 19 | Log: { 'userAgent': 'John' } 20 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.scheduled_rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | RuleID: 'basic.scheduled_rule' 3 | Detection: 4 | - Key: userAgent 5 | Condition: Equals 6 | Value: Max 7 | DedupPeriodMinutes: 60 8 | DisplayName: 'basic.scheduled_rule' 9 | Enabled: true 10 | ScheduledQueries: 11 | - MyQuery 12 | Severity: Medium 13 | Tests: 14 | - Name: alerts 15 | ExpectedResult: true 16 | Log: { 'userAgent': 'Max' } 17 | - Name: no alerts - wrong userAgent 18 | ExpectedResult: false 19 | Log: { 'userAgent': 'John' } 20 | -------------------------------------------------------------------------------- /tests/fixtures/lookup-tables/invalid/lookup-table-1.yml: -------------------------------------------------------------------------------- 1 | schema: Custom.SampleSchema1 2 | description: "Sample Schema 1" 3 | referenceURL: "https://runpanther.io" 4 | version: 0 5 | fields: 6 | - name: time 7 | description: Event timestamp 8 | required: true 9 | type: timestamp 10 | timeFormat: rfc3339 11 | isEventTime: true 12 | - name: method 13 | description: The HTTP method used for the request 14 | type: string 15 | -------------------------------------------------------------------------------- /tests/fixtures/lookup-tables/valid/lookup-table-1.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: lookup_table 2 | LookupName: LocalFileLookupTable 3 | Filename: sample_aws_accounts.csv # relative file path 4 | Description: This example specifies a local input file 5 | Enabled: true 6 | Reference: https:/mysamplelookupdocpage.com 7 | LogTypeMap: 8 | PrimaryKey: awsacctid 9 | AssociatedLogTypes: 10 | - LogType: AWS.CloudTrail 11 | Selectors: 12 | - "$userIdentity.accountId" # This is JSON path and needs to start with $ 13 | - "recipientAccountId" 14 | Schema: Custom.AWSAccountIDs 15 | -------------------------------------------------------------------------------- /tests/fixtures/lookup-tables/valid/lookup-table-2.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: lookup_table 2 | LookupName: RefreshLookupTable 3 | Refresh: 4 | RoleARN: arn:aws:iam::123456789123:role/PantherLUTsRole-refreshlookuptable 5 | ObjectPath: s3://lookup-foobar/sample_aws_accounts.csv 6 | PeriodMinutes: 30 7 | ObjectKMSKey: arn:aws:kms:us-east-1:123456789123:key/73d4c1e5-26d7-4a60-b2b5-13d171af7772 8 | Description: This example specifies an input file in S3 9 | Enabled: true 10 | Reference: https:/mysamplelookupdocpage.com 11 | LogTypeMap: 12 | PrimaryKey: awsacctid 13 | AssociatedLogTypes: 14 | - LogType: AWS.CloudTrail 15 | Selectors: 16 | - "$.userIdentity.accountId" # A nested selector must be a valid JSONPath expression starting with '$' 17 | - "recipientAccountId" 18 | Schema: Custom.AWSAccountIDs 19 | -------------------------------------------------------------------------------- /tests/fixtures/lookup-tables/valid/sample_aws_accounts.csv: -------------------------------------------------------------------------------- 1 | email,awsacctid,isProduction 2 | someone@panther.io,210768072911,True 3 | someone.else@panther.io,210768072912,True 4 | someone.other@panther.io,210768072913,True 5 | -------------------------------------------------------------------------------- /tests/fixtures/queries/invalid/example-scheduled-query-invalid-tablename-1.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_1 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM datalake.account_usage.login_history 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM datalake.account_usage.login_history 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | RateMinutes: 20 33 | TimeoutMinutes: 2 34 | -------------------------------------------------------------------------------- /tests/fixtures/queries/invalid/example-scheduled-query-invalid-tablename-2.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_2 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM login_history 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM login_history 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | RateMinutes: 20 33 | TimeoutMinutes: 2 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/queries/invalid/example-scheduled-query-invalid-tablename-3.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_3 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM datalake.public.account_usage.login_history 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM datalake.public.account_usage.login_history 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | RateMinutes: 20 33 | TimeoutMinutes: 2 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/queries/invalid/example-scheduled-query-invalid-tablename-4.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_4 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM datalake.public 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM datalake.public 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | RateMinutes: 20 33 | TimeoutMinutes: 2 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/queries/valid/example-scheduled-query-cron.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_Cron 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM snowflake.account_usage.login_history 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM datalake.public.login_history 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | CronExpression: "0 0 29 2 *" 33 | TimeoutMinutes: 2 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/queries/valid/example-scheduled-query-rateminutes.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_query 2 | QueryName: ScheduledQuery_Example_RateMinutes 3 | Description: Example for a scheduled query for PAT 4 | Enabled: true 5 | Query: 6 | "SELECT 7 | user_name, 8 | reported_client_type, 9 | COUNT(event_id) AS counts 10 | FROM datalake.public.login_history 11 | WHERE 12 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 13 | AND 14 | error_code IS NOT NULL 15 | GROUP BY reported_client_type, user_name 16 | HAVING counts >= 3" 17 | SnowflakeQuery: 18 | "SELECT 19 | user_name, 20 | reported_client_type, 21 | COUNT(event_id) AS counts 22 | FROM datalake.public.login_history 23 | WHERE 24 | DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24 25 | AND 26 | error_code IS NOT NULL 27 | GROUP BY reported_client_type, user_name 28 | HAVING counts >= 3" 29 | Tags: 30 | - Datalake 31 | Schedule: 32 | RateMinutes: 20 33 | TimeoutMinutes: 2 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/invalid/invalid_Test.MultiMatch.Key.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Panther Labs, Inc. 2 | # 3 | # The Panther SaaS is licensed under the terms of the Panther Enterprise Subscription 4 | # Agreement available at https://panther.com/enterprise-subscription-agreement/. 5 | # All intellectual property rights in and to the Panther SaaS, including any and all 6 | # rights to access the Panther SaaS, are governed by the Panther Enterprise Subscription Agreement. 7 | 8 | AnalysisType: rule 9 | RuleID: Test.MultiMatch.Key 10 | DisplayName: EKS Audit Log based single sourceIP is generating multiple 403s 11 | Severity: High 12 | Enabled: true 13 | LogTypes: 14 | - Amazon.EKS.Audit 15 | Detection: 16 | - Condition: StartsWith 17 | Values: 18 | - Key: user_id 19 | - Key: actor_user_id 20 | - DeepKey: 21 | - user 22 | - user_id 23 | - Condition: Equals 24 | Values: 25 | - Key: source_ip 26 | - Key: source_ip_address 27 | - DeepKey: 28 | - source_ip 29 | - source_ip_address 30 | - Condition: DoesNotEqual 31 | Values: 32 | - Key: response_status 33 | - Key: response_status_code 34 | - DeepKey: 35 | - response 36 | - status_code 37 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/invalid/invalid_asana_team_privacy.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: "An Asana team's setting was changed to public to the organization (not public to internet). " 3 | DisplayName: 'Asana Team Privacy Public' 4 | Enabled: true 5 | Severity: Low 6 | Detection: 7 | - Key: event_type 8 | Condition: Equals 9 | Value: team_privacy_settings_changed 10 | - DeepKey: details.new_value 11 | Condition: Equals 12 | Value: public 13 | Extra: Param 14 | Tests: 15 | - ExpectedResult: true 16 | Log: 17 | actor: 18 | actor_type: user 19 | email: homer.simpson@panther.io 20 | gid: '12345' 21 | name: Homer Simpson 22 | context: 23 | client_ip_address: 12.12.12.12 24 | context_type: web 25 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 26 | created_at: '2022-12-16 19:35:21.026' 27 | details: 28 | new_value: public 29 | event_category: access_control 30 | event_type: team_privacy_settings_changed 31 | gid: '12345' 32 | resource: 33 | gid: '12345' 34 | name: Example Team Name 35 | resource_type: team 36 | p_log_type: Asana.Audit 37 | Name: Team made public 38 | - ExpectedResult: false 39 | Log: 40 | actor: 41 | actor_type: user 42 | email: homer.simpsons@simpsons.com 43 | gid: '1234567890' 44 | name: Homer Simpson 45 | context: 46 | client_ip_address: 1.2.3.4 47 | context_type: web 48 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 49 | created_at: '2023-02-01 18:05:08.413' 50 | details: 51 | method: 52 | - SAML 53 | event_category: logins 54 | event_type: user_login_succeeded 55 | gid: '123456789' 56 | resource: 57 | email: homer.simpsons@simpsons.com 58 | gid: '1234567890' 59 | name: Homer Simpson 60 | resource_type: user 61 | p_log_type: Asana.Audit 62 | Name: Other event 63 | DedupPeriodMinutes: 60 64 | LogTypes: 65 | - Asana.Audit 66 | RuleID: 'Asana.Team.Privacy.Public' 67 | Threshold: 1 68 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/GitHub.Team.Modified.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Panther Labs, Inc. 2 | # 3 | # The Panther SaaS is licensed under the terms of the Panther Enterprise Subscription 4 | # Agreement available at https://panther.com/enterprise-subscription-agreement/. 5 | # All intellectual property rights in and to the Panther SaaS, including any and all 6 | # rights to access the Panther SaaS, are governed by the Panther Enterprise Subscription Agreement. 7 | 8 | AnalysisType: rule 9 | RuleID: GitHub.Team.Modified 10 | DisplayName: GitHub Team Modified 11 | Enabled: true 12 | LogTypes: 13 | - GitHub.Audit 14 | Tags: 15 | - GitHub 16 | - Initial Access:Supply Chain Compromise 17 | Reports: 18 | MITRE ATT&CK: 19 | - TA0001:T1195 20 | Severity: Info 21 | Description: Detects when a team is modified in some way, such as adding a new team, deleting a team, modifying members, or a change in repository control. 22 | Detection: 23 | - Key: action 24 | Condition: IsIn 25 | Values: 26 | - 'team.add_member' 27 | - 'team.add_repository' 28 | - 'team.change_parent_team' 29 | - 'team.create' 30 | - 'team.destroy' 31 | - 'team.remove_member' 32 | - 'team.remove_repository' 33 | Tests: 34 | - Name: GitHub - Team Deleted 35 | ExpectedResult: true 36 | Log: 37 | { 38 | 'actor': 'cat', 39 | 'action': 'team.destroy', 40 | 'created_at': 1621305118553, 41 | 'data': { 'team': 'my-org/my-team' }, 42 | 'org': 'my-org', 43 | 'p_log_type': 'GitHub.Audit', 44 | 'repo': 'my-org/my-repo', 45 | } 46 | - Name: GitHub - Team Created 47 | ExpectedResult: true 48 | Log: 49 | { 50 | 'actor': 'cat', 51 | 'action': 'team.create', 52 | 'created_at': 1621305118553, 53 | 'data': { 'team': 'my-org/my-team' }, 54 | 'org': 'my-org', 55 | 'p_log_type': 'GitHub.Audit', 56 | 'repo': 'my-org/my-repo', 57 | } 58 | - Name: GitHub - Team Add repository 59 | ExpectedResult: true 60 | Log: 61 | { 62 | 'actor': 'cat', 63 | 'action': 'team.add_repository', 64 | 'created_at': 1621305118553, 65 | 'data': { 'team': 'my-org/my-team' }, 66 | 'org': 'my-org', 67 | 'p_log_type': 'GitHub.Audit', 68 | 'repo': 'my-org/my-repo', 69 | } 70 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.AbsoluteCondition.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Test.Absolute.Condition' 3 | Enabled: false 4 | LogTypes: 5 | - AWS.VPCFlow 6 | Severity: Low 7 | Detection: 8 | - Condition: AlwaysTrue 9 | - Condition: AlwaysFalse 10 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.Combinators.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Test.Combinators' 3 | Enabled: false 4 | LogTypes: 5 | - AWS.VPCFlow 6 | Severity: Low 7 | Detection: 8 | - All: 9 | - Key: flurb 10 | Condition: Equals 11 | Value: asdfasdf 12 | - Key: bar 13 | Condition: Equals 14 | Value: blah 15 | - Any: 16 | - DeepKey: 17 | - abc 18 | - def 19 | Condition: Equals 20 | Value: 123 21 | - DeepKey: 22 | - abc 23 | - def 24 | Condition: Equals 25 | Value: 456 26 | - OnlyOne: 27 | - Key: flargle 28 | Condition: Equals 29 | Value: 1234 30 | - Key: blurgle 31 | Condition: Equals 32 | Value: 999999 33 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.Extra.Top.Level.Keys.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Test.Path.Specifiers' 3 | Enabled: false 4 | LogTypes: 5 | - AWS.VPCFlow 6 | Severity: Low 7 | Detection: 8 | - KeyPath: abc.def 9 | Condition: Equals 10 | Value: 123 11 | - KeyPath: fed.cbc 12 | Condition: IsIn 13 | Values: 14 | - 123 15 | - 456 16 | - KeyPath: one.two.three[0] 17 | Condition: DoesNotExist 18 | - KeyPath: some.list 19 | Condition: AnyElement 20 | Expressions: 21 | - KeyPath: three.two.one 22 | Condition: Equals 23 | Value: heylo 24 | - KeyPath: x.y.z 25 | Condition: IsIn 26 | Values: 27 | - one 28 | - two 29 | - Condition: Equals 30 | Values: 31 | - KeyPath: some.list 32 | - Key: someOtherList 33 | ThisIsNotARealTopLevelKey: 34 | - KeyPath: abc.def 35 | Condition: Equals 36 | Value: 123 37 | - someKey: 'blah blah' -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.ListComprehension.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Test.List.Comprehension' 3 | Enabled: false 4 | LogTypes: 5 | - AWS.VPCFlow 6 | Severity: Low 7 | Detection: 8 | - Key: 'someList' 9 | Condition: AnyElement 10 | Expressions: 11 | - Key: action 12 | Condition: Equals 13 | Value: ADD 14 | - Key: member 15 | Condition: IsIn 16 | Values: 17 | - allUsers 18 | - allAuthenticatedUsers 19 | - Key: 'someList' 20 | Condition: NoElement 21 | Expressions: 22 | - Key: foo 23 | Condition: Equals 24 | Value: test 25 | - Key: bar 26 | Condition: IsIn 27 | Values: 28 | - baz 29 | - quz 30 | Tests: 31 | - Name: Any - all matches 32 | ExpectedResult: true 33 | Log: { 'someList': [{ 'action': 'ADD', 'member': 'allUsers', 'foo': 'boo', 'bar': 'blah' }] } 34 | - Name: Any - one matches 35 | ExpectedResult: false 36 | Log: { 'someList': [{ 'action': 'ADD', 'member': '', 'foo': 'boo', 'bar': 'blah' }] } 37 | - Name: NoElement - element matches all expressions 38 | ExpectedResult: false 39 | Log: { 'someList': [{ 'action': 'ADD', 'member': 'allUsers', 'foo': 'test', 'bar': 'baz' }] } 40 | - Name: NoElement - element matches one of the expressions 41 | ExpectedResult: true 42 | Log: { 'someList': [{ 'action': 'ADD', 'member': 'allUsers', 'foo': 'test', 'bar': 'blah' }] } 43 | - Name: NoElement - element does not match any expression 44 | ExpectedResult: true 45 | Log: { 'someList': [{ 'action': 'ADD', 'member': 'allUsers', 'foo': 'boo', 'bar': 'blah' }] } 46 | - Name: NoElement - one element matches all expressions 47 | ExpectedResult: false 48 | Log: 49 | { 50 | 'someList': 51 | [ 52 | { 'action': 'ADD', 'member': 'allUsers', 'foo': 'test', 'bar': 'baz' }, 53 | { 'action': 'ADD', 'member': 'allUsers', 'foo': 'test', 'bar': 'oh hello' }, 54 | ], 55 | } 56 | - Name: NoElement - no element matches any expression 57 | ExpectedResult: true 58 | Log: 59 | { 60 | 'someList': 61 | [ 62 | { 'action': 'ADD', 'member': 'allUsers', 'foo': 'boo', 'bar': 'blah' }, 63 | { 'action': 'ADD', 'member': 'allUsers', 'foo': 'bar', 'bar': 'oh hello' }, 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.MultiMatch.Key.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Panther Labs, Inc. 2 | # 3 | # The Panther SaaS is licensed under the terms of the Panther Enterprise Subscription 4 | # Agreement available at https://panther.com/enterprise-subscription-agreement/. 5 | # All intellectual property rights in and to the Panther SaaS, including any and all 6 | # rights to access the Panther SaaS, are governed by the Panther Enterprise Subscription Agreement. 7 | 8 | AnalysisType: rule 9 | RuleID: Test.MultiMatch.Key 10 | DisplayName: EKS Audit Log based single sourceIP is generating multiple 403s 11 | Severity: High 12 | Enabled: true 13 | LogTypes: 14 | - Amazon.EKS.Audit 15 | Detection: 16 | - Values: 17 | - Key: user_id 18 | - DeepKey: 19 | - user 20 | - user_id 21 | Condition: StartsWith 22 | - Values: 23 | - Key: source_ip 24 | - DeepKey: 25 | - source_ip 26 | - source_ip_address 27 | Condition: Equals 28 | - Values: 29 | - Key: response_status 30 | - DeepKey: 31 | - response 32 | - status_code 33 | Condition: DoesNotEqual 34 | - Values: 35 | - Key: error_count 36 | - DeepKey: 37 | - history 38 | - last_error_count 39 | Condition: IsGreaterThan 40 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/Test.Numeric.Comparison.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Test.Numeric.Comparison' 3 | Enabled: false 4 | LogTypes: 5 | - AWS.VPCFlow 6 | Severity: Low 7 | Detection: 8 | - Key: someNum 9 | Condition: IsGreaterThan 10 | Value: 5 11 | - Key: someOtherNum 12 | Condition: IsLessThan 13 | Value: 10 14 | - Key: someOtherOtherNum 15 | Condition: IsGreaterThanOrEquals 16 | Value: 10 17 | - Key: someOtherOtherOtherNum 18 | Condition: IsLessThanOrEquals 19 | Value: 10 20 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/asana_team_privacy_public.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: "An Asana team's setting was changed to public to the organization (not public to internet). " 3 | DisplayName: 'Asana Team Privacy Public' 4 | Enabled: true 5 | Severity: Low 6 | # def rule(event): 7 | # return ( 8 | # event.get("event_type") == "team_privacy_settings_changed" 9 | # and deep_get(event, "details", "new_value") == "public" 10 | # ) 11 | Detection: 12 | - Key: event_type 13 | Condition: Equals 14 | Value: team_privacy_settings_changed 15 | - DeepKey: 16 | - details 17 | - new_value 18 | Condition: Equals 19 | Value: public 20 | Tests: 21 | - ExpectedResult: true 22 | Log: 23 | actor: 24 | actor_type: user 25 | email: homer.simpson@panther.io 26 | gid: '12345' 27 | name: Homer Simpson 28 | context: 29 | client_ip_address: 12.12.12.12 30 | context_type: web 31 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 32 | created_at: '2022-12-16 19:35:21.026' 33 | details: 34 | new_value: public 35 | event_category: access_control 36 | event_type: team_privacy_settings_changed 37 | gid: '12345' 38 | resource: 39 | gid: '12345' 40 | name: Example Team Name 41 | resource_type: team 42 | p_log_type: Asana.Audit 43 | Name: Team made public 44 | - ExpectedResult: false 45 | Log: 46 | actor: 47 | actor_type: user 48 | email: homer.simpsons@simpsons.com 49 | gid: '1234567890' 50 | name: Homer Simpson 51 | context: 52 | client_ip_address: 1.2.3.4 53 | context_type: web 54 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 55 | created_at: '2023-02-01 18:05:08.413' 56 | details: 57 | method: 58 | - SAML 59 | event_category: logins 60 | event_type: user_login_succeeded 61 | gid: '123456789' 62 | resource: 63 | email: homer.simpsons@simpsons.com 64 | gid: '1234567890' 65 | name: Homer Simpson 66 | resource_type: user 67 | p_log_type: Asana.Audit 68 | Name: Other event 69 | DedupPeriodMinutes: 60 70 | LogTypes: 71 | - Asana.Audit 72 | RuleID: 'Asana.Team.Privacy.Public' 73 | Threshold: 1 74 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/asana_workspace_saml_optional.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: An Asana user made SAML optional for your organization. 3 | DisplayName: 'Asana Workspace SAML Optional' 4 | Enabled: true 5 | Runbook: Confirm this user acted with valid business intent and determine whether this activity was authorized. 6 | Severity: Medium 7 | # def rule(event): 8 | # old_val = deep_get(event, "details", "old_value", default="") 9 | # new_val = deep_get(event, "details", "new_value", default="") 10 | # return all( 11 | # [ 12 | # event.get("event_type", "") == "workspace_saml_settings_changed", 13 | # old_val == "required", 14 | # new_val == "optional", 15 | # ] 16 | # ) 17 | Detection: 18 | - All: 19 | - Key: event_type 20 | Condition: Equals 21 | Value: workspace_saml_settings_changed 22 | - DeepKey: 23 | - details 24 | - old_value 25 | Condition: Equals 26 | Value: required 27 | - DeepKey: 28 | - details 29 | - new_value 30 | Condition: Equals 31 | Value: optional 32 | Tests: 33 | - ExpectedResult: false 34 | Log: 35 | actor: 36 | actor_type: user 37 | email: homer.simpson@example.io 38 | gid: '1234' 39 | name: Homer Simpson 40 | context: 41 | client_ip_address: 12.12.12.12 42 | context_type: web 43 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 44 | created_at: '2022-12-16 19:31:36.289' 45 | details: 46 | new_value: required 47 | old_value: optional 48 | event_category: admin_settings 49 | event_type: workspace_saml_settings_changed 50 | gid: '1234' 51 | resource: 52 | gid: '1234' 53 | name: example.io 54 | resource_type: email_domain 55 | Name: SAML required 56 | - ExpectedResult: true 57 | Log: 58 | actor: 59 | actor_type: user 60 | email: homer.simpson@example.io 61 | gid: '1234' 62 | name: Homer Simpson 63 | context: 64 | client_ip_address: 12.12.12.12 65 | context_type: web 66 | user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 67 | created_at: '2022-12-16 19:31:36.289' 68 | details: 69 | new_value: optional 70 | old_value: required 71 | event_category: admin_settings 72 | event_type: workspace_saml_settings_changed 73 | gid: '1234' 74 | resource: 75 | gid: '1234' 76 | name: example.io 77 | resource_type: email_domain 78 | Name: SAML optional 79 | DedupPeriodMinutes: 60 80 | LogTypes: 81 | - Asana.Audit 82 | RuleID: 'Asana.Workspace.SAML.Optional' 83 | Threshold: 1 84 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_authentication_from_crowdstrike_unmanaged_device.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | Description: Detects AWS Logins from IP addresses not found in CrowdStrike's AIP list. May indicate unmanaged device being used, or faulty CrowdStrike Sensor. 3 | DisplayName: 'AWS Authentication From CrowdStrike Unmanaged Device' 4 | Enabled: false 5 | Severity: Medium 6 | # def rule(_): 7 | # return True 8 | Detection: 9 | - Condition: AlwaysTrue 10 | Tests: 11 | - ExpectedResult: true 12 | Log: 13 | additionalEventData: 14 | LoginTo: https://console.aws.amazon.com/console/home 15 | MFAIdentifier: arn:aws:iam::12345:mfa/homer_simpson 16 | MFAUsed: 'Yes' 17 | MobileVersion: 'No' 18 | awsRegion: us-east-2 19 | eventCategory: Management 20 | eventID: '12345' 21 | eventName: ConsoleLogin 22 | eventSource: signin.amazonaws.com 23 | eventTime: '2023-01-10 20:10:41' 24 | eventType: AwsConsoleSignIn 25 | eventVersion: '1.08' 26 | managementEvent: true 27 | readOnly: false 28 | recipientAccountId: '12345' 29 | responseElements: 30 | ConsoleLogin: Success 31 | sourceIPAddress: 1.2.3.4 32 | tlsDetails: 33 | cipherSuite: ECDHE-RSA-AES128-GCM-SHA256 34 | clientProvidedHostHeader: us-east-2.signin.aws.amazon.com 35 | tlsVersion: TLSv1.2 36 | userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 37 | userIdentity: 38 | accountId: '12345' 39 | arn: arn:aws:iam::12345:user/homer_simpson 40 | principalId: ABCDEF 41 | type: IAMUser 42 | userName: homer_simpson 43 | Name: Test-d8301d 44 | DedupPeriodMinutes: 60 45 | RuleID: 'AWS.Authentication.From.CrowdStrike.Unmanaged.Device' 46 | Threshold: 1 47 | ScheduledQueries: 48 | - AWS Authentication from CrowdStrike Unmanaged Device 49 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_guardduty_high_sev_findings.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.GuardDuty.HighSeverityFinding' 3 | DisplayName: 'AWS GuardDuty High Severity Finding' 4 | Enabled: true 5 | LogTypes: 6 | - AWS.GuardDuty 7 | Tags: 8 | - AWS 9 | Severity: High 10 | DedupPeriodMinutes: 60 11 | Description: > 12 | A high-severity GuardDuty finding has been identified. 13 | Runbook: > 14 | Search related logs to understand the root cause of the activity. 15 | Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity 16 | SummaryAttributes: 17 | - severity 18 | - type 19 | - title 20 | - p_any_domain_names 21 | - p_any_aws_arns 22 | - p_any_aws_account_ids 23 | # def rule(event): 24 | # return 7.0 <= float(event.get("severity", 0)) <= 8.9 25 | Detection: 26 | - Key: severity 27 | Condition: IsGreaterThanOrEquals 28 | Value: 7.0 29 | - Key: severity 30 | Condition: IsLessThanOrEquals 31 | Value: 8.9 32 | Tests: 33 | - Name: High Sev Finding 34 | ExpectedResult: true 35 | Log: 36 | { 37 | 'schemaVersion': '2.0', 38 | 'accountId': '123456789012', 39 | 'region': 'us-east-1', 40 | 'partition': 'aws', 41 | 'arn': 'arn:aws:guardduty:us-west-2:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/90b82273685661b9318f078d0851fe9a', 42 | 'type': 'PrivilegeEscalation:IAMUser/AdministrativePermissions', 43 | 'service': 44 | { 45 | 'serviceName': 'guardduty', 46 | 'detectorId': '111111bbbbbbbbbb5555555551111111', 47 | 'action': 48 | { 49 | 'actionType': 'AWS_API_CALL', 50 | 'awsApiCallAction': 51 | { 52 | 'api': 'PutRolePolicy', 53 | 'serviceName': 'iam.amazonaws.com', 54 | 'callerType': 'Domain', 55 | 'domainDetails': { 'domain': 'cloudformation.amazonaws.com' }, 56 | 'affectedResources': 57 | { 'AWS::IAM::Role': 'arn:aws:iam::123456789012:role/IAMRole' }, 58 | }, 59 | }, 60 | 'resourceRole': 'TARGET', 61 | 'additionalInfo': {}, 62 | 'evidence': null, 63 | 'eventFirstSeen': '2020-02-14T17:59:17Z', 64 | 'eventLastSeen': '2020-02-14T17:59:17Z', 65 | 'archived': false, 66 | 'count': 1, 67 | }, 68 | 'severity': 8, 69 | 'id': 'eeb88ab56556eb7771b266670dddee5a', 70 | 'createdAt': '2020-02-14T18:12:22.316Z', 71 | 'updatedAt': '2020-02-14T18:12:22.316Z', 72 | 'title': 'Principal AssumedRole:IAMRole attempted to add a policy to themselves that is highly permissive.', 73 | 'description': 'Principal AssumedRole:IAMRole attempted to add a highly permissive policy to themselves.', 74 | } 75 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_guardduty_low_sev_findings.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.GuardDuty.LowSeverityFinding' 3 | DisplayName: 'AWS GuardDuty Low Severity Finding' 4 | Enabled: true 5 | LogTypes: 6 | - AWS.GuardDuty 7 | Tags: 8 | - AWS 9 | Severity: Low 10 | DedupPeriodMinutes: 480 # 8 hours 11 | Description: > 12 | A low-severity GuardDuty finding has been identified. 13 | Runbook: > 14 | Search related logs to understand the root cause of the activity. 15 | Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity 16 | SummaryAttributes: 17 | - severity 18 | - type 19 | - title 20 | - p_any_domain_names 21 | - p_any_aws_arns 22 | - p_any_aws_account_ids 23 | # def rule(event): 24 | # return 0.1 <= float(event.get("severity", 0)) <= 3.9 25 | Detection: 26 | - Key: severity 27 | Condition: IsGreaterThanOrEquals 28 | Value: 0.1 29 | - Key: severity 30 | Condition: IsLessThanOrEquals 31 | Value: 3.9 32 | Tests: 33 | - Name: Low Sev Finding 34 | ExpectedResult: true 35 | Log: 36 | { 37 | 'schemaVersion': '2.0', 38 | 'accountId': '123456789012', 39 | 'region': 'us-east-1', 40 | 'partition': 'aws', 41 | 'arn': 'arn:aws:guardduty:us-west-2:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/90b82273685661b9318f078d0851fe9a', 42 | 'type': 'PrivilegeEscalation:IAMUser/AdministrativePermissions', 43 | 'service': 44 | { 45 | 'serviceName': 'guardduty', 46 | 'detectorId': '111111bbbbbbbbbb5555555551111111', 47 | 'action': 48 | { 49 | 'actionType': 'AWS_API_CALL', 50 | 'awsApiCallAction': 51 | { 52 | 'api': 'PutRolePolicy', 53 | 'serviceName': 'iam.amazonaws.com', 54 | 'callerType': 'Domain', 55 | 'domainDetails': { 'domain': 'cloudformation.amazonaws.com' }, 56 | 'affectedResources': 57 | { 'AWS::IAM::Role': 'arn:aws:iam::123456789012:role/IAMRole' }, 58 | }, 59 | }, 60 | 'resourceRole': 'TARGET', 61 | 'additionalInfo': {}, 62 | 'evidence': null, 63 | 'eventFirstSeen': '2020-02-14T17:59:17Z', 64 | 'eventLastSeen': '2020-02-14T17:59:17Z', 65 | 'archived': false, 66 | 'count': 1, 67 | }, 68 | 'severity': 1, 69 | 'id': 'eeb88ab56556eb7771b266670dddee5a', 70 | 'createdAt': '2020-02-14T18:12:22.316Z', 71 | 'updatedAt': '2020-02-14T18:12:22.316Z', 72 | 'title': 'Principal AssumedRole:IAMRole attempted to add a policy to themselves that is highly permissive.', 73 | 'description': 'Principal AssumedRole:IAMRole attempted to add a highly permissive policy to themselves.', 74 | } 75 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_s3_unauthenticated_access.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.S3.ServerAccess.Unauthenticated' 3 | DisplayName: 'AWS S3 Unauthenticated Access' 4 | Enabled: false 5 | LogTypes: 6 | - AWS.S3ServerAccess 7 | Tags: 8 | - AWS 9 | - Configuration Required 10 | - Security Control 11 | - Collection:Data From Cloud Storage Object 12 | Reports: 13 | MITRE ATT&CK: 14 | - TA0009:T1530 15 | Severity: Low 16 | Description: > 17 | Checks for S3 access attempts where the requester is not an authenticated AWS user. 18 | Runbook: > 19 | If unauthenticated S3 access is not expected for this bucket, update its access policies. 20 | SummaryAttributes: 21 | - bucket 22 | - key 23 | - requester 24 | # # A list of buckets where authenticated access is expected 25 | # AUTH_BUCKETS = {"example-bucket"} 26 | # 27 | # def rule(event): 28 | # return event.get("bucket") in AUTH_BUCKETS and not event.get("requester") 29 | Detection: 30 | - Key: bucket 31 | Condition: IsIn 32 | Values: 33 | - example-bucket 34 | - Key: requester 35 | Condition: IsNullOrEmpty 36 | Tests: 37 | - Name: Authenticated Access 38 | ExpectedResult: false 39 | Log: 40 | { 41 | 'bucket': 'example-bucket', 42 | 'requester': '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be', 43 | } 44 | - Name: Unauthenticated Access 45 | ExpectedResult: true 46 | Log: { 'bucket': 'example-bucket' } 47 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_vpc_inbound_traffic_port_allowlist.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.VPC.InboundPortWhitelist' 3 | DisplayName: 'VPC Flow Logs Inbound Port Allowlist' 4 | Enabled: false 5 | LogTypes: 6 | - AWS.VPCFlow 7 | Tags: 8 | - AWS 9 | - Configuration Required 10 | - Security Control 11 | - Command and Control:Non-Standard Port 12 | Reports: 13 | MITRE ATT&CK: 14 | - TA0011:T1571 15 | Severity: High 16 | Description: > 17 | VPC Flow Logs observed inbound traffic violating the port allowlist. 18 | Runbook: > 19 | Block the unapproved traffic, or update the approved ports list. 20 | SummaryAttributes: 21 | - srcaddr 22 | - dstaddr 23 | - dstport 24 | # APPROVED_PORTS = { 25 | # 80, 26 | # 443, 27 | # } 28 | # 29 | # 30 | # def rule(event): 31 | # # Can't perform this check without a destination port 32 | # if "dstport" not in event: 33 | # return False 34 | # 35 | # # Only monitor for non allowlisted ports 36 | # if event.get("dstport") in APPROVED_PORTS: 37 | # return False 38 | # 39 | # # Only monitor for traffic coming from non-private IP space 40 | # # 41 | # # Defaults to True (no alert) if 'srcaddr' key is not present 42 | # if ip_network(event.get("srcaddr", "0.0.0.0/32")).is_private: 43 | # return False 44 | # 45 | # # Alert if the traffic is destined for internal IP addresses 46 | # # 47 | # # Defaults to False (no alert) if 'dstaddr' key is not present 48 | # return ip_network(event.get("dstaddr", "1.0.0.0/32")).is_private 49 | 50 | # Requires https://app.asana.com/0/1202324455056256/1204671191731202/f for fix 51 | 52 | Detection: 53 | - Key: dstport 54 | Condition: Exists 55 | - Key: dstport 56 | Condition: IsNotIn 57 | Values: 58 | - 80 59 | - 443 60 | - Key: srcaddr 61 | Condition: IsIPAddressPublic 62 | Value: true 63 | - Key: dstaddr 64 | Condition: IsIPAddressPrivate 65 | Value: true 66 | Tests: 67 | - Name: Public to Private IP on Restricted Port 68 | ExpectedResult: true 69 | Log: { 'dstport': 22, 'dstaddr': '10.0.0.1', 'srcaddr': '1.1.1.1' } 70 | - Name: Public to Private IP on Allowed Port 71 | ExpectedResult: false 72 | Log: { 'dstport': 443, 'dstaddr': '10.0.0.1', 'srcaddr': '1.1.1.1' } 73 | - Name: Private to Private IP on Restricted Port 74 | ExpectedResult: false 75 | Log: { 'dstport': 22, 'dstaddr': '10.0.0.1', 'srcaddr': '10.10.10.1' } 76 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/aws_vpc_unapproved_outbound_dns.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'AWS.VPC.UnapprovedOutboundDNS' 3 | DisplayName: 'VPC Flow Logs Unapproved Outbound DNS Traffic' 4 | Enabled: false 5 | LogTypes: 6 | - AWS.VPCFlow 7 | Tags: 8 | - AWS 9 | - Configuration Required 10 | - Security Control 11 | - Command and Control:Application Layer Protocol 12 | Reports: 13 | MITRE ATT&CK: 14 | - TA0011:T1071 15 | Severity: Medium 16 | Description: > 17 | Alerts if outbound DNS traffic is detected to a non-approved DNS server. DNS is often used as a means to exfiltrate data or perform command and control for compromised hosts. All DNS traffic should be routed through internal DNS servers or trusted 3rd parties. 18 | Runbook: > 19 | Investigate the host sending unapproved DNS activity for signs of compromise or other malicious activity. Update network configurations appropriately to ensure all DNS traffic is routed to approved DNS servers. 20 | SummaryAttributes: 21 | - srcaddr 22 | - dstaddr 23 | - dstport 24 | # APPROVED_DNS_SERVERS = { 25 | # "1.1.1.1", # CloudFlare DNS 26 | # "8.8.8.8", # Google DNS 27 | # } 28 | # 29 | # 30 | # def rule(event): 31 | # # Common DNS ports, for better security use an application layer aware network monitor 32 | # # 33 | # # Defaults to True (no alert) if 'dstport' key is not present 34 | # if event.get("dstport") != 53 and event.get("dstport") != 5353: 35 | # return False 36 | # 37 | # # Only monitor traffic that is originating internally 38 | # # 39 | # # Defaults to True (no alert) if 'srcaddr' key is not present 40 | # if not ip_network(event.get("srcaddr", "0.0.0.0/32")).is_private: 41 | # return False 42 | # 43 | # # No clean way to default to False (no alert), so explicitly check for key 44 | # return "dstaddr" in event and event.get("dstaddr") not in APPROVED_DNS_SERVERS 45 | 46 | # Requires https://app.asana.com/0/1202324455056256/1204671191731202/f for fix 47 | Detection: 48 | - Key: dstport 49 | Condition: IsIn 50 | Values: 51 | - 53 52 | - 5353 53 | - Key: srcaddr 54 | Condition: IsIPAddressPrivate 55 | Value: true 56 | - Key: dstaddr 57 | Condition: Exists 58 | - Key: dstaddr 59 | Condition: IsNotIn 60 | Values: 61 | - 1.1.1.1 62 | - 8.8.8.8 63 | Tests: 64 | - Name: Approved Outbound DNS Traffic 65 | ExpectedResult: false 66 | Log: { 'dstport': 53, 'dstaddr': '1.1.1.1', 'srcaddr': '10.0.0.1' } 67 | - Name: Unapproved Outbound DNS Traffic 68 | ExpectedResult: true 69 | Log: { 'dstport': 53, 'dstaddr': '100.100.100.100', 'srcaddr': '10.0.0.1' } 70 | - Name: Outbound Non-DNS Traffic 71 | ExpectedResult: false 72 | Log: { 'dstport': 80, 'dstaddr': '100.100.100.100', 'srcaddr': '10.0.0.1' } 73 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/box_user_downloads.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Box.Large.Number.Downloads' 3 | DisplayName: 'Box Large Number of Downloads' 4 | Enabled: true 5 | LogTypes: 6 | - Box.Event 7 | Tags: 8 | - Box 9 | - Exfiltration:Exfiltration Over Web Service 10 | Reports: 11 | MITRE ATT&CK: 12 | - TA0010:T1567 13 | Severity: Low 14 | Description: > 15 | A user has exceeded the threshold for number of downloads within a single time frame. 16 | Reference: https://developer.box.com/reference/resources/event/ 17 | Runbook: > 18 | Investigate whether this user's download activity is expected. Investigate the cause of this download activity. 19 | SummaryAttributes: 20 | - ip_address 21 | Threshold: 100 22 | DedupPeriodMinutes: 60 23 | # def rule(event): 24 | # return event.get("event_type") == "DOWNLOAD" 25 | Detection: 26 | - Key: event_type 27 | Condition: IEquals # insensitive for more test cases 28 | Value: DOWNLOAD 29 | Tests: 30 | - Name: Regular Event 31 | ExpectedResult: false 32 | Log: 33 | { 34 | 'type': 'event', 35 | 'additional_details': '{"key": "value"}', 36 | 'created_by': 37 | { 'id': '12345678', 'type': 'user', 'login': 'cat@example', 'name': 'Bob Cat' }, 38 | 'event_type': 'DELETE', 39 | } 40 | - Name: User Download 41 | ExpectedResult: true 42 | Log: 43 | { 44 | 'type': 'event', 45 | 'additional_details': '{"key": "value"}', 46 | 'created_by': 47 | { 'id': '12345678', 'type': 'user', 'login': 'cat@example', 'name': 'Bob Cat' }, 48 | 'event_type': 'DOWNLOAD', 49 | 'source': { 'id': '12345678', 'type': 'user', 'login': 'user@example', 'name': 'Bob Cat' }, 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/gsuite_leaked_password.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'GSuite.LeakedPassword' 3 | DisplayName: 'GSuite User Password Leaked' 4 | Enabled: true 5 | LogTypes: 6 | - GSuite.ActivityEvent 7 | Tags: 8 | - GSuite 9 | - Credential Access:Unsecured Credentials 10 | Reports: 11 | MITRE ATT&CK: 12 | - TA0006:T1552 13 | Severity: High 14 | Description: > 15 | GSuite reported a user's password has been compromised, so they disabled the account. 16 | Reference: https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login#account_disabled_password_leak 17 | Runbook: > 18 | GSuite has already disabled the compromised user's account. Consider investigating how the user's account was compromised, and reset their account and password. Advise the user to change any other passwords in use that are the sae as the compromised password. 19 | SummaryAttributes: 20 | - actor:email 21 | # PASSWORD_LEAKED_EVENTS = { 22 | # "account_disabled_password_leak", 23 | # } 24 | # 25 | # 26 | # def rule(event): 27 | # if deep_get(event, "id", "applicationName") != "login": 28 | # return False 29 | # 30 | # if event.get("type") == "account_warning": 31 | # return bool(event.get("name") in PASSWORD_LEAKED_EVENTS) 32 | # return False 33 | Detection: 34 | - DeepKey: 35 | - id 36 | - applicationName 37 | Condition: Equals 38 | Value: login 39 | - Key: type 40 | Condition: Equals 41 | Value: account_warning 42 | - Key: name 43 | Condition: IsIn 44 | Values: 45 | - account_disabled_password_leak 46 | Tests: 47 | - Name: Normal Login Event 48 | ExpectedResult: false 49 | Log: 50 | { 51 | 'id': { 'applicationName': 'login' }, 52 | 'type': 'login', 53 | 'name': 'logout', 54 | 'parameters': { 'login_type': 'saml' }, 55 | } 56 | - Name: Account Warning Not For Password Leaked 57 | ExpectedResult: false 58 | Log: 59 | { 60 | 'id': { 'applicationName': 'login' }, 61 | 'type': 'account_warning', 62 | 'name': 'account_disabled_spamming', 63 | 'parameters': { 'affected_email_address': 'homer.simpson@example.com' }, 64 | } 65 | - Name: Account Warning For Password Leaked 66 | ExpectedResult: true 67 | Log: 68 | { 69 | 'id': { 'applicationName': 'login' }, 70 | 'type': 'account_warning', 71 | 'name': 'account_disabled_password_leak', 72 | 'parameters': { 'affected_email_address': 'homer.simpson@example.com' }, 73 | } 74 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/onelogin_high_risk_failed_login.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'OneLogin.HighRiskFailedLogin' 3 | DisplayName: 'OneLogin Failed High Risk Login' 4 | Enabled: true 5 | LogTypes: 6 | - OneLogin.Events 7 | Tags: 8 | - OneLogin 9 | Severity: Low 10 | Description: A OneLogin attempt with a high risk factor (>50) resulted in a failed authentication. 11 | Reference: https://developers.onelogin.com/api-docs/1/events/event-resource 12 | Runbook: Investigate why this user login is tagged as high risk as well as whether this was caused by expected user activity. 13 | SummaryAttributes: 14 | - account_id 15 | - user_name 16 | - user_id 17 | # def rule(event): 18 | # 19 | # # check risk associated with this event 20 | # if event.get("risk_score", 0) > 50: 21 | # # a failed authentication attempt with high risk 22 | # return event.get("event_type_id") == 6 23 | # return False 24 | Detection: 25 | - Key: risk_score 26 | Condition: Exists 27 | - Key: risk_score 28 | Condition: IsGreaterThan 29 | Value: 50 30 | - Key: event_type_id 31 | Condition: Equals 32 | Value: 6 33 | Tests: 34 | - Name: Normal Login Event 35 | ExpectedResult: false 36 | Log: 37 | { 38 | 'event_type_id': 6, 39 | 'actor_user_id': 123456, 40 | 'actor_user_name': 'Bob Cat', 41 | 'user_id': 123456, 42 | 'user_name': 'Bob Cat', 43 | } 44 | - Name: Failed High Risk Login 45 | ExpectedResult: true 46 | Log: 47 | { 48 | 'event_type_id': 6, 49 | 'risk_score': 55, 50 | 'actor_user_id': 123456, 51 | 'actor_user_name': 'Bob Cat', 52 | 'user_id': 123456, 53 | 'user_name': 'Bob Cat', 54 | } 55 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/onelogin_password_accessed.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'OneLogin.PasswordAccess' 3 | DisplayName: 'OneLogin Password Access' 4 | Enabled: true 5 | LogTypes: 6 | - OneLogin.Events 7 | Tags: 8 | - OneLogin 9 | - Credential Access:Unsecured Credentials 10 | Reports: 11 | MITRE ATT&CK: 12 | - TA0006:T1552 13 | Severity: Medium 14 | Description: > 15 | User accessed another user's application password 16 | Reference: https://developers.onelogin.com/api-docs/1/events/event-resource 17 | Runbook: > 18 | Investigate whether this was authorized access. 19 | SummaryAttributes: 20 | - account_id 21 | - user_name 22 | - user_id 23 | # def rule(event): 24 | # 25 | # # Filter events; event type 240 is actor_user revealed user's app password 26 | # if ( 27 | # event.get("event_type_id") != 240 28 | # or not event.get("actor_user_id") 29 | # or not event.get("user_id") 30 | # ): 31 | # return False 32 | # 33 | # # Determine if actor_user accessed another user's password 34 | # return event.get("actor_user_id") != event.get("user_id") 35 | Detection: 36 | - Key: event_type_id 37 | Condition: Equals 38 | Value: 240 39 | - Key: actor_user_id 40 | Condition: Exists 41 | - Key: user_id 42 | Condition: Exists 43 | - Condition: DoesNotEqual 44 | Values: 45 | - Key: user_id 46 | - Key: actor_user_id 47 | Tests: 48 | - Name: User accessed their own password 49 | ExpectedResult: false 50 | Log: 51 | { 52 | 'event_type_id': 240, 53 | 'actor_user_id': 123456, 54 | 'actor_user_name': 'Bob Cat', 55 | 'user_id': 123456, 56 | 'user_name': 'Bob Cat', 57 | } 58 | - Name: User accessed another user's password 59 | ExpectedResult: true 60 | Log: 61 | { 62 | 'event_type_id': 240, 63 | 'actor_user_id': 654321, 64 | 'actor_user_name': 'Mountain Lion', 65 | 'user_id': 123456, 66 | 'user_name': 'Bob Cat', 67 | } 68 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/onelogin_user_account_locked.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'OneLogin.UserAccountLocked' 3 | DisplayName: 'OneLogin User Locked' 4 | Enabled: true 5 | LogTypes: 6 | - OneLogin.Events 7 | Tags: 8 | - OneLogin 9 | - Credential Access:Brute Force 10 | Reports: 11 | MITRE ATT&CK: 12 | - TA0006:T1110 13 | Severity: Low 14 | Description: > 15 | User locked or suspended from their account. 16 | Reference: https://developers.onelogin.com/api-docs/1/events/event-resource 17 | Runbook: > 18 | Investigate whether this was caused by expected action. 19 | SummaryAttributes: 20 | - account_id 21 | - event_type_id 22 | - user_name 23 | - user_id 24 | # def rule(event): 25 | # 26 | # # check for a user locked event 27 | # # event 531 and 553 are user lock events via api 28 | # # event 551 is user suspended via api 29 | # return event.get("event_type_id") in [531, 553, 551] 30 | Detection: 31 | - Key: event_type_id 32 | Condition: IsIn 33 | Values: 34 | - 531 35 | - 553 36 | - 551 37 | Tests: 38 | - Name: User account locked via api - first method. 39 | ExpectedResult: true 40 | Log: 41 | { 42 | 'event_type_id': 531, 43 | 'actor_user_id': 123456, 44 | 'actor_user_name': 'Bob Cat', 45 | 'user_id': 123456, 46 | 'user_name': 'Bob Cat', 47 | } 48 | - Name: User account locked via api - second method. 49 | ExpectedResult: true 50 | Log: 51 | { 52 | 'event_type_id': 553, 53 | 'actor_user_id': 654321, 54 | 'actor_user_name': 'Mountain Lion', 55 | 'user_id': 123456, 56 | 'user_name': 'Bob Cat', 57 | } 58 | - Name: User account suspended via api. 59 | ExpectedResult: true 60 | Log: 61 | { 62 | 'event_type_id': 551, 63 | 'actor_user_id': 654321, 64 | 'actor_user_name': 'Mountain Lion', 65 | 'user_id': 123456, 66 | 'user_name': 'Bob Cat', 67 | } 68 | - Name: Normal User Activated Event 69 | ExpectedResult: false 70 | Log: 71 | { 72 | 'event_type_id': 11, 73 | 'actor_user_id': 654321, 74 | 'actor_user_name': 'Mountain Lion', 75 | 'user_id': 123456, 76 | 'user_name': 'Bob Cat', 77 | } 78 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/snowflake_login_without_mfa.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | RuleID: 'Snowflake.LoginWithoutMFA' 3 | Description: > 4 | Detect snowflake logins without multifactor authentication 5 | DisplayName: 'Snowflake Login Without MFA' 6 | Enabled: false 7 | ScheduledQueries: 8 | - Query.Snowflake.MFALogin 9 | Tags: 10 | - Snowflake 11 | - Defense Evasion:Modify Authentication Process 12 | Reports: 13 | MITRE ATT&CK: 14 | - TA0005:T1556 15 | Severity: Medium 16 | # MFA_EXCEPTIONS = { 17 | # "PANTHER_READONLY", 18 | # "PANTHER_ADMIN" 19 | #} 20 | # 21 | #def rule(event): 22 | # return event.get("user_name", "") not in MFA_EXCEPTIONS 23 | # 24 | Detection: 25 | - Key: user_name 26 | Condition: IsNotIn 27 | Values: 28 | - PANTHER_READONLY 29 | - PANTHER_ADMIN 30 | Tests: 31 | - Name: Return True 32 | ExpectedResult: true 33 | Log: 34 | Anything: any value 35 | - Name: True Exception 36 | ExpectedResult: false 37 | Log: 38 | user_name: PANTHER_READONLY 39 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/test.enrichment.rule.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | LogTypes: 3 | - AWS.VPCFlow 4 | RuleID: basic.enrichment.rule 5 | Enabled: true 6 | Severity: High 7 | Detection: 8 | - Enrichment: 9 | Table: ipinfo_location 10 | Selector: srcAddr 11 | FieldPath: city 12 | Condition: Equals 13 | Value: Idaho Falls 14 | Tests: 15 | - Name: alerts 16 | ExpectedResult: true 17 | Log: 18 | { 19 | "p_enrichment": { 20 | "ipinfo_location": { 21 | "srcAddr": { 22 | "city": "Idaho Falls", 23 | "country": "US", 24 | "lat": "43.5518", 25 | "lng": "-111.8919", 26 | "p_match": "75.174.152.196", 27 | "postal_code": "83401", 28 | "region": "Idaho", 29 | "region_code": "ID", 30 | "timezone": "America/Boise" 31 | } 32 | } 33 | }, 34 | "p_log_type": "AWS.VPCFlow", 35 | "srcAddr": "75.174.152.196" 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/test.rule.with.dynamic.funcs.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'basic.rule.with.dynamic.funcs' 3 | Detection: 4 | - Key: userAgent 5 | Condition: Equals 6 | Value: Max 7 | InlineFilters: 8 | - KeyPath: actionName 9 | Condition: Equals 10 | Value: Beans 11 | AlertTitle: The user agent was {userAgent} 12 | DynamicSeverities: 13 | - ChangeTo: High 14 | Conditions: 15 | - Key: actionName 16 | Condition: Equals 17 | Value: Beans 18 | AlertContext: 19 | - KeyName: foo 20 | KeyValue: 21 | Key: actionName 22 | GroupBy: 23 | - Key: actionName 24 | - KeyPath: userAgent 25 | DedupPeriodMinutes: 60 26 | DisplayName: 'basic.rule' 27 | Enabled: true 28 | LogTypes: 29 | - Panther.Audit 30 | Severity: Medium 31 | Tests: 32 | - Name: alerts 33 | ExpectedResult: true 34 | Log: { 'userAgent': 'Max', 'actionName': 'Beans' } 35 | - Name: no alerts - wrong userAgent 36 | ExpectedResult: false 37 | Log: { 'userAgent': 'John' } 38 | -------------------------------------------------------------------------------- /tests/fixtures/simple-detections/valid/vpc_dns_tunneling.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: scheduled_rule 2 | RuleID: 'VPC.DNS.Tunneling' 3 | DisplayName: 'VPC DNS Tunneling' 4 | Description: > 5 | Detect dns tunneling traffic using a scheduled query 6 | Reports: 7 | MITRE ATT&CK: 8 | - TA0005:T1599 9 | Tags: 10 | - Defense Evasion:Network Boundary Bridging 11 | Enabled: false 12 | ScheduledQueries: 13 | - Query.VPC.DNS.Tunneling 14 | Severity: Medium 15 | # def rule(_): 16 | # return True 17 | # 18 | Detection: 19 | - Condition: AlwaysTrue 20 | Tests: 21 | - Name: Value Returned By Query 22 | ExpectedResult: true 23 | Log: 24 | Anything: any value 25 | -------------------------------------------------------------------------------- /tests/fixtures/tests_can_be_inherited/base.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | if event.get("operationName") == "Sign-in activity": 3 | return True 4 | return False 5 | -------------------------------------------------------------------------------- /tests/fixtures/tests_can_be_inherited/base.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Description: This rule alerts for suspicious login activity 3 | DisplayName: 'Suspicious Logins' 4 | Enabled: false 5 | Runbook: Examine other activities done by this user to determine whether or not activity is suspicious. 6 | Severity: Medium 7 | DedupPeriodMinutes: 60 8 | LogTypes: 9 | - AWS.CloudTrail 10 | - Azure.Audit 11 | RuleID: 'Sus.Login.Base' 12 | Threshold: 1 13 | Filename: base.py 14 | Tests: 15 | - ExpectedResult: false 16 | Log: {} 17 | Name: t1 18 | - ExpectedResult: true 19 | Log: 20 | operationName: "Sign-in activity" 21 | Name: t2 22 | -------------------------------------------------------------------------------- /tests/fixtures/tests_can_be_inherited/derive.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Sus.Login.Derived' 3 | BaseDetection: 'Sus.Login.Base' 4 | Severity: High 5 | Enabled: true 6 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/unit/panther_analysis_tool/__init__.py -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/unit/panther_analysis_tool/backend/__init__.py -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/unit/panther_analysis_tool/command/__init__.py -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/command/test_bulk_delete.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from panther_analysis_tool.backend.client import ( 5 | BackendResponse, 6 | DeleteDetectionsResponse, 7 | DeleteSavedQueriesResponse, 8 | ) 9 | from panther_analysis_tool.backend.mocks import MockBackend 10 | from panther_analysis_tool.command.bulk_delete import ( 11 | _delete_detections_dry_run, 12 | _delete_queries_dry_run, 13 | ) 14 | 15 | 16 | class TestBulkDelete(unittest.TestCase): 17 | def test_delete_detections_dry_run(self) -> None: 18 | mock_ids = ["1", "2", "3"] 19 | backend = MockBackend() 20 | backend.delete_detections = mock.MagicMock( 21 | return_value=BackendResponse( 22 | data=DeleteDetectionsResponse(ids=mock_ids, saved_query_names=["a"]), 23 | status_code=200, 24 | ) 25 | ) 26 | 27 | code, msg = _delete_detections_dry_run(backend, mock_ids) 28 | self.assertEqual(code, 0) 29 | self.assertEqual(msg, "") 30 | 31 | def test_delete_queries_dry_run(self) -> None: 32 | mock_names = ["a", "b", "c"] 33 | backend = MockBackend() 34 | backend.delete_saved_queries = mock.MagicMock( 35 | return_value=BackendResponse( 36 | data=DeleteSavedQueriesResponse(names=mock_names, detection_ids=["1"]), 37 | status_code=200, 38 | ) 39 | ) 40 | 41 | code, msg = _delete_queries_dry_run(backend, mock_names) 42 | self.assertEqual(code, 0) 43 | self.assertEqual(msg, "") 44 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/log_schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/unit/panther_analysis_tool/log_schemas/__init__.py -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import unittest 21 | from typing import Any, Dict, List 22 | 23 | from panther_core.exceptions import PantherError 24 | 25 | 26 | class TestPantherError(unittest.TestCase): 27 | def test_equals(self) -> None: 28 | cases: List[Dict[str, Any]] = [ 29 | {"args": (("a", "b"), ("a", "b")), "expected": True}, 30 | {"args": (("a",), ("a",)), "expected": True}, 31 | {"args": (("a", "b", "c"), ("a", "b")), "expected": False}, 32 | ] 33 | for case in cases: 34 | instance_args, other_args = case["args"] 35 | instance = PantherError(*instance_args) 36 | other = PantherError(*other_args) 37 | self.assertIs(instance.equals(other), case["expected"]) 38 | # Test symmetry 39 | self.assertIs(other.equals(instance), case["expected"]) 40 | 41 | def test_has_message_prefix(self) -> None: 42 | exc = PantherError("something", "went wrong") 43 | self.assertTrue(exc.has_message_prefix("something")) 44 | self.assertFalse(exc.has_message_prefix("something went")) 45 | 46 | def test_to_string(self) -> None: 47 | exc = PantherError("generic error", "went wrong") 48 | self.assertEqual(str(exc), "generic error: went wrong") 49 | exc = PantherError("generic error") 50 | self.assertEqual(str(exc), "generic error") 51 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/test_lookup_tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panther Analysis Tool is a command line interface for writing, 3 | testing, and packaging policies/rules. 4 | Copyright (C) 2020 Panther Labs Inc 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import argparse 21 | import os 22 | from unittest import TestCase 23 | 24 | from panther_analysis_tool import main as pat 25 | 26 | FIXTURES_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../", "fixtures")) 27 | LUTS_FIXTURES_PATH = os.path.join(FIXTURES_PATH, "lookup-tables") 28 | 29 | 30 | class TestLookupTable(TestCase): # pylint: disable=too-many-public-methods 31 | def test_load_invalid_specs_from_folder(self): 32 | args = argparse.Namespace() 33 | args.path = f"{LUTS_FIXTURES_PATH}/invalid/lookup-table-1.yml" 34 | rc, file_path = pat.test_lookup_table(args) 35 | self.assertEqual(1, rc) 36 | self.assertEqual(file_path, "") 37 | 38 | def test_load_invalid_specs_from_folder(self): 39 | args = argparse.Namespace() 40 | args.path = f"{LUTS_FIXTURES_PATH}/valid/lookup-table-1.yml" 41 | rc, file_path = pat.test_lookup_table(args) 42 | self.assertEqual(0, rc) 43 | self.assertEqual(file_path, "") 44 | 45 | args.path = f"{LUTS_FIXTURES_PATH}/valid/lookup-table-2.yml" 46 | rc, file_path = pat.test_lookup_table(args) 47 | self.assertEqual(0, rc) 48 | self.assertEqual(file_path, "") 49 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/test_zip_chunker.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from panther_analysis_tool.zip_chunker import ( 4 | ChunkFiles, 5 | ZipChunk, 6 | create_additional_chunks_if_needed, 7 | ) 8 | 9 | 10 | class TestZipChunks(unittest.TestCase): 11 | def test_shared_parents_matches(self) -> None: 12 | # tests that a single file can have multiple parents 13 | chunk = ChunkFiles(ZipChunk(patterns=["*"])) 14 | chunk.add_file("file1") 15 | chunk.add_file("shared_dep", "file1") 16 | chunk.add_file("file2") 17 | chunk.add_file("shared_dep", "file2") 18 | 19 | self.assertEqual(chunk.related_files, {"file1": "shared_dep", "file2": "shared_dep"}) 20 | 21 | def test_additional_chunks_does_not_split_shared_files(self) -> None: 22 | # tests that a shared file is included in both chunks 23 | chunk = ChunkFiles(ZipChunk(patterns=["*"], max_size=1)) 24 | chunk.add_file("file1") 25 | chunk.add_file("shared_dep", "file1") 26 | chunk.add_file("file2") 27 | chunk.add_file("shared_dep", "file2") 28 | 29 | chunks = create_additional_chunks_if_needed([chunk]) 30 | 31 | self.assertEqual(len(chunks), 2) 32 | self.assertEqual(chunks[0].files, ["file1", "shared_dep"]) 33 | self.assertEqual(chunks[1].files, ["file2", "shared_dep"]) 34 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panther-labs/panther_analysis_tool/2fe70db6079f47766ba5f5ffc5664f6e3e78bcb4/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/get_specs_for_test.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from panther_analysis_tool.analysis_utils import ( 4 | LoadAnalysisSpecsResult, 5 | get_yaml_loader, 6 | ) 7 | from panther_analysis_tool.constants import AnalysisTypes 8 | 9 | 10 | def get_specs_for_test() -> typing.Dict[str, LoadAnalysisSpecsResult]: 11 | return { 12 | AnalysisTypes.RULE: LoadAnalysisSpecsResult( 13 | f"filname.rule", 14 | f"filepath.rule", 15 | get_yaml_loader(roundtrip=True).load( 16 | """ 17 | RuleID: foo.bar.rule 18 | AnalysisType: rule 19 | Tests: 20 | - Name: Test1 21 | ExpectedResult: true 22 | Log: 23 | a: event_type 24 | b: Equals 25 | c: 1234 26 | json: {"foo": "bar"} 27 | """ 28 | ), 29 | yaml_ctx=get_yaml_loader(roundtrip=True), 30 | error=None, 31 | ), 32 | AnalysisTypes.SCHEDULED_RULE: LoadAnalysisSpecsResult( 33 | f"filname.scheduled_rule", 34 | f"filepath.scheduled_rule", 35 | get_yaml_loader(roundtrip=True).load( 36 | """ 37 | RuleID: foo.bar.scheduled_rule 38 | AnalysisType: scheduled_rule 39 | Tests: 40 | - Name: Test1 41 | ExpectedResult: true 42 | Log: 43 | a: event_type 44 | b: Equals 45 | c: 1234 46 | json: {"foo": "bar"} 47 | """ 48 | ), 49 | yaml_ctx=get_yaml_loader(roundtrip=True), 50 | error=None, 51 | ), 52 | AnalysisTypes.POLICY: LoadAnalysisSpecsResult( 53 | f"filname.policy", 54 | f"filepath.policy", 55 | get_yaml_loader(roundtrip=True).load( 56 | """ 57 | PolicyID: foo.bar.policy 58 | AnalysisType: policy 59 | Tests: 60 | - Name: Test1 61 | ExpectedResult: true 62 | Resource: 63 | a: event_type 64 | b: Equals 65 | c: 1234 66 | json: {"foo": "bar"} 67 | """ 68 | ), 69 | yaml_ctx=get_yaml_loader(roundtrip=True), 70 | error=None, 71 | ), 72 | } 73 | --------------------------------------------------------------------------------