├── tests ├── __init__.py ├── unit │ ├── __init__.py │ └── panther_analysis_tool │ │ ├── __init__.py │ │ ├── backend │ │ └── __init__.py │ │ ├── command │ │ ├── __init__.py │ │ └── test_bulk_delete.py │ │ ├── core │ │ └── __init__.py │ │ ├── log_schemas │ │ └── __init__.py │ │ ├── test_lookup_tables.py │ │ ├── test_zip_chunker.py │ │ └── test_exceptions.py ├── utils │ ├── __init__.py │ └── get_specs_for_test.py └── fixtures │ ├── __init__.py │ ├── status │ ├── all_statuses │ │ ├── no_status.py │ │ ├── status_stable.py │ │ ├── status_deprecated.py │ │ ├── status_experimental.py │ │ ├── no_status.yml │ │ ├── status_stable.yml │ │ ├── status_deprecated.yml │ │ └── status_experimental.yml │ ├── status_stable │ │ ├── status_stable.py │ │ └── status_stable.yml │ ├── status_deprecated │ │ ├── status_deprecated.py │ │ └── status_deprecated.yml │ └── status_experimental │ │ ├── status_experimental.py │ │ └── status_experimental.yml │ ├── detections │ ├── debug │ │ ├── rule_that_works.py │ │ ├── rule_that_prints.py │ │ ├── rule_with_error.py │ │ ├── rule_that_works.yml │ │ ├── rule_with_error.yml │ │ └── rule_that_prints.yml │ ├── disabled_rule │ │ ├── example_rule.py │ │ ├── example_disabled_rule.py │ │ ├── example_rule.yml │ │ └── example_disabled_rule.yml │ ├── example_rule_bad_log_type.py │ ├── example_rule_set_duplicates.py │ ├── example_unhandled_exception.py │ ├── example_policy_bad_resource_type.py │ ├── example_rule_invalid_test.py │ ├── valid_analysis │ │ ├── global_helpers │ │ │ ├── helpers.py │ │ │ ├── b_helper.py │ │ │ ├── a_helper.py │ │ │ ├── a_helper.yml │ │ │ ├── b_helper.yml │ │ │ └── helpers.yml │ │ ├── rules │ │ │ ├── example_rule_global.py │ │ │ ├── example_rule_global.yml │ │ │ ├── example_rule_mocks.py │ │ │ ├── example_rule_generated_functions.py │ │ │ ├── example_rule_extraneous_fields.py │ │ │ ├── example_rule.py │ │ │ ├── example_rule_generated_functions.yml │ │ │ ├── example_rule_mocks.yml │ │ │ ├── example_rule_extraneous_fields.yml │ │ │ └── example_rule.yml │ │ ├── data_models │ │ │ ├── example_data_model.yml │ │ │ ├── GSuite.Events.DataModel.py │ │ │ ├── example_data_model_disabled.yml │ │ │ └── example_data_model_python.yml │ │ ├── packs │ │ │ └── sample-pack.yml │ │ ├── queries │ │ │ ├── query_three.yml │ │ │ ├── query_one.yml │ │ │ └── query_two.yml │ │ ├── policies │ │ │ ├── example_policy_beta.py │ │ │ ├── example_policy.py │ │ │ ├── example_policy_extraneous_fields.py │ │ │ ├── example_policy_generated_functions.py │ │ │ ├── example_policy_extraneous_fields.yml │ │ │ ├── example_policy_beta.yml │ │ │ ├── example_policy_generated_functions.yml │ │ │ └── example_policy.yml │ │ ├── scheduled_rules │ │ │ ├── example_scheduled_rule.py │ │ │ └── example_scheduled_rule.yml │ │ └── advanced_rules │ │ │ ├── example_rule_data_model.py │ │ │ └── example_rule_data_model.yml │ ├── .panther_settings.yml │ ├── example_unhandled_exception_on_import.py │ ├── example_policy_import.py │ ├── destinations │ │ ├── example_available_destination_name.py │ │ └── example_available_destination_name.yml │ ├── example_invalid_pack.yml │ ├── example_rule_invalid_mocks.py │ ├── aws_globals.py │ ├── example_data_model_conflict.yml │ ├── example_policy_missing_policy_file.yml │ ├── example_policy_import.yml │ ├── aws_globals.yml │ ├── example_policy.py │ ├── example_policy_invalid_characters.py │ ├── example_policy_required_tests.py │ ├── example_policy_set_duplicates.py │ ├── example_unhandled_exception.yml │ ├── example_rule_invalid_test.yml │ ├── example_rule_bad_log_type.yml │ ├── example_policy_bad_resource_type.yml │ ├── example_strict_invalid_yaml.yml │ ├── example_rule.py │ ├── example_rule_set_duplicates.yml │ ├── example_rule_invalid_mocks.yml │ ├── example_rule_missing_field.yml │ ├── example_malformed_yaml.yml │ ├── example_policy_required_tests.yml │ ├── example_malformed_policy.yml │ ├── example_policy.json │ ├── example_policy_set_duplicates.yml │ ├── example_ignored.yml │ ├── example_ignored_multi.yml │ ├── example_policy.yml │ └── example_policy_invalid_characters.yml │ ├── check-packs │ ├── missing-dependencies │ │ ├── global_helpers │ │ │ ├── helpers.py │ │ │ ├── b_helper.py │ │ │ ├── a_helper.py │ │ │ ├── a_helper.yml │ │ │ ├── b_helper.yml │ │ │ └── helpers.yml │ │ ├── rules │ │ │ ├── example_rule_global.py │ │ │ ├── example_rule_global.yml │ │ │ ├── example_rule_mocks.py │ │ │ ├── example_rule_generated_functions.py │ │ │ ├── example_rule_extraneous_fields.py │ │ │ ├── example_rule.py │ │ │ ├── example_rule_generated_functions.yml │ │ │ ├── example_rule_mocks.yml │ │ │ ├── example_rule_extraneous_fields.yml │ │ │ └── example_rule.yml │ │ ├── packs │ │ │ ├── missing_global.yml │ │ │ ├── missing_query.yml │ │ │ ├── missing_subrules.yml │ │ │ └── missing_datamodel.yml │ │ ├── queries │ │ │ ├── query_three.yml │ │ │ ├── query_one.yml │ │ │ └── query_two.yml │ │ ├── policies │ │ │ ├── example_policy_beta.py │ │ │ ├── example_policy.py │ │ │ ├── example_policy_extraneous_fields.py │ │ │ ├── example_policy_generated_functions.py │ │ │ ├── example_policy_extraneous_fields.yml │ │ │ ├── example_policy_beta.yml │ │ │ ├── example_policy_generated_functions.yml │ │ │ └── example_policy.yml │ │ ├── scheduled_rules │ │ │ ├── example_scheduled_rule.py │ │ │ └── example_scheduled_rule.yml │ │ ├── correlation_rules │ │ │ ├── github_cicd.yml │ │ │ ├── aws_cloudtrail_iaas.yml │ │ │ └── discovering_exfiltrated_credentials.yml │ │ ├── data_models │ │ │ ├── aws_cloudtrail_data_model.yml │ │ │ └── aws_cloudtrail_data_model.py │ │ └── advanced_rules │ │ │ ├── example_rule_data_model.py │ │ │ └── example_rule_data_model.yml │ └── packless-rule │ │ ├── packs │ │ └── test.yml │ │ └── rules │ │ └── test_rules │ │ ├── test_included.yml │ │ ├── test_missing.yml │ │ └── test_deprecated.yml │ ├── inline-filters │ ├── basic.python.rule.py │ ├── basic.python.scheduled_rule.py │ ├── basic.python.rule.with.filters.py │ ├── basic.python.rule.yml │ ├── basic.rule.yml │ ├── basic.python.scheduled_rule.yml │ ├── basic.scheduled_rule.yml │ ├── basic.python.rule.with.filters.yml │ └── basic.rule.with.filters.yml │ ├── tests_can_be_inherited │ ├── base.py │ ├── derive.yml │ └── base.yml │ ├── lookup-tables │ ├── valid │ │ ├── sample_aws_accounts.csv │ │ ├── lookup-table-1.yml │ │ └── lookup-table-2.yml │ └── invalid │ │ └── lookup-table-1.yml │ ├── derived_without_base │ └── derived.yml │ ├── simple-detections │ ├── valid │ │ ├── Test.AbsoluteCondition.yml │ │ ├── Test.Numeric.Comparison.yml │ │ ├── vpc_dns_tunneling.yml │ │ ├── Test.Combinators.yml │ │ ├── Test.Extra.Top.Level.Keys.yml │ │ ├── test.rule.with.dynamic.funcs.yml │ │ ├── snowflake_login_without_mfa.yml │ │ ├── test.enrichment.rule.yml │ │ ├── Test.MultiMatch.Key.yml │ │ ├── aws_s3_unauthenticated_access.yml │ │ ├── onelogin_high_risk_failed_login.yml │ │ ├── box_user_downloads.yml │ │ ├── aws_authentication_from_crowdstrike_unmanaged_device.yml │ │ ├── onelogin_password_accessed.yml │ │ ├── onelogin_user_account_locked.yml │ │ ├── GitHub.Team.Modified.yml │ │ ├── Test.ListComprehension.yml │ │ ├── aws_vpc_inbound_traffic_port_allowlist.yml │ │ ├── asana_team_privacy_public.yml │ │ └── gsuite_leaked_password.yml │ └── invalid │ │ ├── invalid_Test.MultiMatch.Key.yml │ │ └── invalid_asana_team_privacy.yml │ ├── custom-schemas │ ├── valid │ │ ├── schema_2_tests.yaml │ │ ├── schema-2.yaml │ │ ├── schema-3.yml │ │ ├── schema-1.yml │ │ ├── lookup-table-schema-1.yml │ │ └── schema_1_tests.yml │ └── invalid │ │ ├── schema-2.yaml │ │ └── schema-1.yml │ ├── correlation-unit-tests │ ├── fails │ │ └── fails1.yml │ └── passes │ │ └── pass1.yml │ └── queries │ ├── invalid │ ├── example-scheduled-query-invalid-tablename-2.yml │ ├── example-scheduled-query-invalid-tablename-4.yml │ ├── example-scheduled-query-invalid-tablename-1.yml │ └── example-scheduled-query-invalid-tablename-3.yml │ └── valid │ ├── example-scheduled-query-rateminutes.yml │ └── example-scheduled-query-cron.yml ├── panther_analysis_tool ├── __init__.py ├── backend │ ├── __init__.py │ ├── graphql │ │ ├── get_version.graphql │ │ ├── delete_detections.graphql │ │ ├── validate_bulk_upload.graphql │ │ ├── generate_enriched_event.graphql │ │ ├── transpile_filters.graphql │ │ ├── delete_saved_queries.graphql │ │ ├── async_bulk_upload.graphql │ │ ├── feature_flags.graphql │ │ ├── metrics.graphql │ │ ├── transpile_sdl.graphql │ │ ├── test_correlation_rule.graphql │ │ ├── validate_bulk_upload_status.graphql │ │ ├── get_rule_body.graphql │ │ ├── create_or_update_schema.graphql │ │ ├── list_schemas.graphql │ │ ├── bulk_upload.graphql │ │ ├── stop_replay.graphql │ │ ├── async_bulk_upload_status.graphql │ │ ├── replay.graphql │ │ ├── create_perf_test.graphql │ │ └── introspection_query.graphql │ └── errors.py ├── command │ ├── __init__.py │ ├── check_connection.py │ └── validate.py ├── log_schemas │ └── __init__.py ├── detection_schemas │ └── __init__.py ├── destination.py ├── directory.py └── cli_output.py ├── bin ├── pat └── panther_analysis_tool ├── .github ├── pull_request_template.md ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── ci.yml │ ├── test_release_publish.yml │ ├── version_bump_pr.yml │ ├── fmt.yml │ └── invisible-characters.yml ├── actions │ └── changed_files │ │ └── action.yml ├── scripts │ └── lint-invisible-characters │ │ ├── lint-invisible-characters-test-file.md │ │ └── README.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── story.md │ └── feature_request.md ├── MANIFEST.in ├── CONTRIBUTING.md ├── .gitignore ├── SECURITY.md └── example_panther_config.yml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /panther_analysis_tool/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /panther_analysis_tool/log_schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /panther_analysis_tool/detection_schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/log_schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/no_status.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/debug/rule_that_works.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/status_stable.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/status_stable/status_stable.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_bad_log_type.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_set_duplicates.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/status_deprecated.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/status_experimental.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /bin/pat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from panther_analysis_tool import main 4 | 5 | main.run() 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/disabled_rule/example_disabled_rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | raise 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_unhandled_exception.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | raise Exception 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/status_deprecated/status_deprecated.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_policy_bad_resource_type.py: -------------------------------------------------------------------------------- 1 | def policy(resource): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/status/status_experimental/status_experimental.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/example_rule_invalid_test.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.udm("any_field") 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/global_helpers/helpers.py: -------------------------------------------------------------------------------- 1 | def test_helper(): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/check-packs/missing-dependencies/global_helpers/helpers.py: -------------------------------------------------------------------------------- 1 | def test_helper(): 2 | return True 3 | -------------------------------------------------------------------------------- /tests/fixtures/inline-filters/basic.python.rule.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.get("userAgent") == "Max" 3 | -------------------------------------------------------------------------------- /bin/panther_analysis_tool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from panther_analysis_tool import main 4 | 5 | main.run() 6 | -------------------------------------------------------------------------------- /tests/fixtures/detections/debug/rule_that_prints.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | print("Test output") 3 | return True 4 | -------------------------------------------------------------------------------- /tests/fixtures/detections/valid_analysis/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.py: -------------------------------------------------------------------------------- 1 | def b_says_hello(): 2 | return "hello from b" 3 | -------------------------------------------------------------------------------- /tests/fixtures/detections/.panther_settings.yml: -------------------------------------------------------------------------------- 1 | ignored_files: 2 | - "example_ignored.yml" 3 | - "example_ignored_multi" 4 | -------------------------------------------------------------------------------- /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.rule.with.filters.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | return event.get("userAgent") == "Max" 3 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/get_version.graphql: -------------------------------------------------------------------------------- 1 | query GetVersion { 2 | generalSettings { 3 | pantherVersion 4 | } 5 | } -------------------------------------------------------------------------------- /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/example_policy_import.py: -------------------------------------------------------------------------------- 1 | import aws_globals 2 | 3 | 4 | def policy(resource): 5 | return aws_globals.GLOBAL_TRUE 6 | -------------------------------------------------------------------------------- /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/derive.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | RuleID: 'Sus.Login.Derived' 3 | BaseDetection: 'Sus.Login.Base' 4 | Severity: High 5 | Enabled: true 6 | -------------------------------------------------------------------------------- /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/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_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/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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Background 2 | 3 | 4 | 5 | ### Changes 6 | 7 | * 8 | 9 | ### Testing 10 | 11 | * 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/validate_bulk_upload.graphql: -------------------------------------------------------------------------------- 1 | mutation ValidateBulkUpload($input: ValidateBulkUploadInput!) { 2 | validateBulkUpload(input: $input) { 3 | receiptId 4 | } 5 | } -------------------------------------------------------------------------------- /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.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: global 2 | GlobalID: b_helper 3 | Filename: b_helper.py 4 | Description: > 5 | Used to test global importing other globals 6 | -------------------------------------------------------------------------------- /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/transpile_filters.graphql: -------------------------------------------------------------------------------- 1 | mutation TranspileFilters($input: TranspileFiltersInput!) { 2 | transpileFilters(input: $input) { 3 | transpiledFilters 4 | } 5 | } 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.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/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/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/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 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/delete_saved_queries.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteSavedQueries($input: DeleteSavedQueriesByNameInput!) { 2 | deleteSavedQueriesByName(input: $input) { 3 | detectionIDs 4 | names 5 | } 6 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /panther_analysis_tool/backend/graphql/async_bulk_upload.graphql: -------------------------------------------------------------------------------- 1 | mutation uploadDetectionEntitiesAsync($input: UploadDetectionEntitiesAsyncInput!) { 2 | uploadDetectionEntitiesAsync(input: $input) { 3 | receiptId 4 | } 5 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/transpile_sdl.graphql: -------------------------------------------------------------------------------- 1 | mutation TranspileSimpleDetectionsToPython($input: TranspileSimpleDetectionsToPythonInput!) { 2 | transpileSimpleDetectionsToPython(input: $input) { 3 | transpiledPython 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/detections/debug/rule_with_error.py: -------------------------------------------------------------------------------- 1 | def rule(event): 2 | """Test rule that deliberately raises an exception for debug testing.""" 3 | # This rule intentionally raises an exception to test debug traceback functionality 4 | sub_func() 5 | 6 | 7 | def sub_func(): 8 | raise ValueError("Test exception for debug tracing") 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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_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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/custom-schemas/valid/schema_2_tests.yaml: -------------------------------------------------------------------------------- 1 | name: test1 2 | logType: Custom.SampleSchema2 3 | input: | 4 | { 5 | "ip": "10.0.0.1", 6 | "time":"2021-03-26T21:40:41Z" 7 | } 8 | result: | 9 | { 10 | "ip": "10.0.0.1", 11 | "time":"2021-03-26T21:40:41Z", 12 | "p_log_type":"Custom.SampleSchema2", 13 | "p_event_time":"2021-03-26T21:40:41Z", 14 | "p_any_ip_addresses":["10.0.0.1"] 15 | } -------------------------------------------------------------------------------- /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/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/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/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_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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/check_connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Tuple 3 | 4 | from panther_analysis_tool.backend.client import Client as BackendClient 5 | 6 | 7 | def run(backend: BackendClient, api_host: str) -> Tuple[int, str]: 8 | logging.info("checking connection to %s...", api_host) 9 | result = backend.check() 10 | 11 | if not result.success: 12 | logging.info("connection failed") 13 | return 1, result.message 14 | 15 | logging.info("connection successful: %s", result.message) 16 | return 0, "" 17 | -------------------------------------------------------------------------------- /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/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/status/all_statuses/no_status.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: no_status.py 3 | DisplayName: rule without status 4 | Description: rule without status description 5 | Severity: High 6 | RuleID: No.Status 7 | Enabled: true 8 | LogTypes: 9 | - AWS.CloudTrail 10 | Tests: 11 | - 12 | Name: Example Test 13 | ExpectedResult: true 14 | Log: 15 | Arn: arn:aws:iam::123456789012:user/root 16 | CreateDate: 2019-01-01T00:00:00Z 17 | CredentialReport: 18 | MfaActive: false 19 | PasswordEnabled: true 20 | UserName: root 21 | -------------------------------------------------------------------------------- /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/status/all_statuses/status_stable.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_stable.py 3 | DisplayName: stable rule 4 | Description: stable rule description 5 | Severity: High 6 | RuleID: Status.Stable 7 | Status: stable 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /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/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/status/status_stable/status_stable.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_stable.py 3 | DisplayName: stable rule 4 | Description: stable rule description here 5 | Severity: High 6 | RuleID: Status.Stable 7 | Status: stable 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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: Setup Virtual Environment 18 | run: make venv 19 | - name: Run CLI Tests 20 | run: make ci -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/status_deprecated.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_deprecated.py 3 | DisplayName: deprecated rule 4 | Description: deprecated rule description 5 | Severity: High 6 | RuleID: Status.Deprecated 7 | Status: deprecated 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /tests/fixtures/status/status_deprecated/status_deprecated.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_deprecated.py 3 | DisplayName: deprecated rule 4 | Description: deprecated rule description 5 | Severity: High 6 | RuleID: Status.Deprecated 7 | Status: deprecated 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /tests/fixtures/status/all_statuses/status_experimental.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_experimental.py 3 | DisplayName: experimental rule 4 | Description: experimental rule description 5 | Severity: High 6 | RuleID: Status.Experimental 7 | Status: experimental 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /tests/fixtures/status/status_experimental/status_experimental.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: status_experimental.py 3 | DisplayName: experimental rule 4 | Description: experimental rule description 5 | Severity: High 6 | RuleID: Status.Experimental 7 | Status: experimental 8 | Enabled: true 9 | LogTypes: 10 | - AWS.CloudTrail 11 | Tests: 12 | - 13 | Name: Example Test 14 | ExpectedResult: true 15 | Log: 16 | Arn: arn:aws:iam::123456789012:user/root 17 | CreateDate: 2019-01-01T00:00:00Z 18 | CredentialReport: 19 | MfaActive: false 20 | PasswordEnabled: true 21 | UserName: root 22 | -------------------------------------------------------------------------------- /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_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/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/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/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/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/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/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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /.github/actions/changed_files/action.yml: -------------------------------------------------------------------------------- 1 | name: Changed Files 2 | description: Determine modified files 3 | outputs: 4 | all_changed_files: 5 | description: 'All the changed files' 6 | value: ${{ steps.changed_files.outputs.all_changed_files }} 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Retrieve changed files 11 | id: changed_files 12 | uses: tj-actions/changed-files@d6babd6899969df1a11d14c368283ea4436bca78 13 | - name: List affected files 14 | if: ${{ steps.changed_files.outputs.all_changed_files != '' }} 15 | run: | 16 | echo "Affected files:" 17 | for file in ${{ steps.changed_files.outputs.all_changed_files }}; do 18 | echo "- ${file}" 19 | done 20 | shell: bash 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.github/scripts/lint-invisible-characters/lint-invisible-characters-test-file.md: -------------------------------------------------------------------------------- 1 | # Test File with Invisible Characters 2 | 3 | This file contains various invisible characters to test the linter. 4 | 5 | ## Examples 6 | 7 | This line has a zero width space:​here (between colon and "here") 8 | 9 | This line has a soft hyphen: soft­hyphen (in the word "softhyphen") 10 | 11 | This line has a zero width non-joiner: test‌case (between "test" and "case") 12 | 13 | This line has a word joiner: word⁠joiner (between "word" and "joiner") 14 | 15 | ## Normal characters (should not be flagged) 16 | 17 | Normal spaces and tabs: legitimate whitespace 18 | Newlines are also fine 19 | 20 | ## Mixed content 21 | 22 | Some normal text with zero width space:​sneaky 23 | Regular content followed by soft­hyphen issue -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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(signum: int = 0, _frame: Any = None) -> None: 19 | shutil.rmtree(temp_dir, ignore_errors=True) 20 | 21 | # If this was called as a signal handler, re-raise the signal 22 | if signum in (signal.SIGINT, signal.SIGTERM): 23 | # reset to the default python handler and redeliver the signal so that the 24 | # python process exits properly 25 | signal.signal(signum, signal.SIG_DFL) 26 | os.kill(os.getpid(), signum) 27 | 28 | atexit.register(clean_me_up) 29 | signal.signal(signal.SIGINT, clean_me_up) 30 | signal.signal(signal.SIGTERM, clean_me_up) 31 | -------------------------------------------------------------------------------- /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/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/unit/panther_analysis_tool/test_lookup_tables.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | from panther_analysis_tool import main as pat 5 | 6 | FIXTURES_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../", "fixtures")) 7 | LUTS_FIXTURES_PATH = os.path.join(FIXTURES_PATH, "lookup-tables") 8 | 9 | 10 | class TestLookupTable(TestCase): # pylint: disable=too-many-public-methods 11 | def test_load_invalid_specs_from_folder(self): 12 | path = f"{LUTS_FIXTURES_PATH}/invalid/lookup-table-1.yml" 13 | rc, file_path = pat.test_lookup_table(path) 14 | self.assertEqual(1, rc) 15 | self.assertEqual(file_path, "") 16 | 17 | def test_load_invalid_specs_from_folder(self): 18 | path = f"{LUTS_FIXTURES_PATH}/valid/lookup-table-1.yml" 19 | rc, file_path = pat.test_lookup_table(path) 20 | self.assertEqual(0, rc) 21 | self.assertEqual(file_path, "") 22 | 23 | path = f"{LUTS_FIXTURES_PATH}/valid/lookup-table-2.yml" 24 | rc, file_path = pat.test_lookup_table(path) 25 | self.assertEqual(0, rc) 26 | self.assertEqual(file_path, "") 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | panther-analysis/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /panther_analysis_tool/cli_output.py: -------------------------------------------------------------------------------- 1 | from panther_analysis_tool.backend.client import BackendMultipartError 2 | 3 | 4 | class BColors: 5 | OKGREEN = "\033[92m" 6 | WARNING = "\033[93m" 7 | FAIL = "\033[91m" 8 | ENDC = "\033[0m" 9 | BOLD = "\033[1m" 10 | 11 | @classmethod 12 | def wrap(cls, start: str, text: str) -> str: 13 | return f"{start}{text}{cls.ENDC}" 14 | 15 | 16 | def bold(text: str) -> str: 17 | return BColors.wrap(BColors.BOLD, text) 18 | 19 | 20 | def success(text: str) -> str: 21 | return BColors.wrap(BColors.OKGREEN, text) 22 | 23 | 24 | def warning(text: str) -> str: 25 | return BColors.wrap(BColors.WARNING, text) 26 | 27 | 28 | def failed(text: str) -> str: 29 | return BColors.wrap(BColors.FAIL, text) 30 | 31 | 32 | def multipart_error_msg(result: BackendMultipartError, msg: str) -> str: 33 | return_str = "\n-----\n" 34 | 35 | if result.has_error(): 36 | return_str += f"{bold('Error')}: {result.get_error()}\n-----\n" 37 | 38 | for issue in result.get_issues(): 39 | if issue.path and issue.path != "": 40 | return_str += f"{bold('Path')}: {issue.path}\n" 41 | 42 | if issue.error_message and issue.error_message != "": 43 | return_str += f"{bold('Error')}: {issue.error_message}\n" 44 | 45 | return_str += "-----\n" 46 | 47 | return f"{return_str}\n{failed(msg)}" 48 | -------------------------------------------------------------------------------- /tests/unit/panther_analysis_tool/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Any, Dict, List 3 | 4 | from panther_core.exceptions import PantherError 5 | 6 | 7 | class TestPantherError(unittest.TestCase): 8 | def test_equals(self) -> None: 9 | cases: List[Dict[str, Any]] = [ 10 | {"args": (("a", "b"), ("a", "b")), "expected": True}, 11 | {"args": (("a",), ("a",)), "expected": True}, 12 | {"args": (("a", "b", "c"), ("a", "b")), "expected": False}, 13 | ] 14 | for case in cases: 15 | instance_args, other_args = case["args"] 16 | instance = PantherError(*instance_args) 17 | other = PantherError(*other_args) 18 | self.assertIs(instance.equals(other), case["expected"]) 19 | # Test symmetry 20 | self.assertIs(other.equals(instance), case["expected"]) 21 | 22 | def test_has_message_prefix(self) -> None: 23 | exc = PantherError("something", "went wrong") 24 | self.assertTrue(exc.has_message_prefix("something")) 25 | self.assertFalse(exc.has_message_prefix("something went")) 26 | 27 | def test_to_string(self) -> None: 28 | exc = PantherError("generic error", "went wrong") 29 | self.assertEqual(str(exc), "generic error: went wrong") 30 | exc = PantherError("generic error") 31 | self.assertEqual(str(exc), "generic error") 32 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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: Setup Virtual Environment 25 | run: make venv 26 | 27 | - name: Export dependencies 28 | run: | 29 | make reqs 30 | 31 | - name: Build Release tar.gz 32 | run: | 33 | make build 34 | 35 | - name: Install Build and Run PAT Tests 36 | run: | 37 | poetry run pip install --root-user-action=ignore dist/panther_analysis_tool-*.tar.gz 38 | make test integration 39 | 40 | - name: Create Github Release 41 | run: | 42 | export NEW_VERSION=$(poetry version -s) 43 | git config user.name "dac-bot" 44 | git config user.email "dac-bot@panther.com" 45 | gh release create v$NEW_VERSION dist/* -t v$NEW_VERSION --draft 46 | env: 47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Publish to PyPI 50 | run: | 51 | make release 52 | env: 53 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 54 | -------------------------------------------------------------------------------- /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/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/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/debug/rule_that_works.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: rule_that_works.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: Debug.RuleThatWorks 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/debug/rule_with_error.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: rule_with_error.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: Debug.RuleWithError 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/debug/rule_that_prints.yml: -------------------------------------------------------------------------------- 1 | AnalysisType: rule 2 | Filename: rule_that_prints.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: Debug.RuleThatPrints 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.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/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/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/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/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_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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /panther_analysis_tool/command/validate.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | import zipfile 4 | from dataclasses import dataclass 5 | from typing import List, 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.core.parse import Filter 15 | from panther_analysis_tool.zip_chunker import ZipArgs, analysis_chunks 16 | 17 | 18 | @dataclass 19 | class ValidateArgs: 20 | out: str 21 | path: str 22 | ignore_files: List[str] 23 | filters: List[Filter] 24 | filters_inverted: List[Filter] 25 | 26 | 27 | def run(backend: BackendClient, args: ValidateArgs) -> Tuple[int, str]: 28 | if backend is None or not backend.supports_bulk_validate(): 29 | return 1, "Invalid backend. `validate` is only supported via API token" 30 | 31 | zip_args = ZipArgs( 32 | out=args.out, 33 | path=args.path, 34 | ignore_files=args.ignore_files, 35 | filters=args.filters, 36 | filters_inverted=args.filters_inverted, 37 | ) 38 | chunks = analysis_chunks(zip_args) 39 | buffer = io.BytesIO() 40 | 41 | with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_out: 42 | for name in chunks[0].files: 43 | zip_out.write(name) 44 | 45 | buffer.seek(0, 0) 46 | params = BulkUploadParams(zip_bytes=buffer.read()) 47 | 48 | try: 49 | result = backend.bulk_validate(params) 50 | if result.is_valid(): 51 | return 0, f"{cli_output.success('Validation success')}" 52 | 53 | return 1, cli_output.multipart_error_msg(result, "Validation failed") 54 | except UnsupportedEndpointError as err: 55 | logging.debug(err) 56 | return 1, cli_output.warning("Your Panther instance does not support this feature") 57 | 58 | except BaseException as err: # pylint: disable=broad-except 59 | return 1, cli_output.multipart_error_msg( 60 | BulkUploadValidateStatusResponse.from_json({"error": str(err)}), "Validation failed" 61 | ) 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/scripts/lint-invisible-characters/README.md: -------------------------------------------------------------------------------- 1 | # Invisible Character Linter 2 | 3 | The `lint-invisible.py` script detects invisible Unicode characters in text files that might cause issues or be used maliciously. It ignores common legitimate whitespace characters (space, tab, CR, LF). 4 | 5 | ### Usage 6 | 7 | ```bash 8 | python3 lint-invisible.py ... [--ignore ,,...] 9 | ``` 10 | 11 | #### Arguments 12 | - ` ...`: One or more files to scan 13 | - `--ignore`: Optional comma-separated list of patterns to ignore 14 | 15 | ### Testing 16 | 17 | To test the linter with the provided test file: 18 | 19 | ```bash 20 | # Basic test 21 | python3 lint-invisible.py lint-invisible-test-file.md 22 | ``` 23 | 24 | Expected output will show detected invisible characters with their Unicode code points and descriptions. The script will exit with status code 1 if any invisible characters are found. 25 | 26 | ### Scanning the Entire Repository 27 | 28 | To scan all files in the repository, you can use the following commands based on your operating system. Run these commands from the root of the repository: 29 | 30 | #### macOS / Linux (bash/zsh) 31 | ```bash 32 | find . -type f -not -path '*/\.*' -exec python3 .github/scripts/lint-invisible-characters/lint-invisible.py {} + 33 | ``` 34 | 35 | #### Windows (PowerShell) 36 | ```powershell 37 | Get-ChildItem -Recurse -File | Where-Object { $_.FullName -notlike '*\.git\*' } | ForEach-Object { python3 .github/scripts/lint-invisible-characters/lint-invisible.py $_.FullName } 38 | ``` 39 | 40 | The commands above will: 41 | 1. Find all files in the current directory and subdirectories 42 | 2. Exclude hidden files and `.git` directory 43 | 3. Pass the files to the linter for scanning 44 | 45 | You can add the `--ignore` flag with patterns if needed: 46 | ```bash 47 | # macOS / Linux 48 | find . -type f -not -path '*/\.*' -exec python3 .github/scripts/lint-invisible-characters/lint-invisible-characters.py --ignore=pattern1,pattern2 {} + 49 | 50 | # Windows PowerShell 51 | Get-ChildItem -Recurse -File | Where-Object { $_.FullName -notlike '*\.git\*' } | ForEach-Object { python3 .github/scripts/lint-invisible-characters/lint-invisible-characters.py --ignore=pattern1,pattern2 $_.FullName } 52 | ``` 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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: Setup Virtual Environment 25 | run: make venv 26 | - name: Format 27 | run: make fmt 28 | - name: Import GPG key 29 | if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' }} 30 | uses: crazy-max/ghaction-import-gpg@v6 31 | with: 32 | gpg_private_key: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY }} 33 | passphrase: ${{ secrets.PANTHER_BOT_GPG_PRIVATE_KEY_PASSPHRASE }} 34 | git_user_signingkey: true 35 | git_commit_gpgsign: true 36 | - name: Commit formatting 37 | if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' }} 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.github/workflows/invisible-characters.yml: -------------------------------------------------------------------------------- 1 | name: Detect Invisible Characters in Changed Files 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | jobs: 13 | changed_files: 14 | name: Changed Files 15 | runs-on: ubuntu-latest 16 | outputs: 17 | changed_files: ${{ steps.changed_files.outputs.all_changed_files }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 21 | - name: Changed Files 22 | id: changed_files 23 | uses: ./.github/actions/changed_files 24 | 25 | test_invisible_characters: 26 | needs: changed_files 27 | if: contains(needs.changed_files.outputs.changed_files, '.github/scripts/lint-invisible-characters') 28 | name: Test Invisible Characters in Changed Files 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 33 | - name: Set up Python 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: '3.12' 37 | - name: Test Invisible Characters 38 | id: test_script 39 | continue-on-error: true 40 | run: | 41 | python .github/scripts/lint-invisible-characters/lint-invisible-characters.py \ 42 | .github/scripts/lint-invisible-characters/lint-invisible-characters-test-file.md 43 | - name: Check Test Result 44 | # .conclusion on steps with continue-on-error: true will always be success 45 | # so we use .outcome to check the exit code of the script 46 | if: steps.test_script.outcome != 'failure' 47 | run: | 48 | echo "Test file check failed - script should have detected invisible characters and exited with status 1" 49 | exit 1 50 | 51 | invisible_characters: 52 | needs: [changed_files, test_invisible_characters] 53 | if: needs.changed_files.outputs.changed_files != '' 54 | name: Detect Invisible Characters in Changed Files 55 | runs-on: ubuntu-latest 56 | permissions: 57 | id-token: write 58 | contents: read 59 | packages: write 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 63 | - name: Set up Python 64 | uses: actions/setup-python@v5 65 | with: 66 | python-version: '3.12' 67 | 68 | - name: Detect invisible characters 69 | run: | 70 | python .github/scripts/lint-invisible-characters/lint-invisible-characters.py \ 71 | ${{ needs.changed_files.outputs.changed_files }} \ 72 | --ignore .github/scripts/lint-invisible-characters --------------------------------------------------------------------------------