diff --git a/.github/workflows/check-packs.yml b/.github/workflows/check-packs.yml index 9912599be..97cdf79a8 100644 --- a/.github/workflows/check-packs.yml +++ b/.github/workflows/check-packs.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - name: Set python version - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d #v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 with: python-version: "3.11" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 83512e4f9..6c096a3e0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,10 +28,10 @@ jobs: www.python.org:443 - name: Checkout panther-analysis uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 #v3.0.0 + - uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee #v3.1.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb #v3.3.0 + uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 #v3.4.0 - name: Build Image run: docker buildx build --load -f Dockerfile -t panther-analysis:latest . - name: Test Image diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 81cbfb35c..f1c81e06e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - name: Set python version - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d #v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 with: python-version: "3.11" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ef6f3a9e..971d361e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: read jobs: release: @@ -29,7 +29,7 @@ jobs: aws-region: ${{ secrets.AWS_REGION }} role-session-name: panther-analysis-release - name: Install Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d #v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 with: python-version: "3.11" - name: Create new panther-analysis release diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83a13ba05..702decec7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - name: Set python version - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d #v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 with: python-version: "3.11" diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index 9cd5f85de..d05afd7bb 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -4,7 +4,7 @@ on: - main permissions: - contents: read + contents: read jobs: upload: @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - name: Set python version - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d #v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 with: python-version: "3.11" diff --git a/correlation_rules/aws_cloudtrail_stopinstance_followed_by_modifyinstanceattributes.yml b/correlation_rules/aws_cloudtrail_stopinstance_followed_by_modifyinstanceattributes.yml new file mode 100644 index 000000000..c60c8ee75 --- /dev/null +++ b/correlation_rules/aws_cloudtrail_stopinstance_followed_by_modifyinstanceattributes.yml @@ -0,0 +1,48 @@ +AnalysisType: correlation_rule +RuleID: "AWS.EC2.StopInstance.FOLLOWED.BY.ModifyInstanceAttributes" +DisplayName: "StopInstance FOLLOWED BY ModifyInstanceAttributes" +Enabled: true +Severity: High +Description: Identifies when StopInstance and ModifyInstanceAttributes CloudTrail events occur in a short period of time. Since EC2 startup scripts cannot be modified without first stopping the instance, StopInstances should be a signal. +Reference: https://unit42.paloaltonetworks.com/malicious-operations-of-exposed-iam-keys-cryptojacking/ +Reports: + MITRE ATT&CK: + - TA0002:T1059 +Detection: + - Sequence: + - ID: StopInstance + RuleID: AWS.EC2.StopInstances + - ID: StartupScriptChange + RuleID: AWS.EC2.Startup.Script.Change + Transitions: + - ID: StopInstance FOLLOWED BY StartupScriptChange + From: StopInstance + To: StartupScriptChange + Match: + - On: p_alert_context.instance_ids + LookbackWindowMinutes: 90 + Schedule: + RateMinutes: 60 + TimeoutMinutes: 1 +Tests: + - Name: Instance Stopped, Followed By Script Change + ExpectedResult: true + RuleOutputs: + - ID: StopInstance + Matches: + p_alert_context.instance_ids: + 'i-abcdef0123456789a': + - "2024-06-01T10:00:01Z" + - ID: StartupScriptChange + Matches: + p_alert_context.instance_ids: + 'i-abcdef0123456789a': + - "2024-06-01T10:01:01Z" + - Name: Instance Stopped, Not Followed By Script Change + ExpectedResult: false + RuleOutputs: + - ID: StopInstance + Matches: + p_alert_context.instance_ids: + 'i-abcdef0123456789a': + - "2024-06-01T10:00:01Z" diff --git a/correlation_rules/aws_potentially_compromised_service_role.yml b/correlation_rules/aws_potentially_compromised_service_role.yml new file mode 100644 index 000000000..799628aa8 --- /dev/null +++ b/correlation_rules/aws_potentially_compromised_service_role.yml @@ -0,0 +1,64 @@ +AnalysisType: correlation_rule +RuleID: "AWS.Potentially.Stolen.Service.Role" +DisplayName: "AWS Potentiall Stolen Service Role" +Enabled: true +Tags: + - AWS +Severity: High +Reports: + MITRE ATT&CK: + - T1528 # Steal Application Access Token +Description: A role was assumed by an AWS service, followed by a user within 24 hours. This could indicate a stolen or compromised AWS service role. +Detection: + - Sequence: + - ID: Role Assumed by Service + RuleID: Role.Assumed.by.AWS.Service + - ID: Role Assumed by User + RuleID: Role.Assumed.by.User + Transitions: + - ID: Role Assumed by Service TO Role Assumed by User ON username + From: Role Assumed by Service + To: Role Assumed by User + Match: + - On: requestParameters.roleArn + Schedule: + RateMinutes: 60 + TimeoutMinutes: 2 + LookbackWindowMinutes: 1440 +Tests: + - Name: Role Assumed By Service, Followed By Role Assumed By User + ExpectedResult: true + RuleOutputs: + - ID: Role Assumed by Service + Matches: + requestParameters.roleArn: + FAKE_ROLE_ARN: [0] + - ID: Role Assumed by User + Matches: + requestParameters.roleArn: + FAKE_ROLE_ARN: [30] + - Name: Role Assumed By Service, Followed By Different Role Assumed By User + ExpectedResult: false + RuleOutputs: + - ID: Role Assumed by Service + Matches: + requestParameters.roleArn: + FAKE_ROLE_ARN: [0] + - ID: Role Assumed by User + Matches: + requestParameters.roleArn: + OTHER_ROLE_ARN: [30] + - Name: Role Assumed By Service, Not Followed By Role Assumed By User + ExpectedResult: false + RuleOutputs: + - ID: Role Assumed by Service + Matches: + requestParameters.roleArn: + FAKE_ROLE_ARN: [0] + - Name: Role Assumed By User, Not Preceded By Role Assumed By Service + ExpectedResult: false + RuleOutputs: + - ID: Role Assumed by User + Matches: + requestParameters.roleArn: + FAKE_ROLE_ARN: [0] \ No newline at end of file diff --git a/correlation_rules/aws_privilege_escalation_via_user_compromise.yml b/correlation_rules/aws_privilege_escalation_via_user_compromise.yml new file mode 100644 index 000000000..499bdfb03 --- /dev/null +++ b/correlation_rules/aws_privilege_escalation_via_user_compromise.yml @@ -0,0 +1,72 @@ +AnalysisType: correlation_rule +RuleID: "AWS.Privilege.Escalation.Via.User.Compromise" +DisplayName: "AWS Privilege Escalation Via User Compromise" +Enabled: true +Severity: Medium +Reports: + MITRE ATT&CK: + - T1098.001 # Additional Cloud Credentials +Detection: + - Sequence: + - ID: User Backdoored + RuleID: AWS.IAM.Backdoor.User.Keys + - ID: User Accessed + RuleID: AWS.CloudTrail.UserAccessKeyAuth + Transitions: + - ID: User Backdoored TO User Accessed ON IP Addr + From: User Backdoored + To: User Accessed + Match: + - On: p_alert_context.ip_accessKeyId + Schedule: + RateMinutes: 15 + TimeoutMinutes: 2 + LookbackWindowMinutes: 60 +Tests: + - Name: Access Key Created and Used from Same IP + ExpectedResult: true + RuleOutputs: + - ID: User Backdoored + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [0] + - ID: User Accessed + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [30] + - Name: Access Key Created and Used from Different IPs + ExpectedResult: false + RuleOutputs: + - ID: User Backdoored + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [0] + - ID: User Accessed + Matches: + p_alert_context.ip_accessKeyId: + 2.2.2.2-FAKE_ACCESS_KEY_ID: [30] + - Name: Single IP Creates Access Key, Uses Different Key + ExpectedResult: false + RuleOutputs: + - ID: User Backdoored + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [0] + - ID: User Accessed + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-OTHER_ACCESS_KEY_ID: [30] + - Name: Access Key Created But Not Used + ExpectedResult: false + RuleOutputs: + - ID: User Backdoored + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [0] + - Name: Access Key Used But Not Created + ExpectedResult: false + RuleOutputs: + - ID: User Accessed + Matches: + p_alert_context.ip_accessKeyId: + 1.1.1.1-FAKE_ACCESS_KEY_ID: [30] \ No newline at end of file diff --git a/correlation_rules/aws_user_takeover_via_password_reset.yml b/correlation_rules/aws_user_takeover_via_password_reset.yml new file mode 100644 index 000000000..3792a8f5f --- /dev/null +++ b/correlation_rules/aws_user_takeover_via_password_reset.yml @@ -0,0 +1,61 @@ +AnalysisType: correlation_rule +RuleID: "AWS.User.Takeover.Via.Password.Reset" +DisplayName: "AWS User Takeover Via Password Reset" +Enabled: true +Severity: High +Reports: + MITRE ATT&CK: + - T1098.001 # Additional Cloud Credentials +Detection: + - Sequence: + - ID: Password Reset + RuleID: AWS.CloudTrail.LoginProfileCreatedOrModified + - ID: Login + RuleID: AWS.Console.Login + Transitions: + - ID: Password Reset TO Login ON IP Addr + From: Password Reset + To: Login + Match: + - On: sourceIPAddress + Schedule: + RateMinutes: 15 + TimeoutMinutes: 2 + LookbackWindowMinutes: 60 +Tests: + - Name: Password Reset, Then Login From Same IP + ExpectedResult: true + RuleOutputs: + - ID: Password Reset + Matches: + sourceIPAddress: + '1.1.1.1': [0] + - ID: Login + Matches: + sourceIPAddress: + '1.1.1.1': [5] + - Name: Password Reset, Then Login From Different IPs + ExpectedResult: false + RuleOutputs: + - ID: Password Reset + Matches: + sourceIPAddress: + '1.1.1.1': [0] + - ID: Login + Matches: + sourceIPAddress: + '2.2.2.2': [5] + - Name: Password Reset Without Login + ExpectedResult: false + RuleOutputs: + - ID: Password Reset + Matches: + sourceIPAddress: + '1.1.1.1': [0] + - Name: Login Without Password Reset + ExpectedResult: false + RuleOutputs: + - ID: Login + Matches: + sourceIPAddress: + '1.1.1.1': [5] diff --git a/correlation_rules/gcp_cloud_run_service_create_followed_by_set_iam_policy.yml b/correlation_rules/gcp_cloud_run_service_create_followed_by_set_iam_policy.yml new file mode 100644 index 000000000..c0643468d --- /dev/null +++ b/correlation_rules/gcp_cloud_run_service_create_followed_by_set_iam_policy.yml @@ -0,0 +1,72 @@ +AnalysisType: correlation_rule +RuleID: "GCP.Cloud.Run.Service.Created.FOLLOWED.BY.Set.IAM.Policy" +DisplayName: "GCP Cloud Run Service Created FOLLOWED BY Set IAM Policy" +Enabled: true +Severity: High +Description: Detects run.services.create method for privilege escalation in GCP. The exploit creates a new Cloud Run + Service that, when invoked, returns the Service Account's access token by accessing the metadata API of the server + it is running on. +Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/ +Runbook: Confirm this was authorized and necessary behavior +Reports: + MITRE ATT&CK: + - TA0004:T1548 # Abuse Elevation Control Mechanism +Detection: + - Sequence: + - ID: ServiceCreated + RuleID: GCP.Cloud.Run.Service.Created + - ID: SetIAMPolicy + RuleID: GCP.Cloud.Run.Set.IAM.Policy + Transitions: + - ID: ServiceCreated FOLLOWED BY SetIAMPolicy + From: ServiceCreated + To: SetIAMPolicy + Match: + - On: p_alert_context.caller_ip + LookbackWindowMinutes: 90 + Schedule: + RateMinutes: 60 + TimeoutMinutes: 1 +Tests: + - Name: GCP Service Run, Followed By IAM Policy Change From Same IP + ExpectedResult: true + RuleOutputs: + - ID: ServiceCreated + Matches: + p_alert_context.caller_ip: + 1.1.1.1: + - "2024-06-01T10:00:00Z" + - ID: SetIAMPolicy + Matches: + p_alert_context.caller_ip: + 1.1.1.1: + - "2024-06-01T10:00:01Z" + - Name: GCP Service Run, Not Followed By IAM Policy Change + ExpectedResult: false + RuleOutputs: + - ID: ServiceCreated + Matches: + p_alert_context.caller_ip: + 1.1.1.1: + - "2024-06-01T10:00:00Z" + - Name: IAM Policy Change, Not Preceeded By GCP Service Run + ExpectedResult: false + RuleOutputs: + - ID: SetIAMPolicy + Matches: + p_alert_context.caller_ip: + 1.1.1.1: + - "2024-06-01T10:00:01Z" + - Name: GCP Service Run, Followed By IAM Policy Change From Different IP + ExpectedResult: false + RuleOutputs: + - ID: ServiceCreated + Matches: + p_alert_context.caller_ip: + 1.1.1.1: + - "2024-06-01T10:00:00Z" + - ID: SetIAMPolicy + Matches: + p_alert_context.caller_ip: + 2.2.2.2: + - "2024-06-01T10:00:01Z" diff --git a/correlation_rules/github_advanced_security_change_not_followed_by_repo_archived.yml b/correlation_rules/github_advanced_security_change_not_followed_by_repo_archived.yml new file mode 100644 index 000000000..a581109c3 --- /dev/null +++ b/correlation_rules/github_advanced_security_change_not_followed_by_repo_archived.yml @@ -0,0 +1,59 @@ +AnalysisType: correlation_rule +RuleID: "GitHub.Advanced.Security.Change.NOT.FOLLOWED.BY.Repo.Archived" +DisplayName: "GitHub Advanced Security Change NOT FOLLOWED BY Repo Archived" +Enabled: true +Severity: Critical +Description: Identifies when advances security change was made not to archive a repo. Eliminates false positives in the Advances Security Change Rule when the repo is archived. +Reference: https://docs.github.com/en/code-security/getting-started/auditing-security-alerts +Detection: + - Sequence: + - ID: GHASChange + RuleID: GitHub.Advanced.Security.Change + - ID: RepoArchived + RuleID: Github.Repo.Archived + Absence: true + Transitions: + - ID: GHASChange NOT FOLLOWED BY RepoArchived + From: GHASChange + To: RepoArchived + Match: + - On: p_alert_context.repo + LookbackWindowMinutes: 15 + Schedule: + RateMinutes: 10 + TimeoutMinutes: 1 +Tests: + - Name: Security Change on Repo, Followed By Same Repo Archived + ExpectedResult: false + RuleOutputs: + - ID: GHASChange + Matches: + p_alert_context.repo: + my-org/example-repo: + - "2024-06-01T10:00:00Z" + - ID: RepoArchived + Matches: + p_alert_context.repo: + my-org/example-repo: + - "2024-06-01T10:00:01Z" + - Name: Security Change on Repo, Followed By Different Repo Archived + ExpectedResult: true + RuleOutputs: + - ID: GHASChange + Matches: + p_alert_context.repo: + my-org/example-repo: + - "2024-06-01T10:00:00Z" + - ID: RepoArchived + Matches: + p_alert_context.repo: + my-org/other-repo: + - "2024-06-01T10:00:01Z" + - Name: Security Change on Repo, Not Followed By Repo Archived + ExpectedResult: true + RuleOutputs: + - ID: GHASChange + Matches: + p_alert_context.repo: + my-org/example-repo: + - "2024-06-01T10:00:00Z" \ No newline at end of file diff --git a/correlation_rules/okta_login_without_push.yml b/correlation_rules/okta_login_without_push.yml new file mode 100644 index 000000000..7e26e5e20 --- /dev/null +++ b/correlation_rules/okta_login_without_push.yml @@ -0,0 +1,65 @@ +AnalysisType: correlation_rule +RuleID: "Okta.Login.Without.Push" +DisplayName: "Okta Login Without Push" +Enabled: false +Tags: + - Push Security + - Configuration Required +Reports: + MITRE ATT&CK: + - T1212 # Exploitation for Credential Access + - T1539 # Steal Web Session Cookie +Severity: Critical +Detection: + - Sequence: + - ID: Okta + RuleID: Okta.Login.Success + - ID: Push + RuleID: Push.Security.Authorized.IdP.Login + Absence: true + Transitions: + - ID: Okta to Push + From: Okta + To: Push + Match: + - From: actor.alternateId + To: new.email + Schedule: + RateMinutes: 5 + TimeoutMinutes: 2 + LookbackWindowMinutes: 30 +Tests: + - Name: Okta Login, Followed By Push Authorized Login + ExpectedResult: false + RuleOutputs: + - ID: Okta + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 + - ID: Push + Matches: + new.email: + frodo.baggins@hobbiton.com: + - 3 + - Name: Okta Login, Not Followed By Push Authorized Login + ExpectedResult: true + RuleOutputs: + - ID: Okta + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 + - Name: Okta Login, Followed By Push Authorized Login By Other User + ExpectedResult: true + RuleOutputs: + - ID: Okta + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 + - ID: Push + Matches: + new.email: + samwise.gamgee@hobbiton.com: + - 3 \ No newline at end of file diff --git a/correlation_rules/potential_compromised_okta_credentials.yml b/correlation_rules/potential_compromised_okta_credentials.yml new file mode 100644 index 000000000..720f21466 --- /dev/null +++ b/correlation_rules/potential_compromised_okta_credentials.yml @@ -0,0 +1,64 @@ +AnalysisType: correlation_rule +RuleID: "Potential.Compromised.Okta.Credentials" +DisplayName: "Potential Compromised Okta Credentials" +Enabled: false +Tags: + - Push Security + - Configuration Required +Reports: + MITRE ATT&CK: + - T1212 # Exploitation for Credential Access + - T1539 # Steal Web Session Cookie +Severity: Critical +Detection: + - Sequence: + - ID: Login Without Push Marker + RuleID: Okta.Login.Without.Push.Marker + - ID: Push Phishing + RuleID: Push.Security.Phishing.Attack + Transitions: + - ID: Match on user + From: Login Without Push Marker + To: Push Phishing + Match: + - From: actor.alternateId + To: new.employee.email + Schedule: + RateMinutes: 5 + TimeoutMinutes: 1 + LookbackWindowMinutes: 30 +Tests: + - Name: Login Without Marker, Followed By Phishing Detection + ExpectedResult: true + RuleOutputs: + - ID: Login Without Push Marker + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 + - ID: Push Phishing + Matches: + new.employee.email: + frodo.baggins@hobbiton.com: + - 3 + - Name: Login Without Marker, Followed By Phishing Detection for Different User + ExpectedResult: false + RuleOutputs: + - ID: Login Without Push Marker + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 + - ID: Push Phishing + Matches: + new.employee.email: + samwise.gamgee@hobbiton.com: + - 3 + - Name: Login Without Marker, Not Followed By Phishing Detection + ExpectedResult: false + RuleOutputs: + - ID: Login Without Push Marker + Matches: + actor.alternateId: + frodo.baggins@hobbiton.com: + - 0 \ No newline at end of file diff --git a/correlation_rules/secret_exposed_and_not_quarantined.yml b/correlation_rules/secret_exposed_and_not_quarantined.yml new file mode 100644 index 000000000..e39b89707 --- /dev/null +++ b/correlation_rules/secret_exposed_and_not_quarantined.yml @@ -0,0 +1,46 @@ +AnalysisType: correlation_rule +RuleID: "Secret.Exposed.and.not.Quarantined" +DisplayName: "Secret Exposed and not Quarantined" +Enabled: false +Tags: + - 'Unsecured Credentials: Credentials in Files' +Severity: High +Reports: + MITRE ATT&CK: + - T1552.001 +Description: The rule detects when a GitHub Secret Scan detects an exposed secret, which is not followed by the expected quarantine operation in AWS. When you make a repository public, or push changes to a public repository, GitHub always scans the code for secrets that match partner patterns. Public packages on the npm registry are also scanned. If secret scanning detects a potential secret, we notify the service provider who issued the secret. The service provider validates the string and then decides whether they should revoke the secret, issue a new secret, or contact you directly. Their action will depend on the associated risks to you or them. +Reference: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning +Detection: + - Sequence: + - ID: SecretFound + RuleID: GitHub.Secret.Scanning.Alert.Created + - ID: SecretNotQuarantined + RuleID: AWS.CloudTrail.IAMCompromisedKeyQuarantine + Absence: true + Transitions: + - ID: SecretFound TO SecretNotQuarantined + From: SecretFound + To: SecretNotQuarantined + Schedule: + RateMinutes: 10 + TimeoutMinutes: 2 + LookbackWindowMinutes: 30 +Tests: + - Name: Secret Found and Quarantied + ExpectedResult: false + RuleOutputs: + - ID: SecretFound + Matches: # The match field is required when using timestamps, so I use a dummy field name + foo: + bar: [0] + - ID: SecretNotQuarantined + Matches: + foo: + bar: [30] + - Name: Secret Found and Not Quarantied + ExpectedResult: true + RuleOutputs: + - ID: SecretFound + Matches: + foo: + bar: [0] \ No newline at end of file diff --git a/correlation_rules/snowflake_data_exfiltration.yml b/correlation_rules/snowflake_data_exfiltration.yml new file mode 100644 index 000000000..ac0e0929c --- /dev/null +++ b/correlation_rules/snowflake_data_exfiltration.yml @@ -0,0 +1,70 @@ +AnalysisType: correlation_rule +RuleID: "Snowflake.Data.Exfiltration" +DisplayName: "Snowflake Data Exfiltration" +Enabled: true +Severity: Critical +Description: In April 2024, Mandiant received threat intelligence on database records that were subsequently determined to have originated from a victim’s Snowflake instance. Mandiant notified the victim, who then engaged Mandiant to investigate suspected data theft involving their Snowflake instance. During this investigation, Mandiant determined that the organization’s Snowflake instance had been compromised by a threat actor using credentials previously stolen via infostealer malware. The threat actor used these stolen credentials to access the customer’s Snowflake instance and ultimately exfiltrate valuable data. At the time of the compromise, the account did not have multi-factor authentication (MFA) enabled. +Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Reports: + MITRE ATT&CK: + - TA0010:T1041 # Exfiltration Over C2 Channel +Detection: + - Sequence: + - ID: SnowflakeTempStageCreated + RuleID: Snowflake.TempStageCreated + - ID: SnowflakeCopyIntoStage + RuleID: Snowflake.CopyIntoStage + - ID: SnowflakeFileDownloaded + RuleID: Snowflake.FileDownloaded + Transitions: + - ID: Match SnowflakeTempStageCreated and SnowflakeCopyIntoStage on stage + From: SnowflakeTempStageCreated + To: SnowflakeCopyIntoStage + Match: + - On: stage + - ID: Match SnowflakeCopyIntoStage and SnowflakeFileDownloaded on path + From: SnowflakeCopyIntoStage + To: SnowflakeFileDownloaded + Match: + - On: stage + Schedule: + RateMinutes: 720 + TimeoutMinutes: 2 + LookbackWindowMinutes: 1440 +Tests: + - Name: Data Exfiltration + ExpectedResult: true + RuleOutputs: + - ID: SnowflakeTempStageCreated + Matches: + stage: + LOGS.PUBLIC.data_exfil: + - "2006-01-02T15:04:05Z" + - "2006-01-02T15:04:06Z" + - ID: SnowflakeCopyIntoStage + Matches: + stage: + LOGS.PUBLIC.data_exfil: + - "2006-01-02T15:04:05Z" + - "2006-01-02T15:04:06Z" + - ID: SnowflakeFileDownloaded + Matches: + stage: + LOGS.PUBLIC.data_exfil: + - "2006-01-02T15:04:05Z" + - "2006-01-02T15:04:06Z" + - Name: Data Staged but not Downloaded + ExpectedResult: false + RuleOutputs: + - ID: SnowflakeTempStageCreated + Matches: + stage: + LOGS.PUBLIC.data_exfil: + - "2006-01-02T15:04:05Z" + - "2006-01-02T15:04:06Z" + - ID: SnowflakeCopyIntoStage + Matches: + stage: + LOGS.PUBLIC.data_exfil: + - "2006-01-02T15:04:05Z" + - "2006-01-02T15:04:06Z" diff --git a/global_helpers/crowdstrike_event_streams_helpers.py b/global_helpers/crowdstrike_event_streams_helpers.py new file mode 100644 index 000000000..ac0b9da08 --- /dev/null +++ b/global_helpers/crowdstrike_event_streams_helpers.py @@ -0,0 +1,7 @@ +from panther_base_helpers import key_value_list_to_dict + + +def cs_alert_context(event): + return key_value_list_to_dict( + event.deep_get("event", "AuditKeyValues", default=[]), "Key", "ValueString" + ) diff --git a/global_helpers/crowdstrike_event_streams_helpers.yml b/global_helpers/crowdstrike_event_streams_helpers.yml new file mode 100644 index 000000000..d27678594 --- /dev/null +++ b/global_helpers/crowdstrike_event_streams_helpers.yml @@ -0,0 +1,5 @@ +AnalysisType: global +Filename: crowdstrike_event_streams_helpers.py +GlobalID: "crowdstrike_event_streams_helpers" +Description: > + Helpers for Crowdstrike Event Streams detections. diff --git a/global_helpers/panther_base_helpers.py b/global_helpers/panther_base_helpers.py index 15026d53e..856eb5115 100644 --- a/global_helpers/panther_base_helpers.py +++ b/global_helpers/panther_base_helpers.py @@ -529,3 +529,10 @@ def is_base64(b64: str) -> str: except UnicodeDecodeError: pass return "" + + +def key_value_list_to_dict(list_objects: List[dict], key: str, value: str) -> dict: + # Convert a list of dictionaries to a single dictionary + # example: [{'key': 'a', 'value': 1}, {'key': 'b', 'value': 2}] + # becomes: {'a': 1, 'b': 2} + return {item[key]: item[value] for item in list_objects} diff --git a/packs/aws.yml b/packs/aws.yml index 5fd6dc10a..0402ce05e 100644 --- a/packs/aws.yml +++ b/packs/aws.yml @@ -103,6 +103,8 @@ PackDefinition: - AWS.DynamoDB.Autoscaling - AWS.EC2.Instance.EBSOptimization - AWS.EC2.Startup.Script.Change + - AWS.EC2.StopInstances + - AWS.EC2.StopInstance.FOLLOWED.BY.ModifyInstanceAttributes - AWS.ELBV2.LoadBalancer.HasSSLPolicy - AWS.ELBv2.SSLPolicy - AWS.GuardDuty.Enabled @@ -161,6 +163,16 @@ PackDefinition: - CloudTrail.Password.Spraying - VPC.DNS.Tunneling - VPCFlow.Port.Scanning + # Correlation Rules + - AWS.Potentially.Stolen.Service.Role + - AWS.Privilege.Escalation.Via.User.Compromise + - AWS.User.Takeover.Via.Password.Reset + # Signal Rules + - Role.Assumed.by.AWS.Service + - Role.Assumed.by.User + - AWS.CloudTrail.UserAccessKeyAuth + - AWS.CloudTrail.LoginProfileCreatedOrModified + - AWS.Console.Login # AWS DataModels - Standard.AWS.ALB diff --git a/packs/crowdstrike.yml b/packs/crowdstrike.yml index 01dd0bfac..5ce41a668 100644 --- a/packs/crowdstrike.yml +++ b/packs/crowdstrike.yml @@ -1,6 +1,6 @@ AnalysisType: pack PackID: PantherManaged.Crowdstrike -Description: Group of all Crowdstrike detections +Description: Group of all Crowdstrike FDR detections PackDefinition: IDs: - Crowdstrike.Detection.passthrough diff --git a/packs/crowdstrike_event_streams.yml b/packs/crowdstrike_event_streams.yml new file mode 100644 index 000000000..cc88ef530 --- /dev/null +++ b/packs/crowdstrike_event_streams.yml @@ -0,0 +1,9 @@ +AnalysisType: pack +PackID: PantherManaged.CrowdstrikeEventStreams +Description: Group of all Crowdstrike Event Stream detections +PackDefinition: + IDs: + - Crowdstrike.API.Key.Created + - Crowdstrike.API.Key.Deleted + - panther_base_helpers + - crowdstrike_event_streams_helpers \ No newline at end of file diff --git a/packs/gcp_audit.yml b/packs/gcp_audit.yml index e99ff6f84..85b7271e5 100644 --- a/packs/gcp_audit.yml +++ b/packs/gcp_audit.yml @@ -10,6 +10,9 @@ PackDefinition: - GCP.CloudBuild.Potential.Privilege.Escalation - GCP.Cloudfunctions.Functions.Create - GCP.Cloudfunctions.Functions.Update + - GCP.Cloud.Run.Service.Created + - GCP.Cloud.Run.Service.Created.FOLLOWED.BY.Set.IAM.Policy + - GCP.Cloud.Run.Set.IAM.Policy - GCP.Destructive.Queries - GCP.DNS.Zone.Modified.or.Deleted - GCP.Firewall.Rule.Created diff --git a/packs/github.yml b/packs/github.yml index bb5de9948..a5d8a650f 100644 --- a/packs/github.yml +++ b/packs/github.yml @@ -5,6 +5,7 @@ DisplayName: "Panther GitHub Audit Pack" PackDefinition: IDs: - GitHub.Advanced.Security.Change + - GitHub.Advanced.Security.Change.NOT.FOLLOWED.BY.Repo.Archived - GitHub.Branch.PolicyOverride - GitHub.Branch.ProtectionDisabled - GitHub.Org.AuthChange diff --git a/packs/multisource_correlations.yml b/packs/multisource_correlations.yml new file mode 100644 index 000000000..78277452b --- /dev/null +++ b/packs/multisource_correlations.yml @@ -0,0 +1,19 @@ +AnalysisType: pack +PackID: PantherManaged.MultiSourceCorrelations +Description: DO NOT ENABLE THIS PACK! This pack contains rules that require multiple log sources to correlate. It is not intended to be enabled unless you have all the required log sources. Please enable individual rules for which you have all the required log sources. +DisplayName: "Panther Multi-Source Correlations Pack" +PackDefinition: + IDs: + # AWS + Okta + - Secret.Exposed.and.not.Quarantined + - GitHub.Secret.Scanning.Alert.Created + - AWS.CloudTrail.IAMCompromisedKeyQuarantine + - global_filter_github + + # Okta + Push Security + - Okta.Login.Without.Push + - Potential.Compromised.Okta.Credentials + - Okta.Login.Success + - Push.Security.Authorized.IdP.Login + - Okta.Login.Without.Push.Marker + - Push.Security.Phishing.Attack \ No newline at end of file diff --git a/packs/snowflake.yml b/packs/snowflake.yml index f68eab223..d8ae02c1d 100644 --- a/packs/snowflake.yml +++ b/packs/snowflake.yml @@ -13,10 +13,13 @@ PackDefinition: - Query.Snowflake.BruteForceByIp - Query.Snowflake.BruteForceByUsername - Query.Snowflake.ClientIp + - Query.Snowflake.CopyIntoStage - Query.Snowflake.External.Shares + - Query.Snowflake.FileDownloaded - Query.Snowflake.KeyUserPasswordLogin - Query.Snowflake.Multiple.Logins.Followed.By.Success - Query.Snowflake.SuspectedUserAccess + - Query.Snowflake.TempStageCreated - Query.Snowflake.UserCreated - Query.Snowflake.UserEnabled # Rules @@ -25,9 +28,13 @@ PackDefinition: - Snowflake.BruteForceByUsername - Snowflake.Client.IP - Snowflake.Configuration.Drift + - Snowflake.CopyIntoStage + - Snowflake.Data.Exfiltration - Snowflake.External.Shares + - Snowflake.FileDownloaded - Snowflake.KeyUserPasswordLogin - Snowflake.Multiple.Failed.Logins.Followed.By.Success + - Snowflake.TempStageCreated - Snowflake.User.Access - Snowflake.UserCreated - Snowflake.UserEnabled diff --git a/queries/aws_queries/cloudtrail_2_minute_count_query.yml b/queries/aws_queries/cloudtrail_2_minute_count_query.yml index e22e33ac4..ba55b67df 100644 --- a/queries/aws_queries/cloudtrail_2_minute_count_query.yml +++ b/queries/aws_queries/cloudtrail_2_minute_count_query.yml @@ -2,11 +2,11 @@ AnalysisType: scheduled_query Enabled: false Query: |- SELECT - count(*) as num_logs, p_log_type + count(*) as num_logs, p_log_type FROM - panther_logs.public.aws_cloudtrail + panther_logs.public.aws_cloudtrail WHERE - p_occurs_since('5m') + p_occurs_since('5m') GROUP BY p_log_type QueryName: "AWS CloudTrail 2-minute count" Schedule: diff --git a/queries/snowflake_queries/scheduled_rule_default_snowflake.py b/queries/snowflake_queries/scheduled_rule_default_snowflake.py new file mode 100644 index 000000000..7a0c3657f --- /dev/null +++ b/queries/snowflake_queries/scheduled_rule_default_snowflake.py @@ -0,0 +1,2 @@ +def rule(_): + return True diff --git a/queries/snowflake_queries/snowflake_file_downloaded_query.yml b/queries/snowflake_queries/snowflake_file_downloaded_query.yml new file mode 100644 index 000000000..e957fdfe6 --- /dev/null +++ b/queries/snowflake_queries/snowflake_file_downloaded_query.yml @@ -0,0 +1,26 @@ +AnalysisType: scheduled_query +QueryName: "Query.Snowflake.FileDownloaded" +Enabled: true +Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Query: > + SELECT + user_name, + role_name, + start_time AS p_event_time, + query_type, + execution_status, + regexp_substr(query_text, 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 2) as stage, + regexp_substr(query_text, 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s', 1, 1, 'i', 2) as path, + query_text + + FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY + WHERE query_type = 'GET_FILES' + AND path IS NOT NULL + AND p_occurs_since('1 day') + AND execution_status = 'SUCCESS' + LIMIT 100 +Schedule: + RateMinutes: 1440 + TimeoutMinutes: 1 +Tags: + - data exfil diff --git a/queries/snowflake_queries/snowflake_file_downloaded_signal.yml b/queries/snowflake_queries/snowflake_file_downloaded_signal.yml new file mode 100644 index 000000000..8538a1c7b --- /dev/null +++ b/queries/snowflake_queries/snowflake_file_downloaded_signal.yml @@ -0,0 +1,29 @@ +AnalysisType: scheduled_rule +Filename: scheduled_rule_default_snowflake.py +RuleID: "Snowflake.FileDownloaded" +Description: > + A file was downloaded from a stage +DisplayName: "Snowflake File Downloaded" +Enabled: true +CreateAlert: false +Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Reports: + MITRE ATT&CK: + - TA0010:T1041 # Exfiltration Over C2 Channel +ScheduledQueries: + - Query.Snowflake.FileDownloaded +Severity: Info +Tests: + - Name: Value Returned By Query + ExpectedResult: true + Log: + { + "execution_status": "SUCCESS", + "path": "LOGS.PUBLIC.data_exfil/DATA.csv", + "query_text": "GET '@LOGS.PUBLIC.data_exfil/DATA.csv' 'file:///Users/evil.genius/Documents'", + "query_type": "GET_FILES", + "role_name": "SYSADMIN", + "stage": "LOGS.PUBLIC.data_exfil", + "start_time": "2024-06-10 19:37:15.698Z", + "user_name": "ADMIN" + } diff --git a/queries/snowflake_queries/snowflake_table_copied_into_stage_query.yml b/queries/snowflake_queries/snowflake_table_copied_into_stage_query.yml new file mode 100644 index 000000000..8f1c3e3c8 --- /dev/null +++ b/queries/snowflake_queries/snowflake_table_copied_into_stage_query.yml @@ -0,0 +1,26 @@ +AnalysisType: scheduled_query +QueryName: "Query.Snowflake.CopyIntoStage" +Enabled: true +Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Query: > + SELECT + user_name, + role_name, + start_time AS p_event_time, + query_type, + execution_status, + regexp_substr(query_text, 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 2) as stage, + regexp_substr(query_text, 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s+FROM', 1, 1, 'i', 2) as path, + query_text + + FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY + WHERE query_type = 'UNLOAD' + AND stage IS NOT NULL + AND p_occurs_since('1 day') + AND execution_status = 'SUCCESS' + LIMIT 100 +Schedule: + RateMinutes: 1440 + TimeoutMinutes: 1 +Tags: + - data exfil diff --git a/queries/snowflake_queries/snowflake_table_copied_into_stage_signal.yml b/queries/snowflake_queries/snowflake_table_copied_into_stage_signal.yml new file mode 100644 index 000000000..5f019977a --- /dev/null +++ b/queries/snowflake_queries/snowflake_table_copied_into_stage_signal.yml @@ -0,0 +1,29 @@ +AnalysisType: scheduled_rule +Filename: scheduled_rule_default_snowflake.py +RuleID: "Snowflake.CopyIntoStage" +Description: > + A table was copied into a stage +DisplayName: "Snowflake Table Copied Into Stage" +Enabled: true +CreateAlert: false +Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Reports: + MITRE ATT&CK: + - TA0010:T1041 # Exfiltration Over C2 Channel +ScheduledQueries: + - Query.Snowflake.CopyIntoStage +Severity: Info +Tests: + - Name: Value Returned By Query + ExpectedResult: true + Log: + { + "execution_status": "SUCCESS", + "path": "LOGS.PUBLIC.data_exfil/DATA.csv", + "query_text": "COPY INTO @LOGS.PUBLIC.data_exfil/DATA.csv\nFROM (SELECT * FROM PANTHER_LOGS.PUBLIC.GITLAB_API_VARIANT LIMIT 100)\nFILE_FORMAT = ( \n TYPE='CSV' \n COMPRESSION=GZIP\n FIELD_DELIMITER=',' \n ESCAPE=NONE \n ESCAPE_UNENCLOSED_FIELD=NONE \n date_format='AUTO' \n time_format='AUTO' \n timestamp_format='AUTO'\n binary_format='UTF-8' \n field_optionally_enclosed_by='\"' \n null_if='' \n EMPTY_FIELD_AS_NULL = FALSE \n) \noverwrite=TRUE \nsingle=FALSE \nmax_file_size=5368709120 \nheader=TRUE", + "query_type": "UNLOAD", + "role_name": "SYSADMIN", + "stage": "LOGS.PUBLIC.data_exfil", + "start_time": "2024-06-10 19:15:37.445Z", + "user_name": "ADMIN" + } diff --git a/queries/snowflake_queries/snowflake_temp_stage_created_query.yml b/queries/snowflake_queries/snowflake_temp_stage_created_query.yml new file mode 100644 index 000000000..28046741c --- /dev/null +++ b/queries/snowflake_queries/snowflake_temp_stage_created_query.yml @@ -0,0 +1,25 @@ +AnalysisType: scheduled_query +QueryName: "Query.Snowflake.TempStageCreated" +Enabled: true +Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Query: > + SELECT + user_name, + role_name, + start_time AS p_event_time, + query_type, + execution_status, + regexp_substr(query_text, 'CREATE\\s+(OR\\s+REPLACE\\s+)?(TEMPORARY\\s+|TEMP\\s+)STAGE\\s+(IF\\s+NOT\\s+EXISTS\\s+)?([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 4) as stage, + query_text + + FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY + WHERE query_type = 'CREATE' + AND stage IS NOT NULL + AND p_occurs_since('1 day') + AND execution_status = 'SUCCESS' + LIMIT 100 +Schedule: + RateMinutes: 1440 + TimeoutMinutes: 1 +Tags: + - data exfil diff --git a/queries/snowflake_queries/snowflake_temp_stage_created_signal.yml b/queries/snowflake_queries/snowflake_temp_stage_created_signal.yml new file mode 100644 index 000000000..0e95ba8d8 --- /dev/null +++ b/queries/snowflake_queries/snowflake_temp_stage_created_signal.yml @@ -0,0 +1,28 @@ +AnalysisType: scheduled_rule +Filename: scheduled_rule_default_snowflake.py +RuleID: "Snowflake.TempStageCreated" +Description: > + A temporary stage was created +DisplayName: "Snowflake Temporary Stage Created" +Enabled: true +CreateAlert: false +Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/ +Reports: + MITRE ATT&CK: + - TA0010:T1041 # Exfiltration Over C2 Channel +ScheduledQueries: + - Query.Snowflake.TempStageCreated +Severity: Info +Tests: + - Name: Value Returned By Query + ExpectedResult: true + Log: + { + "execution_status": "SUCCESS", + "query_text": "CREATE OR REPLACE TEMP STAGE logs.PUBLIC.data_exfil", + "query_type": "CREATE", + "role_name": "SYSADMIN", + "stage": "logs.PUBLIC.data_exfil", + "start_time": "2024-06-10 19:48:52.068Z", + "user_name": "ADMIN" + } diff --git a/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.py b/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.py new file mode 100644 index 000000000..2b3fc2551 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.py @@ -0,0 +1,33 @@ +from panther_base_helpers import aws_rule_context, deep_get + +PROFILE_EVENTS = { + "UpdateLoginProfile", + "CreateLoginProfile", +} + + +def rule(event): + # Only look for successes + if event.get("errorCode") or event.get("errorMessage"): + return False + + # Check when someone other than the user themselves creates or modifies a login profile + return ( + event.get("eventSource", "") == "iam.amazonaws.com" + and event.get("eventName", "") in PROFILE_EVENTS + and not deep_get(event, "userIdentity", "arn", default="").endswith( + f"/{deep_get(event, 'requestParameters', 'userName', default='')}" + ) + ) + + +def title(event): + return ( + f"[{deep_get(event, 'userIdentity', 'arn')}] " + f"changed the password for " + f"[{deep_get(event, 'requestParameters','userName')}]" + ) + + +def alert_context(event): + return aws_rule_context(event) diff --git a/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.yml b/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.yml new file mode 100644 index 000000000..a1e2c115a --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_cloudtrail_loginprofilecreatedormodified.yml @@ -0,0 +1,93 @@ +AnalysisType: rule +Filename: aws_cloudtrail_loginprofilecreatedormodified.py +RuleID: "AWS.CloudTrail.LoginProfileCreatedOrModified" +DisplayName: "AWS User Login Profile Created or Modified" +Enabled: true +LogTypes: + - AWS.CloudTrail +Severity: Low +Reports: + MITRE ATT&CK: + - TA0003:T1098 + - TA0005:T1108 + - TA0005:T1550 + - TA0008:T1550 +Description: An attacker with iam:UpdateLoginProfile permission on other users can change the password used to login to the AWS console. May be legitimate account administration. +DedupPeriodMinutes: 60 +Threshold: 1 +Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-pass-accesskeys-ssh.html +Tests: + - Name: ChangeOwnPassword + ExpectedResult: false + Log: + awsRegion: us-east-1 + eventCategory: Management + eventID: "1234" + eventName: UpdateLoginProfile + eventSource: iam.amazonaws.com + eventTime: "2022-09-15 13:45:24" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + readOnly: false + recipientAccountId: "987654321" + requestParameters: + passwordResetRequired: false + userName: alice + sessionCredentialFromConsole: true + sourceIPAddress: AWS Internal + userAgent: AWS Internal + userIdentity: + accessKeyId: ABC1234 + accountId: "987654321" + arn: arn:aws:sts::98765432:assumed-role/IAM/alice + principalId: ABCDE:alice + sessionContext: + attributes: + creationDate: "2022-09-15T13:36:47Z" + mfaAuthenticated: "true" + sessionIssuer: + accountId: "987654321" + arn: arn:aws:iam::9876432:role/IAM + principalId: 1234ABC + type: Role + userName: IAM + webIdFederationData: {} + type: AssumedRole + - Name: User changed password for other + ExpectedResult: true + Log: + awsRegion: us-east-1 + eventCategory: Management + eventID: "1234" + eventName: UpdateLoginProfile + eventSource: iam.amazonaws.com + eventTime: "2022-09-15 13:45:24" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + readOnly: false + recipientAccountId: "987654321" + requestParameters: + passwordResetRequired: false + userName: bob + sessionCredentialFromConsole: true + sourceIPAddress: AWS Internal + userAgent: AWS Internal + userIdentity: + accessKeyId: ABC1234 + accountId: "987654321" + arn: arn:aws:sts::98765432:assumed-role/IAM/alice + principalId: ABCDE:alice + sessionContext: + attributes: + creationDate: "2022-09-15T13:36:47Z" + mfaAuthenticated: "true" + sessionIssuer: + accountId: "987654321" + arn: arn:aws:iam::9876432:role/IAM + principalId: 1234ABC + type: Role + userName: IAM + webIdFederationData: {} + type: AssumedRole diff --git a/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.py b/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.py new file mode 100644 index 000000000..6b41dadc0 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.py @@ -0,0 +1,20 @@ +def rule(event): + # Only look for successes + if event.get("errorCode") or event.get("errorMessage"): + return False + # Reference: https://awsteele.com/blog/2020/09/26/aws-access-key-format.html + return event.deep_get("userIdentity", "accessKeyId").startswith("AKIA") + + +def title(event): + arn = event.deep_get("userIdentity", "arn") + key = event.deep_get("userIdentity", "accessKeyId") + return f"User {arn} signed in with access key {key}" + + +def alert_context(event): + return { + "ip_accessKeyId": event.get("sourceIpAddress") + + ":" + + event.deep_get("userIdentity", "accessKeyId") + } diff --git a/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.yml b/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.yml new file mode 100644 index 000000000..9c1aed1af --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_cloudtrail_useraccesskeyauth.yml @@ -0,0 +1,208 @@ +AnalysisType: rule +Filename: aws_cloudtrail_useraccesskeyauth.py +RuleID: "AWS.CloudTrail.UserAccessKeyAuth" +DisplayName: "AWS.CloudTrail.UserAccessKeyAuth" +Enabled: true +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +InlineFilters: + - All: [] +Tests: + - Name: Access Key Action Failed + ExpectedResult: false + Log: + awsRegion: us-east-1 + errorCode: AccessDenied + errorMessage: 'User: arn:aws:iam::187901811700:user/exposed.user is not authorized to perform: lambda:ListFunctions on resource: * because no identity-based policy allows the lambda:ListFunctions action' + eventCategory: Management + eventID: 57b719ad-f2fa-4d18-bd84-29fee11799b1 + eventName: ListFunctions20150331 + eventSource: lambda.amazonaws.com + eventTime: "2024-06-02 20:16:32.000000000" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + p_any_actor_ids: + - AIDASXP6SDP2AJQPWVFII + p_any_aws_account_ids: + - "187901811700" + p_any_aws_arns: + - arn:aws:iam::187901811700:user/exposed.user + p_any_ip_addresses: + - 73.252.165.138 + p_any_trace_ids: + - AKIASXP6SDP2F3JQERZ2 + p_any_usernames: + - exposed.user + p_event_time: "2024-06-02 20:16:32.000000000" + p_log_type: AWS.CloudTrail + p_parse_time: "2024-06-02 20:25:54.290533382" + p_row_id: f26228572e3f9acd88e0cadc1fcc8e0a + p_schema_version: 0 + p_source_file: + aws_s3_bucket: threat-research-trail-trail-bucket-0ipb5nzxam + aws_s3_key: AWSLogs/187901811700/CloudTrail/us-east-1/2024/06/02/187901811700_CloudTrail_us-east-1_20240602T2020Z_12tLndRWeXi4IWKg.json.gz + p_source_id: d0a1e235-6548-4e7f-952a-35063b304007 + p_source_label: threat-research-trail-us-east-1 + p_udm: + source: + address: 73.252.165.138 + ip: 73.252.165.138 + user: + arns: + - arn:aws:iam::187901811700:user/exposed.user + name: exposed.user + readOnly: true + recipientAccountId: "187901811700" + requestID: ea7dbdf9-6c03-421e-abf4-f201919f9f26 + sourceIPAddress: 73.252.165.138 + tlsDetails: + cipherSuite: TLS_AES_128_GCM_SHA256 + clientProvidedHostHeader: lambda.us-east-1.amazonaws.com + tlsVersion: TLSv1.3 + userAgent: aws-cli/2.15.59 md/awscrt#0.19.19 ua/2.0 os/macos#22.6.0 md/arch#arm64 lang/python#3.11.9 md/pyimpl#CPython cfg/retry-mode#standard md/installer#source md/prompt#off md/command#lambda.list-functions + userIdentity: + accessKeyId: AKIASXP6SDP2F3JQERZ2 + accountId: "187901811700" + arn: arn:aws:iam::187901811700:user/exposed.user + principalId: AIDASXP6SDP2AJQPWVFII + type: IAMUser + userName: exposed.user + - Name: Access Key Action Success + ExpectedResult: true + Log: + awsRegion: us-east-1 + eventCategory: Management + eventID: 4c24450d-007e-4849-9e1b-4954622dbb08 + eventName: GetCallerIdentity + eventSource: sts.amazonaws.com + eventTime: "2024-06-02 20:16:22.000000000" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + p_any_actor_ids: + - AIDASXP6SDP2AJQPWVFII + p_any_aws_account_ids: + - "187901811700" + p_any_aws_arns: + - arn:aws:iam::187901811700:user/exposed.user + p_any_ip_addresses: + - 73.252.165.138 + p_any_trace_ids: + - AKIASXP6SDP2F3JQERZ2 + p_any_usernames: + - exposed.user + p_event_time: "2024-06-02 20:16:22.000000000" + p_log_type: AWS.CloudTrail + p_parse_time: "2024-06-02 20:25:54.289981899" + p_row_id: f26228572e3f9acd88e0cadc1fc88e0a + p_schema_version: 0 + p_source_file: + aws_s3_bucket: threat-research-trail-trail-bucket-0ipb5nzxam + aws_s3_key: AWSLogs/187901811700/CloudTrail/us-east-1/2024/06/02/187901811700_CloudTrail_us-east-1_20240602T2020Z_12tLndRWeXi4IWKg.json.gz + p_source_id: d0a1e235-6548-4e7f-952a-35063b304007 + p_source_label: threat-research-trail-us-east-1 + p_udm: + source: + address: 73.252.165.138 + ip: 73.252.165.138 + user: + arns: + - arn:aws:iam::187901811700:user/exposed.user + name: exposed.user + readOnly: true + recipientAccountId: "187901811700" + requestID: c77ed3ee-8480-41df-98cb-9cf52ce04a1c + sourceIPAddress: 73.252.165.138 + tlsDetails: + cipherSuite: TLS_AES_128_GCM_SHA256 + clientProvidedHostHeader: sts.us-east-1.amazonaws.com + tlsVersion: TLSv1.3 + userAgent: aws-cli/2.15.59 md/awscrt#0.19.19 ua/2.0 os/macos#22.6.0 md/arch#arm64 lang/python#3.11.9 md/pyimpl#CPython cfg/retry-mode#standard md/installer#source md/prompt#off md/command#sts.get-caller-identity + userIdentity: + accessKeyId: AKIASXP6SDP2F3JQERZ2 + accountId: "187901811700" + arn: arn:aws:iam::187901811700:user/exposed.user + principalId: AIDASXP6SDP2AJQPWVFII + type: IAMUser + userName: exposed.user + - Name: Console Login + ExpectedResult: false + Log: + additionalEventData: + MFAUsed: "No" + MobileVersion: "No" + awsRegion: us-west-2 + eventCategory: Management + eventID: 364ad368-42bf-4e05-a500-971ddfe8ebff + eventName: ConsoleLogin + eventSource: signin.amazonaws.com + eventTime: "2024-06-02 19:41:40.000000000" + eventType: AwsConsoleSignIn + eventVersion: "1.08" + managementEvent: true + p_any_actor_ids: + - AROASXP6SDP2F4WLQVARB + - AROASXP6SDP2F4WLQVARB:nicholas.hakmiller + p_any_aws_account_ids: + - "187901811700" + p_any_aws_arns: + - arn:aws:iam::187901811700:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin_635426549a280cc6 + - arn:aws:sts::187901811700:assumed-role/AWSReservedSSO_DevAdmin_635426549a280cc6/nicholas.hakmiller + - arn:aws:iam::187901811700:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin_635426549a280cc6 + p_any_ip_addresses: + - 73.252.165.138 + p_any_trace_ids: + - ASIASXP6SDP2HUZY3TOB + p_any_usernames: + - AWSReservedSSO_DevAdmin_635426549a280cc6 + - nicholas.hakmiller + p_event_time: "2024-06-02 19:41:40.000000000" + p_log_type: AWS.CloudTrail + p_parse_time: "2024-06-02 19:50:54.391407154" + p_row_id: f26228572e3f9acd88e0cadc1f80a502 + p_schema_version: 0 + p_source_file: + aws_s3_bucket: threat-research-trail-trail-bucket-xhh4yndpq5 + aws_s3_key: AWSLogs/187901811700/CloudTrail/us-west-2/2024/06/02/187901811700_CloudTrail_us-west-2_20240602T1945Z_ggfzMNc1AHPJypqR.json.gz + p_source_id: 469edf86-a0c6-4d13-ba48-47c4060bb804 + p_source_label: threat-research-trail-us-west-2 + p_udm: + source: + address: 73.252.165.138 + ip: 73.252.165.138 + user: + arns: + - arn:aws:iam::187901811700:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin_635426549a280cc6 + - arn:aws:sts::187901811700:assumed-role/AWSReservedSSO_DevAdmin_635426549a280cc6/nicholas.hakmiller + readOnly: false + recipientAccountId: "187901811700" + responseElements: + ConsoleLogin: Success + sourceIPAddress: 73.252.165.138 + tlsDetails: + cipherSuite: TLS_AES_128_GCM_SHA256 + clientProvidedHostHeader: us-west-2.signin.aws.amazon.com + tlsVersion: TLSv1.3 + userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 + userIdentity: + accessKeyId: ASIASXP6SDP2HUZY3TOB + accountId: "187901811700" + arn: arn:aws:sts::187901811700:assumed-role/AWSReservedSSO_DevAdmin_635426549a280cc6/nicholas.hakmiller + principalId: AROASXP6SDP2F4WLQVARB:nicholas.hakmiller + sessionContext: + attributes: + creationDate: "2024-06-02T19:41:40Z" + mfaAuthenticated: "false" + sessionIssuer: + accountId: "187901811700" + arn: arn:aws:iam::187901811700:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin_635426549a280cc6 + principalId: AROASXP6SDP2F4WLQVARB + type: Role + userName: AWSReservedSSO_DevAdmin_635426549a280cc6 + webIdFederationData: {} + type: AssumedRole +CreateAlert: false diff --git a/rules/aws_cloudtrail_rules/aws_console_login.py b/rules/aws_cloudtrail_rules/aws_console_login.py new file mode 100644 index 000000000..9e0dfe8f0 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_console_login.py @@ -0,0 +1,2 @@ +def rule(event): + return event.get("eventName") == "ConsoleLogin" diff --git a/rules/aws_cloudtrail_rules/aws_console_login.yml b/rules/aws_cloudtrail_rules/aws_console_login.yml new file mode 100644 index 000000000..861b88c00 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_console_login.yml @@ -0,0 +1,11 @@ +AnalysisType: rule +Filename: aws_console_login.py +RuleID: "AWS.Console.Login" +DisplayName: "AWS Console Login" +Enabled: true +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +CreateAlert: false diff --git a/rules/aws_cloudtrail_rules/aws_console_signin.py b/rules/aws_cloudtrail_rules/aws_console_signin.py new file mode 100644 index 000000000..b01916c83 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_console_signin.py @@ -0,0 +1,4 @@ +def rule(event): + return ( + event.get("eventSource") == "sso.amazonaws.com" and event.get("eventName") == "Authenticate" + ) diff --git a/rules/aws_cloudtrail_rules/aws_console_signin.yml b/rules/aws_cloudtrail_rules/aws_console_signin.yml new file mode 100644 index 000000000..a662f3ca5 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_console_signin.yml @@ -0,0 +1,24 @@ +AnalysisType: rule +Filename: aws_console_signin.py +RuleID: "AWS.Console.Sign-In" +DisplayName: "SIGNAL - AWS Console SSO Sign-In" +Enabled: true +CreateAlert: false +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Test-94439c + ExpectedResult: true + Log: + awsRegion: us-east-1 + eventCategory: Management + eventID: 8cb05708-9764-4774-a048-59a4c8e1684d + eventName: Authenticate + eventSource: sso.amazonaws.com + eventTime: "2024-06-03 15:23:22.000000000" + eventType: AwsServiceEvent + eventVersion: "1.08" + managementEvent: true diff --git a/rules/aws_cloudtrail_rules/aws_ec2_startup_script_change.py b/rules/aws_cloudtrail_rules/aws_ec2_startup_script_change.py index fe7883d03..b41b045d9 100644 --- a/rules/aws_cloudtrail_rules/aws_ec2_startup_script_change.py +++ b/rules/aws_cloudtrail_rules/aws_ec2_startup_script_change.py @@ -23,4 +23,6 @@ def dedup(event): def alert_context(event): - return aws_rule_context(event) + context = aws_rule_context(event) + context["instance_ids"] = [deep_get(event, "requestParameters", "instanceId"), "no_instance_id"] + return context diff --git a/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.py b/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.py new file mode 100644 index 000000000..cdb5c6e35 --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.py @@ -0,0 +1,29 @@ +from panther_base_helpers import aws_rule_context + + +def rule(event): + return all( + [ + not event.get("errorCode"), + not event.get("errorMessage"), + event.get("eventName") == "StopInstances", + ] + ) + + +def title(event): + instances = [ + instance["instanceId"] + for instance in event.deep_get("requestParameters", "instancesSet", "items", default=[]) + ] + account = event.get("recipientAccountId") + return f"EC2 instances {instances} stopped in account {account}." + + +def alert_context(event): + context = aws_rule_context(event) + context["instance_ids"] = [ + instance["instanceId"] + for instance in event.deep_get("requestParameters", "instancesSet", "items", default=[]) + ] + return context diff --git a/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.yml b/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.yml new file mode 100644 index 000000000..90332e42c --- /dev/null +++ b/rules/aws_cloudtrail_rules/aws_ec2_stopinstances.yml @@ -0,0 +1,238 @@ +AnalysisType: rule +RuleID: "AWS.EC2.StopInstances" +DisplayName: "CloudTrail EC2 StopInstances" +Enabled: true +CreateAlert: false +Filename: aws_ec2_stopinstances.py +LogTypes: + - AWS.CloudTrail +Tags: + - panther-signal +Severity: Info +Description: > + A CloudTrail instances were stopped. It makes further changes of instances possible +Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-examples.html +Tests: + - + Name: CloudTrail Instances Were Stopped + ExpectedResult: true + Log: + { + "eventVersion": "1.08", + "userIdentity": { + "type": "IAMUser", + "principalId": "EXAMPLE6E4XEGITWATV6R", + "arn": "arn:aws:iam::777788889999:user/Nikki", + "accountId": "777788889999", + "accessKeyId": "AKIAI44QH8DHBEXAMPLE", + "userName": "Nikki", + "sessionContext": { + "sessionIssuer": { }, + "webIdFederationData": { }, + "attributes": { + "creationDate": "2023-07-19T21:11:57Z", + "mfaAuthenticated": "false" + } + } + }, + "eventTime": "2023-07-19T21:14:20Z", + "eventSource": "ec2.amazonaws.com", + "eventName": "StopInstances", + "awsRegion": "us-east-1", + "sourceIPAddress": "192.0.2.0", + "userAgent": "aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/ec2.stop-instances", + "requestParameters": { + "instancesSet": { + "items": [ + { + "instanceId": "i-EXAMPLE56126103cb" + }, + { + "instanceId": "i-EXAMPLEaff4840c22" + } + ] + }, + "force": false + }, + "responseElements": { + "requestId": "c308a950-e43e-444e-afc1-EXAMPLE73e49", + "instancesSet": { + "items": [ + { + "instanceId": "i-EXAMPLE56126103cb", + "currentState": { + "code": 64, + "name": "stopping" + }, + "previousState": { + "code": 16, + "name": "running" + } + }, + { + "instanceId": "i-EXAMPLEaff4840c22", + "currentState": { + "code": 64, + "name": "stopping" + }, + "previousState": { + "code": 16, + "name": "running" + } + } + ] + } + }, + "requestID": "c308a950-e43e-444e-afc1-EXAMPLE73e49", + "eventID": "9357a8cc-a0eb-46a1-b67e-EXAMPLE19b14", + "readOnly": false, + "eventType": "AwsApiCall", + "managementEvent": true, + "recipientAccountId": "777788889999", + "eventCategory": "Management", + "tlsDetails": { + "tlsVersion": "TLSv1.2", + "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", + "clientProvidedHostHeader": "ec2.us-east-1.amazonaws.com" + }, + "sessionCredentialFromConsole": "true" + } + - + Name: CloudTrail Instances Were Started + ExpectedResult: false + Log: + { + "eventVersion": "1.08", + "userIdentity": { + "type": "IAMUser", + "principalId": "EXAMPLE6E4XEGITWATV6R", + "arn": "arn:aws:iam::123456789012:user/Mateo", + "accountId": "123456789012", + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "userName": "Mateo", + "sessionContext": { + "sessionIssuer": { }, + "webIdFederationData": { }, + "attributes": { + "creationDate": "2023-07-19T21:11:57Z", + "mfaAuthenticated": "false" + } + } + }, + "eventTime": "2023-07-19T21:17:28Z", + "eventSource": "ec2.amazonaws.com", + "eventName": "StartInstances", + "awsRegion": "us-east-1", + "sourceIPAddress": "192.0.2.0", + "userAgent": "aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/ec2.start-instances", + "requestParameters": { + "instancesSet": { + "items": [ + { + "instanceId": "i-EXAMPLE56126103cb" + }, + { + "instanceId": "i-EXAMPLEaff4840c22" + } + ] + } + }, + "responseElements": { + "requestId": "e4336db0-149f-4a6b-844d-EXAMPLEb9d16", + "instancesSet": { + "items": [ + { + "instanceId": "i-EXAMPLEaff4840c22", + "currentState": { + "code": 0, + "name": "pending" + }, + "previousState": { + "code": 80, + "name": "stopped" + } + }, + { + "instanceId": "i-EXAMPLE56126103cb", + "currentState": { + "code": 0, + "name": "pending" + }, + "previousState": { + "code": 80, + "name": "stopped" + } + } + ] + } + }, + "requestID": "e4336db0-149f-4a6b-844d-EXAMPLEb9d16", + "eventID": "e755e09c-42f9-4c5c-9064-EXAMPLE228c7", + "readOnly": false, + "eventType": "AwsApiCall", + "managementEvent": true, + "recipientAccountId": "123456789012", + "eventCategory": "Management", + "tlsDetails": { + "tlsVersion": "TLSv1.2", + "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", + "clientProvidedHostHeader": "ec2.us-east-1.amazonaws.com" + }, + "sessionCredentialFromConsole": "true" + } + - + Name: Error Stopping CloudTrail Instances + ExpectedResult: false + Log: + { + "eventVersion": "1.05", + "errorCode": "SomeErrorCode", + "userIdentity": { + "type": "IAMUser", + "principalId": "EXAMPLE6E4XEGITWATV6R", + "arn": "arn:aws:iam::777788889999:user/Nikki", + "accountId": "777788889999", + "accessKeyId": "AKIAI44QH8DHBEXAMPLE", + "userName": "Nikki", + "sessionContext": { + "sessionIssuer": { }, + "webIdFederationData": { }, + "attributes": { + "creationDate": "2023-07-19T21:11:57Z", + "mfaAuthenticated": "false" + } + } + }, + "eventTime": "2023-07-19T21:14:20Z", + "eventSource": "ec2.amazonaws.com", + "eventName": "StopInstances", + "awsRegion": "us-east-1", + "sourceIPAddress": "192.0.2.0", + "userAgent": "aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/ec2.stop-instances", + "requestParameters": { + "instancesSet": { + "items": [ + { + "instanceId": "i-EXAMPLE56126103cb" + }, + { + "instanceId": "i-EXAMPLEaff4840c22" + } + ] + }, + "force": false + }, + "requestID": "c308a950-e43e-444e-afc1-EXAMPLE73e49", + "eventID": "9357a8cc-a0eb-46a1-b67e-EXAMPLE19b14", + "readOnly": false, + "eventType": "AwsApiCall", + "managementEvent": true, + "recipientAccountId": "777788889999", + "eventCategory": "Management", + "tlsDetails": { + "tlsVersion": "TLSv1.2", + "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", + "clientProvidedHostHeader": "ec2.us-east-1.amazonaws.com" + }, + "sessionCredentialFromConsole": "true" + } \ No newline at end of file diff --git a/rules/aws_cloudtrail_rules/aws_iam_user_key_created.py b/rules/aws_cloudtrail_rules/aws_iam_user_key_created.py index 93366ee2e..c91741a41 100644 --- a/rules/aws_cloudtrail_rules/aws_iam_user_key_created.py +++ b/rules/aws_cloudtrail_rules/aws_iam_user_key_created.py @@ -28,4 +28,10 @@ def dedup(event): def alert_context(event): - return aws_rule_context(event) + base = aws_rule_context(event) + base["ip_accessKeyId"] = ( + event.get("sourceIpAddress") + + ":" + + event.deep_get("responseElements", "accessKey", "accessKeyId") + ) + return base diff --git a/rules/aws_cloudtrail_rules/retrieve_sso_access_token.py b/rules/aws_cloudtrail_rules/retrieve_sso_access_token.py new file mode 100644 index 000000000..844c992fd --- /dev/null +++ b/rules/aws_cloudtrail_rules/retrieve_sso_access_token.py @@ -0,0 +1,4 @@ +def rule(event): + return ( + event.get("eventSource") == "sso.amazonaws.com" and event.get("eventName") == "CreateToken" + ) diff --git a/rules/aws_cloudtrail_rules/retrieve_sso_access_token.yml b/rules/aws_cloudtrail_rules/retrieve_sso_access_token.yml new file mode 100644 index 000000000..aad6c6caf --- /dev/null +++ b/rules/aws_cloudtrail_rules/retrieve_sso_access_token.yml @@ -0,0 +1,38 @@ +AnalysisType: rule +Filename: retrieve_sso_access_token.py +RuleID: "Retrieve.SSO.access.token" +DisplayName: "SIGNAL - Retrieve SSO access token" +Enabled: true +CreateAlert: false +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Retrieve SSO access token + ExpectedResult: true + Log: + eventName: CreateToken + eventSource: sso.amazonaws.com + eventVersion: "1.08" + recipientAccountId: + requestParameters: + clientId: '...' + clientSecret: HIDDEN_DUE_TO_SECURITY_REASONS + deviceCode: '...' + grantType: urn:ietf:params:oauth:grant-type:device_code + responseElements: + accessToken: HIDDEN_DUE_TO_SECURITY_REASONS + expiresIn: 28800 + idToken: HIDDEN_DUE_TO_SECURITY_REASONS + refreshToken: HIDDEN_DUE_TO_SECURITY_REASONS + tokenType: Bearer + sourceIPAddress: + userAgent: '' + userIdentity: + accountId: + principalId: + type: Unknown + userName: + diff --git a/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.py b/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.py new file mode 100644 index 000000000..2b5f0b2a0 --- /dev/null +++ b/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.py @@ -0,0 +1,9 @@ +def rule(event): + aws_service = event.deep_get("userIdentity", "type") == "AWSService" + return all( + [ + event.get("eventName") == "AssumeRole", + event.deep_get("requestParameters", "roleArn"), + aws_service, + ] + ) diff --git a/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.yml b/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.yml new file mode 100644 index 000000000..2b179d05c --- /dev/null +++ b/rules/aws_cloudtrail_rules/role_assumed_by_aws_service.yml @@ -0,0 +1,45 @@ +AnalysisType: rule +Filename: role_assumed_by_aws_service.py +RuleID: "Role.Assumed.by.AWS.Service" +DisplayName: "SIGNAL - Role Assumed by AWS Service" +Enabled: true +CreateAlert: false +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Role Assumed by AWS Service + ExpectedResult: true + Log: + awsRegion: us-west-2 + eventCategory: Management + eventID: 1f3d7d49-6637-3304-b959-9be15f20215d + eventName: AssumeRole + eventSource: sts.amazonaws.com + eventTime: "2024-06-02 20:27:12" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + readOnly: true + recipientAccountId: "123456789123" + requestID: a0dda101-6e27-4f88-8250-f3d475f88b56 + requestParameters: + roleArn: arn:aws:iam::123456789123:role/my_role_arn + roleSessionName: awslambda_55_20240602202712548 + resources: + - accountId: "123456789123" + arn: arn:aws:iam::123456789123:role/my_role_arn + type: AWS::IAM::Role + responseElements: + credentials: + accessKeyId: REDACTED + expiration: Jun 2, 2024, 10:37:12 PM + sessionToken: REDACTED + sharedEventID: 95e84e79-100a-40a6-985e-3c9c4b41f622 + sourceIPAddress: lambda.amazonaws.com + userAgent: lambda.amazonaws.com + userIdentity: + invokedBy: lambda.amazonaws.com + type: AWSService diff --git a/rules/aws_cloudtrail_rules/role_assumed_by_user.py b/rules/aws_cloudtrail_rules/role_assumed_by_user.py new file mode 100644 index 000000000..b512ce999 --- /dev/null +++ b/rules/aws_cloudtrail_rules/role_assumed_by_user.py @@ -0,0 +1,9 @@ +def rule(event): + aws_service = event.deep_get("userIdentity", "type") == "AWSService" + return all( + [ + event.get("eventName") == "AssumeRole", + event.deep_get("requestParameters", "roleArn"), + not aws_service, + ] + ) diff --git a/rules/aws_cloudtrail_rules/role_assumed_by_user.yml b/rules/aws_cloudtrail_rules/role_assumed_by_user.yml new file mode 100644 index 000000000..4b27a55c3 --- /dev/null +++ b/rules/aws_cloudtrail_rules/role_assumed_by_user.yml @@ -0,0 +1,45 @@ +AnalysisType: rule +Filename: role_assumed_by_user.py +RuleID: "Role.Assumed.by.User" +DisplayName: "SIGNAL - Role Assumed by User" +Enabled: true +CreateAlert: false +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Role Assumed by Service + ExpectedResult: false + Log: + awsRegion: us-west-2 + eventCategory: Management + eventID: 1f3d7d49-6637-3304-b959-9be15f20215d + eventName: AssumeRole + eventSource: sts.amazonaws.com + eventTime: "2024-06-02 20:27:12" + eventType: AwsApiCall + eventVersion: "1.08" + managementEvent: true + readOnly: true + recipientAccountId: "123456789123" + requestID: a0dda101-6e27-4f88-8250-f3d475f88b56 + requestParameters: + roleArn: arn:aws:iam::123456789123:role/my_role_arn + roleSessionName: awslambda_55_20240602202712548 + resources: + - accountId: "123456789123" + arn: arn:aws:iam::123456789123:role/my_role_arn + type: AWS::IAM::Role + responseElements: + credentials: + accessKeyId: REDACTED + expiration: Jun 2, 2024, 10:37:12 PM + sessionToken: REDACTED + sharedEventID: 95e84e79-100a-40a6-985e-3c9c4b41f622 + sourceIPAddress: lambda.amazonaws.com + userAgent: lambda.amazonaws.com + userIdentity: + invokedBy: lambda.amazonaws.com + type: AWSService diff --git a/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.py b/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.py new file mode 100644 index 000000000..5d94f8971 --- /dev/null +++ b/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.py @@ -0,0 +1,5 @@ +def rule(event): + return ( + event.get("eventSource") == "sso.amazonaws.com" + and event.get("eventName") == "ListApplications" + ) diff --git a/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.yml b/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.yml new file mode 100644 index 000000000..44c545051 --- /dev/null +++ b/rules/aws_cloudtrail_rules/signin_with_aws_cli_prompt.yml @@ -0,0 +1,26 @@ +AnalysisType: rule +Filename: signin_with_aws_cli_prompt.py +RuleID: "Sign-in.with.AWS.CLI.prompt" +DisplayName: "SIGNAL - Sign-in with AWS CLI prompt" +Enabled: true +CreateAlert: false +LogTypes: + - AWS.CloudTrail +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Test-291327 + ExpectedResult: true + Log: + eventName: ListApplications + eventSource: sso.amazonaws.com + eventTime: '...' + eventVersion: "1.08" + sourceIPAddress: + userAgent: + userIdentity: + accountId: + principalId: + type: Unknown + userName: diff --git a/rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml b/rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml index 12a5071f8..6d8090198 100644 --- a/rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml +++ b/rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml @@ -13,6 +13,7 @@ Description: > A high-severity GuardDuty finding has been identified. Runbook: > Search related logs to understand the root cause of the activity. + Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details. Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity SummaryAttributes: - severity diff --git a/rules/aws_guardduty_rules/aws_guardduty_low_sev_findings.yml b/rules/aws_guardduty_rules/aws_guardduty_low_sev_findings.yml index c3f44c245..fe676c11c 100644 --- a/rules/aws_guardduty_rules/aws_guardduty_low_sev_findings.yml +++ b/rules/aws_guardduty_rules/aws_guardduty_low_sev_findings.yml @@ -13,6 +13,7 @@ Description: > A low-severity GuardDuty finding has been identified. Runbook: > Search related logs to understand the root cause of the activity. + Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details. Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity SummaryAttributes: - severity diff --git a/rules/aws_guardduty_rules/aws_guardduty_med_sev_findings.yml b/rules/aws_guardduty_rules/aws_guardduty_med_sev_findings.yml index a0758d4e7..d051c24fe 100644 --- a/rules/aws_guardduty_rules/aws_guardduty_med_sev_findings.yml +++ b/rules/aws_guardduty_rules/aws_guardduty_med_sev_findings.yml @@ -13,6 +13,7 @@ Description: > A medium-severity GuardDuty finding has been identified. Runbook: > Search related logs to understand the root cause of the activity. + Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details. Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity SummaryAttributes: - severity diff --git a/rules/carbonblack_rules/cb_audit_flagged.py b/rules/carbonblack_rules/cb_audit_flagged.py index 0c31fa76d..9c9daf193 100644 --- a/rules/carbonblack_rules/cb_audit_flagged.py +++ b/rules/carbonblack_rules/cb_audit_flagged.py @@ -7,3 +7,9 @@ def title(event): ip_addr = event.get("clientIp", "") desc = event.get("description", "") return f"{user} [{ip_addr}] {desc}" + + +def severity(event): + if event.get("description").startswith("Requested sensor update"): + return "INFO" + return "DEFAULT" diff --git a/rules/carbonblack_rules/cb_audit_flagged.yml b/rules/carbonblack_rules/cb_audit_flagged.yml index a4db08d30..a7ab0c834 100644 --- a/rules/carbonblack_rules/cb_audit_flagged.yml +++ b/rules/carbonblack_rules/cb_audit_flagged.yml @@ -6,7 +6,7 @@ Description: "Detects when Carbon Black has flagged a log as important, such as DisplayName: "Carbon Black Log Entry Flagged" Enabled: true Filename: cb_audit_flagged.py -Severity: High +Severity: Medium Tags: - Credential Access - Brute Force @@ -44,3 +44,16 @@ Tests: "requestUrl": "/access/v2/orgs/A1234567/grants", "verbose": false, } + - Name: Sensor update requested + ExpectedResult: true + Log: + { + "description": "Requested sensor update to version: 2.16.0.2566828 for the following device: ABCDEFG012 (ID: 21360056)", + "eventId": "ac5f46923e9c11efaadd07ba65d6cd7b", + "eventTime": "2024-07-10 09:13:29.952000000", + "flagged": true, + "loginName": "", + "orgName": "acme.com", + "requestUrl": "/settings/users/pushSensorKits", + "verbose": false + } diff --git a/rules/crowdstrike_rules/crowdstrike_connection_to_embargoed_country.py b/rules/crowdstrike_rules/crowdstrike_connection_to_embargoed_country.py index ed6a24f00..61fcd29de 100644 --- a/rules/crowdstrike_rules/crowdstrike_connection_to_embargoed_country.py +++ b/rules/crowdstrike_rules/crowdstrike_connection_to_embargoed_country.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_network_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_network_detection_alert_context # U.S. Gov Sanctioned Destinations EMBARGO_COUNTRY_CODES = { @@ -10,7 +10,7 @@ def get_enrichment_obj(event): - return deep_get(event, "p_enrichment", "ipinfo_location", "p_any_ip_addresses", default=None) + return event.deep_get("p_enrichment", "ipinfo_location", "p_any_ip_addresses", default=None) def rule(event): diff --git a/rules/crowdstrike_rules/crowdstrike_credential_dumping_tool.py b/rules/crowdstrike_rules/crowdstrike_credential_dumping_tool.py index 133e95ec7..4c53f89b9 100644 --- a/rules/crowdstrike_rules/crowdstrike_credential_dumping_tool.py +++ b/rules/crowdstrike_rules/crowdstrike_credential_dumping_tool.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context CREDENTIAL_DUMPING_TOOLS = { "mimikatz.exe", @@ -33,7 +33,7 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) if process_name in CREDENTIAL_DUMPING_TOOLS: return True @@ -42,9 +42,7 @@ def rule(event): def title(event): tool = ( - deep_get(event, "event", "ImageFileName", default="") - .lower() - .split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) aid = event.get("aid", "") return f"Crowdstrike: Credential dumping tool [{tool}] detected on aid [{aid}]" diff --git a/rules/crowdstrike_rules/crowdstrike_cryptomining_tools.py b/rules/crowdstrike_rules/crowdstrike_cryptomining_tools.py index 53f484405..4cc13a606 100644 --- a/rules/crowdstrike_rules/crowdstrike_cryptomining_tools.py +++ b/rules/crowdstrike_rules/crowdstrike_cryptomining_tools.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context CRYPTOCURRENCY_MINING_TOOLS = { "xmrig.exe", @@ -31,7 +31,7 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) return process_name in CRYPTOCURRENCY_MINING_TOOLS return False @@ -39,9 +39,7 @@ def rule(event): def title(event): tool = ( - deep_get(event, "event", "ImageFileName", default="") - .lower() - .split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) aid = event.get("aid", "") return f"Crowdstrike: Cryptocurrency mining tool [{tool}] detected on aid [{aid}]" diff --git a/rules/crowdstrike_rules/crowdstrike_lolbas.py b/rules/crowdstrike_rules/crowdstrike_lolbas.py index dcaa4c48d..4e3bcfdf6 100644 --- a/rules/crowdstrike_rules/crowdstrike_lolbas.py +++ b/rules/crowdstrike_rules/crowdstrike_lolbas.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_process_alert_context, deep_get +from panther_base_helpers import crowdstrike_process_alert_context LOLBAS_EXE = { "AppInstaller.exe", @@ -111,22 +111,22 @@ def rule(event): - if deep_get(event, "event", "event_simpleName") == "ProcessRollup2": - if deep_get(event, "event", "event_platform") == "Win": + if event.deep_get("event", "event_simpleName") == "ProcessRollup2": + if event.deep_get("event", "event_platform") == "Win": exe = event.udm("process_name") return bool(exe.lower() in [x.lower() for x in LOLBAS_EXE]) return False def title(event): - exe = deep_get(event, "event", "ImageFileName").split("\\")[-1] - return f'Crowdstrike: LOLBAS execution - [{exe}] - [{deep_get(event, "event", "CommandLine")}]' + exe = event.deep_get("event", "ImageFileName").split("\\")[-1] + return f'Crowdstrike: LOLBAS execution - [{exe}] - [{event.deep_get("event", "CommandLine")}]' def dedup(event): # dedup string on "{aid}-{exe}" exe = event.udm("process_name") - return f'{deep_get(event, "event", "aid")}-{exe}' + return f'{event.deep_get("event", "aid")}-{exe}' def alert_context(event): diff --git a/rules/crowdstrike_rules/crowdstrike_macos_add_trusted_cert.py b/rules/crowdstrike_rules/crowdstrike_macos_add_trusted_cert.py index e09318ebc..a7f1559da 100644 --- a/rules/crowdstrike_rules/crowdstrike_macos_add_trusted_cert.py +++ b/rules/crowdstrike_rules/crowdstrike_macos_add_trusted_cert.py @@ -1,11 +1,11 @@ -from panther_base_helpers import crowdstrike_process_alert_context, deep_get +from panther_base_helpers import crowdstrike_process_alert_context def rule(event): - event_platform = deep_get(event, "event_platform", default="") - fdr_event_type = deep_get(event, "fdr_event_type", default="") - image_filename = deep_get(event, "event", "ImageFileName", default="") - command_line = deep_get(event, "event", "CommandLine", default="") + event_platform = event.deep_get("event_platform", default="") + fdr_event_type = event.deep_get("fdr_event_type", default="") + image_filename = event.deep_get("event", "ImageFileName", default="") + command_line = event.deep_get("event", "CommandLine", default="") return all( [ event_platform == "Mac", diff --git a/rules/crowdstrike_rules/crowdstrike_macos_osascript_administrator.py b/rules/crowdstrike_rules/crowdstrike_macos_osascript_administrator.py index 73ccb4c4f..9a33eac98 100644 --- a/rules/crowdstrike_rules/crowdstrike_macos_osascript_administrator.py +++ b/rules/crowdstrike_rules/crowdstrike_macos_osascript_administrator.py @@ -1,11 +1,11 @@ -from panther_base_helpers import crowdstrike_process_alert_context, deep_get +from panther_base_helpers import crowdstrike_process_alert_context def rule(event): - event_platform = deep_get(event, "event_platform", default="") - event_simplename = deep_get(event, "event_simplename", default="") - image_filename = deep_get(event, "event", "ImageFileName", default="") - command_line = deep_get(event, "event", "CommandLine", default="") + event_platform = event.deep_get("event_platform", default="") + event_simplename = event.deep_get("event_simplename", default="") + image_filename = event.deep_get("event", "ImageFileName", default="") + command_line = event.deep_get("event", "CommandLine", default="") return all( [ event_platform == "Mac", diff --git a/rules/crowdstrike_rules/crowdstrike_macos_plutil_usage.py b/rules/crowdstrike_rules/crowdstrike_macos_plutil_usage.py index 5ef380f8c..ba272f888 100644 --- a/rules/crowdstrike_rules/crowdstrike_macos_plutil_usage.py +++ b/rules/crowdstrike_rules/crowdstrike_macos_plutil_usage.py @@ -1,17 +1,17 @@ -from panther_base_helpers import crowdstrike_process_alert_context, deep_get +from panther_base_helpers import crowdstrike_process_alert_context def rule(event): - command_line = deep_get(event, "event", "CommandLine", default="") + command_line = event.deep_get("event", "CommandLine", default="") if ( command_line == "plutil -convert binary1 /Library/Preferences/com.tinyspeck.slackmacgap.plist" ): return False - event_platform = deep_get(event, "event_platform", default="") - fdr_event_type = deep_get(event, "fdr_event_type", default="") - image_filename = deep_get(event, "event", "ImageFileName", default="") + event_platform = event.deep_get("event_platform", default="") + fdr_event_type = event.deep_get("fdr_event_type", default="") + image_filename = event.deep_get("event", "ImageFileName", default="") return all( [ diff --git a/rules/crowdstrike_rules/crowdstrike_remote_access_tool_execution.py b/rules/crowdstrike_rules/crowdstrike_remote_access_tool_execution.py index 7922fda06..867c00315 100644 --- a/rules/crowdstrike_rules/crowdstrike_remote_access_tool_execution.py +++ b/rules/crowdstrike_rules/crowdstrike_remote_access_tool_execution.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context REMOTE_ACCESS_EXECUTABLES = { "teamviewer_service.exe", @@ -25,7 +25,7 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) return process_name in REMOTE_ACCESS_EXECUTABLES return False @@ -33,9 +33,7 @@ def rule(event): def title(event): tool = ( - deep_get(event, "event", "ImageFileName", default="") - .lower() - .split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) aid = event.get("aid", "") return f"Crowdstrike: Remote access tool [{tool}] detected on aid [{aid}]" diff --git a/rules/crowdstrike_rules/crowdstrike_reverse_shell_tool_executed.py b/rules/crowdstrike_rules/crowdstrike_reverse_shell_tool_executed.py index 82bc74b0c..98b06b1fb 100644 --- a/rules/crowdstrike_rules/crowdstrike_reverse_shell_tool_executed.py +++ b/rules/crowdstrike_rules/crowdstrike_reverse_shell_tool_executed.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context REMOTE_SHELL_TOOLS = { # process name: reverse shell signature @@ -17,9 +17,9 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) - command_line = deep_get(event, "event", "CommandLine", default="") + command_line = event.deep_get("event", "CommandLine", default="") signatures = REMOTE_SHELL_TOOLS.get(process_name, []) for signature in signatures: if signature in command_line: @@ -29,9 +29,7 @@ def rule(event): def title(event): tool = ( - deep_get(event, "event", "ImageFileName", default="") - .lower() - .split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) aid = event.get("aid", "") return f"Crowdstrike: Reverse shell tool [{tool}] detected on aid [{aid}]" diff --git a/rules/crowdstrike_rules/crowdstrike_systemlog_tampering.py b/rules/crowdstrike_rules/crowdstrike_systemlog_tampering.py index c4575d947..9cf355e1e 100644 --- a/rules/crowdstrike_rules/crowdstrike_systemlog_tampering.py +++ b/rules/crowdstrike_rules/crowdstrike_systemlog_tampering.py @@ -1,4 +1,4 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context CLEARING_SYSTEM_LOG_TOOLS = { "wevtutil.exe": ["cl", "clear-log"], @@ -10,12 +10,10 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) if process_name in CLEARING_SYSTEM_LOG_TOOLS: - process_command_line = deep_get(event, "event", "CommandLine", default="").split( - " " - ) + process_command_line = event.deep_get("event", "CommandLine", default="").split(" ") suspicious_command_lines = CLEARING_SYSTEM_LOG_TOOLS.get(process_name) for suspicious_command_line in suspicious_command_lines: if suspicious_command_line in process_command_line: @@ -25,7 +23,7 @@ def rule(event): def title(event): aid = event.get("aid", "") - command = deep_get(event, "event", "CommandLine", default="") + command = event.deep_get("event", "CommandLine", default="") return ( "Crowdstrike: System log tampering attempt detected on " f"aid [{aid}] with command [{command}]" diff --git a/rules/crowdstrike_rules/crowdstrike_unusual_parent_child_processes.py b/rules/crowdstrike_rules/crowdstrike_unusual_parent_child_processes.py index eac17c878..f81291349 100644 --- a/rules/crowdstrike_rules/crowdstrike_unusual_parent_child_processes.py +++ b/rules/crowdstrike_rules/crowdstrike_unusual_parent_child_processes.py @@ -1,8 +1,7 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context SUSPICIOUS_PARENT_CHILD_COMBINATIONS_WINDOWS = { ("svchost.exe", "cmd.exe"), - ("explorer.exe", "powershell.exe"), ("winword.exe", "cmd.exe"), ("winword.exe", "powershell.exe"), ("excel.exe", "cmd.exe"), @@ -15,9 +14,9 @@ def rule(event): if event.get("fdr_event_type", "") == "ProcessRollup2": if event.get("event_platform", "") == "Win": - parent_process_name = deep_get(event, "event", "ParentBaseFileName", default="").lower() + parent_process_name = event.deep_get("event", "ParentBaseFileName", default="").lower() child_process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) return ( parent_process_name, @@ -27,9 +26,9 @@ def rule(event): def title(event): - parent_process_name = deep_get(event, "event", "ParentBaseFileName", default="").lower() + parent_process_name = event.deep_get("event", "ParentBaseFileName", default="").lower() child_process_name = ( - deep_get(event, "event", "ImageFileName", default="").lower().split("\\")[-1] + event.deep_get("event", "ImageFileName", default="").lower().split("\\")[-1] ) procs = (parent_process_name, child_process_name) aid = event.get("aid", "") diff --git a/rules/crowdstrike_rules/crowdstrike_wmi_query_detection.py b/rules/crowdstrike_rules/crowdstrike_wmi_query_detection.py index adf960b8b..7ceae7370 100644 --- a/rules/crowdstrike_rules/crowdstrike_wmi_query_detection.py +++ b/rules/crowdstrike_rules/crowdstrike_wmi_query_detection.py @@ -1,13 +1,13 @@ -from panther_base_helpers import crowdstrike_detection_alert_context, deep_get +from panther_base_helpers import crowdstrike_detection_alert_context WMIC_SIGNATURES = ["get", "list", "process call create", "cmd.exe", "powershell.exe", "command.exe"] def rule(event): - if deep_get(event, "event", "event_simpleName") == "ProcessRollup2": - if deep_get(event, "event", "event_platform") == "Win": - if deep_get(event, "event", "ImageFileName", default="").split("\\")[-1] == "wmic.exe": - command_line = deep_get(event, "event", "CommandLine", default="") + if event.deep_get("event", "event_simpleName") == "ProcessRollup2": + if event.deep_get("event", "event_platform") == "Win": + if event.deep_get("event", "ImageFileName", default="").split("\\")[-1] == "wmic.exe": + command_line = event.deep_get("event", "CommandLine", default="") for signature in WMIC_SIGNATURES: if signature in command_line: return True @@ -15,7 +15,7 @@ def rule(event): def title(event): - cmd = deep_get(event, "event", "CommandLine", default="") + cmd = event.deep_get("event", "CommandLine", default="") aid = event.get("aid", "") return f"Crowdstrike: WMIC Query [{cmd}] performed on aid [{aid}]" diff --git a/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.py b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.py new file mode 100644 index 000000000..63b41dc51 --- /dev/null +++ b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.py @@ -0,0 +1,20 @@ +from crowdstrike_event_streams_helpers import cs_alert_context + + +def rule(event): + return all( + [ + event.deep_get("event", "OperationName") == "CreateAPIClient", + event.deep_get("event", "Success"), + ] + ) + + +def title(event): + user = event.deep_get("event", "UserId") + service = event.deep_get("event", "ServiceName") + return f"{user} created a new API key in {service}" + + +def alert_context(event): + return cs_alert_context(event) diff --git a/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.yml b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.yml new file mode 100644 index 000000000..52bc17e54 --- /dev/null +++ b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_created.yml @@ -0,0 +1,49 @@ +AnalysisType: rule +Filename: crowdstrike_api_key_created.py +RuleID: "Crowdstrike.API.Key.Created" +DisplayName: "Crowdstrike API Key Created" +Reports: + MITRE ATT&CK: + - T1098.001 # Additional Cloud Credentials +Enabled: true +LogTypes: + - Crowdstrike.EventStreams +Severity: Medium +Description: A user created an API Key in CrowdStrike +DedupPeriodMinutes: 60 +Threshold: 1 +Runbook: Reach out to the user if needed to validate the activity. +Tests: + - Name: API Key Created + ExpectedResult: true + Log: + event: + AuditKeyValues: + - Key: scope(s) + ValueString: alerts:read,api-integrations:read + - Key: actor_user + ValueString: tester@panther.com + - Key: actor_user_uuid + ValueString: a11a1111-1a11-1a1a-1a11-a11a111a111a + - Key: actor_cid + ValueString: aaa111111111111111aaaaaa11a11a11 + - Key: trace_id + ValueString: 1a111111-a1a1-111a-11aa-a111111a1a1a + - Key: APIClientID + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: id + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: name + ValueString: key name + OperationName: CreateAPIClient + ServiceName: Crowdstrike API Client + Success: true + UTCTimestamp: "2024-07-08 14:01:54.000000000" + UserId: tester@panther.com + UserIp: 11.1.111.11 + metadata: + customerIDString: aaa111111111111111aaaaaa11a11a11 + eventCreationTime: "2024-07-08 14:01:54.451000000" + eventType: AuthActivityAuditEvent + offset: 111111 + version: "1.0" diff --git a/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.py b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.py new file mode 100644 index 000000000..f508e0b1a --- /dev/null +++ b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.py @@ -0,0 +1,20 @@ +from crowdstrike_event_streams_helpers import cs_alert_context + + +def rule(event): + return all( + [ + event.deep_get("event", "OperationName") == "DeleteAPIClients", + event.deep_get("event", "Success"), + ] + ) + + +def title(event): + user = event.deep_get("event", "UserId") + service = event.deep_get("event", "ServiceName") + return f"{user} deleted an API key in {service}" + + +def alert_context(event): + return cs_alert_context(event) diff --git a/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.yml b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.yml new file mode 100644 index 000000000..d66afc23e --- /dev/null +++ b/rules/crowdstrike_rules/event_stream_rules/crowdstrike_api_key_deleted.yml @@ -0,0 +1,83 @@ +AnalysisType: rule +Filename: crowdstrike_api_key_deleted.py +RuleID: "Crowdstrike.API.Key.Deleted" +DisplayName: "Crowdstrike API Key Deleted" +Reports: + MITRE ATT&CK: + - T1531 # Account Access Removal + - T1070 # Indicator Removal +Enabled: true +LogTypes: + - Crowdstrike.EventStreams +Severity: Medium +Description: A user deleted an API Key in CrowdStrike +DedupPeriodMinutes: 60 +Threshold: 1 +Runbook: Validate this action was authorized. +Tests: + - Name: API Key Deleted + ExpectedResult: true + Log: + event: + AuditKeyValues: + - Key: scope(s) + ValueString: alerts:read,api-integrations:read + - Key: actor_user + ValueString: tester@panther.com + - Key: actor_user_uuid + ValueString: a11a1111-1a11-1a1a-1a11-a11a111a111a + - Key: actor_cid + ValueString: aaa111111111111111aaaaaa11a11a11 + - Key: trace_id + ValueString: 1a111111-a1a1-111a-11aa-a111111a1a1a + - Key: APIClientID + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: id + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: name + ValueString: key name + OperationName: DeleteAPIClients + ServiceName: Crowdstrike API Client + Success: true + UTCTimestamp: "2024-07-08 14:01:54.000000000" + UserId: tester@panther.com + UserIp: 11.1.111.11 + metadata: + customerIDString: aaa111111111111111aaaaaa11a11a11 + eventCreationTime: "2024-07-08 14:01:54.451000000" + eventType: AuthActivityAuditEvent + offset: 111111 + version: "1.0" + - Name: API Key Delete failed + ExpectedResult: false + Log: + event: + AuditKeyValues: + - Key: scope(s) + ValueString: alerts:read,api-integrations:read + - Key: actor_user + ValueString: tester@panther.com + - Key: actor_user_uuid + ValueString: a11a1111-1a11-1a1a-1a11-a11a111a111a + - Key: actor_cid + ValueString: aaa111111111111111aaaaaa11a11a11 + - Key: trace_id + ValueString: 1a111111-a1a1-111a-11aa-a111111a1a1a + - Key: APIClientID + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: id + ValueString: aaa1a11aaa111a1a11a11aaaa1aa1a11 + - Key: name + ValueString: key name + OperationName: DeleteAPIClients + ServiceName: Crowdstrike API Client + Success: false + UTCTimestamp: "2024-07-08 14:01:54.000000000" + UserId: tester@panther.com + UserIp: 11.1.111.11 + metadata: + customerIDString: aaa111111111111111aaaaaa11a11a11 + eventCreationTime: "2024-07-08 14:01:54.451000000" + eventType: AuthActivityAuditEvent + offset: 111111 + version: "1.0" diff --git a/rules/gcp_audit_rules/gcp_cloud_run_service_created.py b/rules/gcp_audit_rules/gcp_cloud_run_service_created.py new file mode 100644 index 000000000..6522b0fdd --- /dev/null +++ b/rules/gcp_audit_rules/gcp_cloud_run_service_created.py @@ -0,0 +1,43 @@ +from gcp_base_helpers import gcp_alert_context +from panther_base_helpers import deep_get, deep_walk + + +def rule(event): + if deep_get(event, "severity") == "ERROR": + return False + + if not deep_get(event, "protoPayload", "methodName").endswith("Services.CreateService"): + return False + + authorization_info = deep_walk(event, "protoPayload", "authorizationInfo") + if not authorization_info: + return False + + for auth in authorization_info: + if auth.get("permission") == "run.services.create" and auth.get("granted") is True: + return True + return False + + +def title(event): + actor = deep_get( + event, "protoPayload", "authenticationInfo", "principalEmail", default="" + ) + project_id = deep_get(event, "resource", "labels", "project_id", default="") + + return f"[GCP]: [{actor}] created new Run Service in project [{project_id}]" + + +def alert_context(event): + context = gcp_alert_context(event) + context["service_account"] = deep_get( + event, + "protoPayload", + "request", + "service", + "spec", + "template", + "spec", + default="", + ) + return context diff --git a/rules/gcp_audit_rules/gcp_cloud_run_service_created.yml b/rules/gcp_audit_rules/gcp_cloud_run_service_created.yml new file mode 100644 index 000000000..154fa3179 --- /dev/null +++ b/rules/gcp_audit_rules/gcp_cloud_run_service_created.yml @@ -0,0 +1,202 @@ +AnalysisType: rule +LogTypes: + - GCP.AuditLog +Description: + Detects creation of new Cloud Run Service, which, if configured maliciously, may be part of the attack + aimed to invoke the service and retrieve the access token. +DisplayName: "GCP Cloud Run Service Created" +RuleID: "GCP.Cloud.Run.Service.Created" +Filename: gcp_cloud_run_service_created.py +Enabled: true +CreateAlert: false +Reference: https://cloud.google.com/run/docs/quickstarts/deploy-container +Runbook: Confirm this was authorized and necessary behavior +Severity: Low +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: GCP Run Service Created + ExpectedResult: true + Log: + { + "insertId": "jzm5rucrn2", + "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity", + "protoPayload": + { + "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": + { + "principalEmail": "some.user@company.com", + "principalSubject": "user:some.user@company.com", + }, + "authorizationInfo": + [ + { + "granted": true, + "permission": "run.services.create", + "resource": "namespaces/some-project/services/cloudrun-exfil", + "resourceAttributes": {}, + }, + ], + "methodName": "google.cloud.run.v1.Services.CreateService", + "request": + { + "@type": "type.googleapis.com/google.cloud.run.v1.CreateServiceRequest", + "parent": "namespaces/some-project", + "service": + { + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "metadata": + { + "annotations": + { + "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test", + }, + "name": "cloudrun-exfil", + "namespace": "some-project", + }, + "spec": + { + "template": + { + "metadata": + { + "annotations": + { + "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test", + }, + "labels": + { + "cloud.googleapis.com/location": "us-west1", + }, + "name": "cloudrun-exfil-00001-zif", + }, + "spec": + { + "serviceAccountName": "abc-test@some-project.iam.gserviceaccount.com", + }, + }, + }, + }, + }, + "requestMetadata": + { + "callerIP": "1.2.3.4", + "callerSuppliedUserAgent": "(gzip),gzip(gfe)", + "destinationAttributes": {}, + "requestAttributes": + { "auth": {}, "time": "2024-02-02T09:43:54.690161Z" }, + }, + "resourceLocation": { "currentLocations": ["us-west1"] }, + "resourceName": "namespaces/some-project/services/cloudrun-exfil", + "response": + { + "@type": "type.googleapis.com/google.cloud.run.v1.Service", + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "metadata": + { + "annotations": + { + "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test", + "run.googleapis.com/ingress": "all", + "run.googleapis.com/operation-id": "6fdf115a-1bdd-4836-b0ca-ae71f8ba6718", + "serving.knative.dev/creator": "some.user@company.com", + "serving.knative.dev/lastModifier": "some.user@company.com", + }, + "creationTimestamp": "2024-02-02T09:43:54.640837Z", + "generation": 1, + "labels": { "cloud.googleapis.com/location": "us-west1" }, + "name": "cloudrun-exfil", + "namespace": "1028347275902", + "resourceVersion": "AAYQYvNHUcU", + "selfLink": "/apis/serving.knative.dev/v1/namespaces/1028347275902/services/cloudrun-exfil", + "uid": "45101e8e-7b91-4c41-81a1-969e876923f4", + }, + "spec": + { + "template": + { + "metadata": + { + "annotations": + { + "autoscaling.knative.dev/maxScale": "100", + "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test", + }, + "labels": + { + "run.googleapis.com/startupProbeType": "Default", + }, + "name": "cloudrun-exfil-00001-zif", + }, + "spec": + { + "containerConcurrency": 80, + "serviceAccountName": "abc-test@some-project.iam.gserviceaccount.com", + "timeoutSeconds": 300, + }, + }, + "traffic": [{ "latestRevision": true, "percent": 100 }], + }, + "status": {}, + }, + "serviceName": "run.googleapis.com", + }, + "receiveTimestamp": "2024-02-02 09:43:54.723840817", + "resource": + { + "labels": + { + "configuration_name": "", + "location": "us-west1", + "project_id": "some-project", + "revision_name": "", + "service_name": "cloudrun-exfil", + }, + "type": "cloud_run_revision", + }, + "severity": "NOTICE", + "timestamp": "2024-02-02 09:43:54.497796000", + } + - Name: GCP Run Service Not Created + ExpectedResult: false + Log: + { + "insertId": "jfca5rd3wqn", + "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity", + "protoPayload": + { + "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": + { + "principalEmail": "some.user@company.com", + "principalSubject": "user:some.user@company.com", + }, + "authorizationInfo": + [ + { + "granted": true, + "permission": "run.services.create", + "resource": "namespaces/some-project/services/cloudrun-exfil", + "resourceAttributes": {}, + }, + ], + "methodName": "google.cloud.run.v1.Services.CreateService", + "request": ..., + "requestMetadata": ..., + "resourceLocation": ..., + "resourceName": ..., + "serviceName": "run.googleapis.com", + "status": + { + "code": 6, + "message": "Resource 'cloudrun-exfil' already exists.", + }, + }, + "receiveTimestamp": "2024-02-02 09:36:22.250430490", + "resource": ..., + "severity": "ERROR", + "timestamp": "2024-02-02 09:36:21.212182000", + } diff --git a/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.py b/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.py new file mode 100644 index 000000000..6acdc64f5 --- /dev/null +++ b/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.py @@ -0,0 +1,45 @@ +from gcp_base_helpers import gcp_alert_context +from panther_base_helpers import deep_get, deep_walk + + +def rule(event): + if deep_get(event, "severity") == "ERROR": + return False + + if not deep_get(event, "protoPayload", "methodName").endswith("Services.SetIamPolicy"): + return False + + authorization_info = deep_walk(event, "protoPayload", "authorizationInfo") + if not authorization_info: + return False + + for auth in authorization_info: + if auth.get("permission") == "run.services.setIamPolicy" and auth.get("granted") is True: + return True + return False + + +def title(event): + actor = deep_get( + event, "protoPayload", "authenticationInfo", "principalEmail", default="" + ) + resource = deep_get(event, "resource", "resourceName", default="") + assigned_role = deep_walk(event, "protoPayload", "response", "bindings", "role") + project_id = deep_get(event, "resource", "labels", "project_id", default="") + + return ( + f"[GCP]: [{actor}] was granted access to [{resource}] service with " + f"the [{assigned_role}] role in project [{project_id}]" + ) + + +def alert_context(event): + context = gcp_alert_context(event) + context["assigned_role"] = deep_walk( + event, + "protoPayload", + "response", + "bindings", + "role", + ) + return context diff --git a/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.yml b/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.yml new file mode 100644 index 000000000..f8d273e1f --- /dev/null +++ b/rules/gcp_audit_rules/gcp_cloud_run_set_iam_policy.yml @@ -0,0 +1,154 @@ +AnalysisType: rule +LogTypes: + - GCP.AuditLog +Description: + Detects new roles granted to users to Cloud Run Services. This could potentially allow the user to perform + actions within the project and its resources, which could pose a security risk. +DisplayName: "GCP Cloud Run Set IAM Policy" +RuleID: "GCP.Cloud.Run.Set.IAM.Policy" +Enabled: true +Filename: gcp_cloud_run_set_iam_policy.py +Reference: https://cloud.google.com/run/docs/securing/managing-access +Runbook: Confirm this was authorized and necessary behavior +Severity: High +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: GCP Run IAM Policy Set + ExpectedResult: true + Log: + { + "insertId": "l3jvzyd2s2s", + "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity", + "protoPayload": + { + "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": + { + "principalEmail": "some.user@company.com", + "principalSubject": "user:some.user@company.com", + }, + "authorizationInfo": + [ + { + "granted": true, + "permission": "run.services.setIamPolicy", + "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + "resourceAttributes": {}, + }, + { + "granted": true, + "permission": "run.services.setIamPolicy", + "resourceAttributes": {}, + }, + ], + "methodName": "google.cloud.run.v1.Services.SetIamPolicy", + "request": + { + "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest", + "policy": + { + "bindings": + [ + { + "members": ["user:some.user@company.com"], + "role": "roles/run.invoker", + }, + ], + }, + "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + }, + "requestMetadata": + { + "callerIP": "1.2.3.4", + "callerSuppliedUserAgent": "(gzip),gzip(gfe)", + "destinationAttributes": {}, + "requestAttributes": + { "auth": {}, "time": "2024-02-02T09:44:26.173186Z" }, + }, + "resourceLocation": { "currentLocations": ["us-west1"] }, + "resourceName": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + "response": + { + "@type": "type.googleapis.com/google.iam.v1.Policy", + "bindings": + [ + { + "members": ["user:some.user@company.com"], + "role": "roles/run.invoker", + }, + ], + "etag": "BwYQYvUoBxs=", + }, + "serviceName": "run.googleapis.com", + }, + "receiveTimestamp": "2024-02-02 09:44:26.653891982", + "resource": + { + "labels": + { + "configuration_name": "", + "location": "us-west1", + "project_id": "some-project", + "revision_name": "", + "service_name": "", + }, + "type": "cloud_run_revision", + }, + "severity": "NOTICE", + "timestamp": "2024-02-02 09:44:26.029835000", + } + - Name: GCP Run IAM Policy Not Set + ExpectedResult: false + Log: + { + "insertId": "l3jvzyd2s2s", + "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity", + "protoPayload": + { + "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": + { + "principalEmail": "some.user@company.com", + "principalSubject": "user:some.user@company.com", + }, + "authorizationInfo": + [ + { + "granted": false, + "permission": "run.services.setIamPolicy", + "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + "resourceAttributes": {}, + }, + { + "granted": false, + "permission": "run.services.setIamPolicy", + "resourceAttributes": {}, + }, + ], + "methodName": "google.cloud.run.v1.Services.SetIamPolicy", + "request": + { + "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest", + "policy": + { + "bindings": + [ + { + "members": ["user:some.user@company.com"], + "role": "roles/run.invoker", + }, + ], + }, + "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + }, + "requestMetadata": ..., + "resourceLocation": ..., + "resourceName": "projects/some-project/locations/us-west1/services/cloudrun-exfil", + "serviceName": "run.googleapis.com", + }, + "receiveTimestamp": "2024-02-02 09:44:26.653891982", + "resource": ..., + "severity": "NOTICE", + "timestamp": "2024-02-02 09:44:26.029835000", + } diff --git a/rules/github_rules/github_advanced_security_change.yml b/rules/github_rules/github_advanced_security_change.yml index 152870f09..fce5ad846 100644 --- a/rules/github_rules/github_advanced_security_change.yml +++ b/rules/github_rules/github_advanced_security_change.yml @@ -3,6 +3,7 @@ Filename: github_advanced_security_change.py RuleID: "GitHub.Advanced.Security.Change" DisplayName: "GitHub Security Change, includes GitHub Advanced Security" Enabled: true +CreateAlert: false LogTypes: - GitHub.Audit Tags: diff --git a/rules/github_rules/github_repo_archived.yml b/rules/github_rules/github_repo_archived.yml new file mode 100644 index 000000000..7348a4a87 --- /dev/null +++ b/rules/github_rules/github_repo_archived.yml @@ -0,0 +1,62 @@ +AnalysisType: rule +RuleID: "Github.Repo.Archived" +DisplayName: "GitHub Repository Archived" +Enabled: true +CreateAlert: false +LogTypes: + - GitHub.Audit +Tags: + - GitHub + - panther-signal +Reference: https://docs.github.com/en/repositories/archiving-a-github-repository/about-archiving-content-and-data-on-github +Severity: Info +Description: Detects when a repository is archived. +Detection: + - Key: action + Condition: Equals + Value: repo.archived +AlertTitle: "Repository [{repo}] archived." +AlertContext: + - KeyName: action + KeyValue: + Key: action + - KeyName: actor + KeyValue: + Key: actor + - KeyName: org + KeyValue: + Key: org + - KeyName: repo + KeyValue: + Key: repo + - KeyName: user + KeyValue: + Key: user + - KeyName: actor_location + KeyValue: + KeyPath: actor_location.country_code +Tests: + - + Name: GitHub - Repo Created + ExpectedResult: false + Log: + { + "actor": "cat", + "action": "repo.create", + "created_at": 1621305118553, + "org": "my-org", + "p_log_type": "GitHub.Audit", + "repo": "my-org/my-repo" + } + - + Name: GitHub - Repo Archived + ExpectedResult: true + Log: + { + "actor": "cat", + "action": "repo.archived", + "created_at": 1621305118553, + "org": "my-org", + "p_log_type": "GitHub.Audit", + "repo": "my-org/my-repo" + } \ No newline at end of file diff --git a/rules/okta_rules/okta_login_signal.py b/rules/okta_rules/okta_login_signal.py new file mode 100644 index 000000000..e7e9ab343 --- /dev/null +++ b/rules/okta_rules/okta_login_signal.py @@ -0,0 +1,9 @@ +def rule(event): + return ( + event.deep_get("eventType") == "user.session.start" + and event.deep_get("outcome", "result") == "SUCCESS" + ) + + +def title(event): + return f'{event.deep_get("actor", "displayName")} logged in to Okta' diff --git a/rules/okta_rules/okta_login_signal.yml b/rules/okta_rules/okta_login_signal.yml new file mode 100644 index 000000000..20ce68849 --- /dev/null +++ b/rules/okta_rules/okta_login_signal.yml @@ -0,0 +1,223 @@ +AnalysisType: rule +Filename: okta_login_signal.py +RuleID: "Okta.Login.Success" +DisplayName: "Okta Login Signal" +Enabled: false +CreateAlert: false +LogTypes: + - Okta.SystemLog +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Non-Login Event + ExpectedResult: false + Log: + actor: + alternateId: jim.kalafut@panther.com + displayName: Jim Kalafut + id: 00u99ped55av2JpGs5d7 + type: User + authenticationContext: + authenticationStep: 0 + externalSessionId: trsxcsf59kYRG-GwAbWjw-PZA + client: + device: Unknown + ipAddress: 11.22.33.44 + userAgent: + browser: UNKNOWN + os: Unknown + rawUserAgent: Go-http-client/2.0 + zone: "null" + debugContext: + debugData: + dtHash: 53dd1a7513e0256eb13b9a47bb07ed61e8ca3d35fbdc36c909567a21a65a2b19 + rateLimitBucketUuid: b192d91c-b242-36da-9332-d97a5579f865 + rateLimitScopeType: ORG + rateLimitSecondsToReset: "6" + requestId: 234cf34e0081e025e1fe14224464bbd6 + requestUri: /api/v1/logs + threshold: "20" + timeSpan: "1" + timeUnit: MINUTES + url: /api/v1/logs?since=2023-09-21T17%3A04%3A22Z&limit=1000&after=1714675441520_1 + userId: 00u99ped55av2JpGs5d7 + warningPercent: "60" + displayMessage: Rate limit warning + eventType: system.org.rate_limit.warning + legacyEventType: core.framework.ratelimit.warning + outcome: + result: SUCCESS + published: "2024-05-02 18:46:21.121000000" + request: + ipChain: + - ip: 11.22.33.44 + version: V4 + securityContext: {} + severity: WARN + target: + - id: /api/v1/logs + type: URL Pattern + - id: b192d91c-b242-36da-9332-d97a5579f865 + type: Bucket Uuid + transaction: + detail: + requestApiTokenId: 00T1bjatrp6Nl1dOc5d7 + id: 234cf34e0081e025e1fe14224464bbd6 + type: WEB + uuid: 44aeb388-08b4-11ef-9cec-73ffcb6f9fdd + version: "0" + - Name: Successful Login + ExpectedResult: true + Log: + actor: + alternateId: casey.hill@hey.com + displayName: Casey Hill + id: 00ubewfku1EX0WCFk697 + type: User + authenticationContext: + authenticationStep: 0 + externalSessionId: idxvF50v_5sT2-GOA7_K0Amyw + client: + device: Computer + geographicalContext: + city: Atlanta + country: United States + geolocation: + lat: 33.9794 + lon: -84.3459 + postalCode: "30350" + state: Georgia + ipAddress: 99.108.5.25 + userAgent: + browser: CHROME + os: Mac OS 14.4.1 (Sonoma) + rawUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 + zone: "null" + debugContext: + debugData: + authnRequestId: 5167029d2c8308348d651c0be650230f + dtHash: f23be3b6d8bfd69c14e0d1b33e790b84fa5358eab0a09a1058816ad65d633da4 + oktaUserAgentExtended: okta-auth-js/7.0.1 okta-signin-widget-7.16.1 + origin: https://trial-2340039.okta.com + requestId: 601b158a3b3e23be5bbf74d0fe63cd78 + requestUri: /idp/idx/challenge/answer + threatSuspected: "false" + url: /idp/idx/challenge/answer? + displayMessage: User login to Okta + eventType: user.session.start + legacyEventType: core.user_auth.login_success + outcome: + result: SUCCESS + published: "2024-04-02 19:17:37.621000000" + request: + ipChain: + - geographicalContext: + city: Atlanta + country: United States + geolocation: + lat: 33.9794 + lon: -84.3459 + postalCode: "30350" + state: Georgia + ip: 99.108.5.25 + version: V4 + securityContext: + asNumber: 7018 + asOrg: at&t corp. + domain: sbcglobal.net + isProxy: false + isp: att services inc + severity: INFO + target: + - alternateId: unknown + displayName: Password + id: lae1at5k3ir9bV1gr697 + type: AuthenticatorEnrollment + - alternateId: Okta Dashboard + displayName: Okta Dashboard + id: 0oabewfkt83T8ve1o697 + type: AppInstance + transaction: + detail: {} + id: 601b158a3b3e23be5bbf74d0fe63cd78 + type: WEB + uuid: aac560bd-f125-11ee-9caa-cd5d09945def + version: "0" + - Name: Failed Login + ExpectedResult: false + Log: + actor: + alternateId: casey.hill@hey.com + displayName: Casey Hill + id: 00ubewfku1EX0WCFk697 + type: User + authenticationContext: + authenticationStep: 0 + externalSessionId: idxvF50v_5sT2-GOA7_K0Amyw + client: + device: Computer + geographicalContext: + city: Atlanta + country: United States + geolocation: + lat: 33.9794 + lon: -84.3459 + postalCode: "30350" + state: Georgia + ipAddress: 99.108.5.25 + userAgent: + browser: CHROME + os: Mac OS 14.4.1 (Sonoma) + rawUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 + zone: "null" + debugContext: + debugData: + authnRequestId: 5167029d2c8308348d651c0be650230f + dtHash: f23be3b6d8bfd69c14e0d1b33e790b84fa5358eab0a09a1058816ad65d633da4 + oktaUserAgentExtended: okta-auth-js/7.0.1 okta-signin-widget-7.16.1 + origin: https://trial-2340039.okta.com + requestId: 601b158a3b3e23be5bbf74d0fe63cd78 + requestUri: /idp/idx/challenge/answer + threatSuspected: "false" + url: /idp/idx/challenge/answer? + displayMessage: User login to Okta + eventType: user.session.start + legacyEventType: core.user_auth.login_success + outcome: + result: FAILURE + published: "2024-04-02 19:17:37.621000000" + request: + ipChain: + - geographicalContext: + city: Atlanta + country: United States + geolocation: + lat: 33.9794 + lon: -84.3459 + postalCode: "30350" + state: Georgia + ip: 99.108.5.25 + version: V4 + securityContext: + asNumber: 7018 + asOrg: at&t corp. + domain: sbcglobal.net + isProxy: false + isp: att services inc + severity: INFO + target: + - alternateId: unknown + displayName: Password + id: lae1at5k3ir9bV1gr697 + type: AuthenticatorEnrollment + - alternateId: Okta Dashboard + displayName: Okta Dashboard + id: 0oabewfkt83T8ve1o697 + type: AppInstance + transaction: + detail: {} + id: 601b158a3b3e23be5bbf74d0fe63cd78 + type: WEB + uuid: aac560bd-f125-11ee-9caa-cd5d09945def + version: "0" \ No newline at end of file diff --git a/rules/okta_rules/okta_login_without_push_marker.py b/rules/okta_rules/okta_login_without_push_marker.py new file mode 100644 index 000000000..042a80cf6 --- /dev/null +++ b/rules/okta_rules/okta_login_without_push_marker.py @@ -0,0 +1,13 @@ +# configure this Push marker based on your environment +PUSH_MARKER = "PS_mxzqarw" + + +def rule(event): + return not event.deep_get("client", "userAgent", "rawUserAgent", default="").endswith( + PUSH_MARKER + ) + + +def title(event): + actor = event.deep_get("actor", "displayName") + return f"{actor} logged in from device without expected Push marker" diff --git a/rules/okta_rules/okta_login_without_push_marker.yml b/rules/okta_rules/okta_login_without_push_marker.yml new file mode 100644 index 000000000..f8c427138 --- /dev/null +++ b/rules/okta_rules/okta_login_without_push_marker.yml @@ -0,0 +1,128 @@ +AnalysisType: rule +Filename: okta_login_without_push_marker.py +RuleID: "Okta.Login.Without.Push.Marker" +DisplayName: "Okta Login Without Push Marker" +Enabled: false +Tags: + - Push Security + - Configuration Required +LogTypes: + - Okta.SystemLog +Severity: Medium +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: Login with marker + ExpectedResult: false + Log: + actor: + alternateId: alice.beaver@company.com + displayName: Alice Beaver + id: 00u99ped55av2JpGs5d7 + type: User + authenticationContext: + authenticationStep: 0 + externalSessionId: trsxcsf59kYRG-GwAbWjw-PZA + client: + device: Unknown + ipAddress: 11.22.33.44 + userAgent: + browser: UNKNOWN + os: Unknown + rawUserAgent: Go-http-client/2.0 PS_mxzqarw + zone: "null" + debugContext: + debugData: + dtHash: 53dd1a7513e0256eb13b9a47bb07ed61e8ca3d35fbdc36c909567a21a65a2b19 + rateLimitBucketUuid: b192d91c-b242-36da-9332-d97a5579f865 + rateLimitScopeType: ORG + rateLimitSecondsToReset: "6" + requestId: 234cf34e0081e025e1fe14224464bbd6 + requestUri: /api/v1/logs + threshold: "20" + timeSpan: "1" + timeUnit: MINUTES + url: /api/v1/logs?since=2023-09-21T17%3A04%3A22Z&limit=1000&after=1714675441520_1 + userId: 00u99ped55av2JpGs5d7 + warningPercent: "60" + displayMessage: Rate limit warning + eventType: system.org.rate_limit.warning + legacyEventType: core.framework.ratelimit.warning + outcome: + result: SUCCESS + published: "2024-05-02 18:46:21.121000000" + request: + ipChain: + - ip: 11.22.33.44 + version: V4 + securityContext: {} + severity: WARN + target: + - id: /api/v1/logs + type: URL Pattern + - id: b192d91c-b242-36da-9332-d97a5579f865 + type: Bucket Uuid + transaction: + detail: + requestApiTokenId: 00T1bjatrp6Nl1dOc5d7 + id: 234cf34e0081e025e1fe14224464bbd6 + type: WEB + uuid: 44aeb388-08b4-11ef-9cec-73ffcb6f9fdd + version: "0" + - Name: Login without marker + ExpectedResult: true + Log: + actor: + alternateId: alice.beaver@company.com + displayName: Alice Beaver + id: 00u99ped55av2JpGs5d7 + type: User + authenticationContext: + authenticationStep: 0 + externalSessionId: trsxcsf59kYRG-GwAbWjw-PZA + client: + device: Unknown + ipAddress: 11.22.33.44 + userAgent: + browser: UNKNOWN + os: Unknown + rawUserAgent: Go-http-client/2.0 + zone: "null" + debugContext: + debugData: + dtHash: 53dd1a7513e0256eb13b9a47bb07ed61e8ca3d35fbdc36c909567a21a65a2b19 + rateLimitBucketUuid: b192d91c-b242-36da-9332-d97a5579f865 + rateLimitScopeType: ORG + rateLimitSecondsToReset: "6" + requestId: 234cf34e0081e025e1fe14224464bbd6 + requestUri: /api/v1/logs + threshold: "20" + timeSpan: "1" + timeUnit: MINUTES + url: /api/v1/logs?since=2023-09-21T17%3A04%3A22Z&limit=1000&after=1714675441520_1 + userId: 00u99ped55av2JpGs5d7 + warningPercent: "60" + displayMessage: Rate limit warning + eventType: system.org.rate_limit.warning + legacyEventType: core.framework.ratelimit.warning + outcome: + result: SUCCESS + published: "2024-05-02 18:46:21.121000000" + request: + ipChain: + - ip: 11.22.33.44 + version: V4 + securityContext: {} + severity: WARN + target: + - id: /api/v1/logs + type: URL Pattern + - id: b192d91c-b242-36da-9332-d97a5579f865 + type: Bucket Uuid + transaction: + detail: + requestApiTokenId: 00T1bjatrp6Nl1dOc5d7 + id: 234cf34e0081e025e1fe14224464bbd6 + type: WEB + uuid: 44aeb388-08b4-11ef-9cec-73ffcb6f9fdd + version: "0" diff --git a/rules/okta_rules/okta_sso_to_aws.py b/rules/okta_rules/okta_sso_to_aws.py new file mode 100644 index 000000000..30c7cb5c2 --- /dev/null +++ b/rules/okta_rules/okta_sso_to_aws.py @@ -0,0 +1,8 @@ +def rule(event): + return all( + [ + event.get("eventType") == "user.authentication.sso", + event.deep_get("outcome", "result") == "SUCCESS", + "AWS IAM Identity Center" in event.deep_walk("target", "displayName"), + ] + ) diff --git a/rules/okta_rules/okta_sso_to_aws.yml b/rules/okta_rules/okta_sso_to_aws.yml new file mode 100644 index 000000000..0f2f94209 --- /dev/null +++ b/rules/okta_rules/okta_sso_to_aws.yml @@ -0,0 +1,39 @@ +AnalysisType: rule +Filename: okta_sso_to_aws.py +RuleID: "Okta.SSO.to.AWS" +DisplayName: "SIGNAL - Okta SSO to AWS" +Enabled: true +CreateAlert: false +LogTypes: + - Okta.SystemLog +Severity: Info +DedupPeriodMinutes: 60 +Threshold: 1 +Tests: + - Name: AWS SSO via Okta + ExpectedResult: true + Log: + displayMessage: User single sign on to app + eventType: user.authentication.sso + legacyEventType: app.auth.sso + outcome: + result: SUCCESS + securityContext: {} + severity: INFO + target: + - alternateId: AWS Production + detailEntry: + signOnModeType: SAML_2_0 + displayName: AWS IAM Identity Center + id: 0oaua5ldoougycQAO696 + type: AppInstance + - alternateId: aardvark + displayName: aardvark + id: 0ua8aardvarkD697 + type: AppUser + transaction: + detail: {} + id: 1a3852fc0d172ecdad0e2447e47fbc98 + type: WEB + uuid: 35cae732-21bd-11ef-a011-dd05aa53a11a + version: "0" diff --git a/rules/push_security_rules/push_security_authorized_idp_login.py b/rules/push_security_rules/push_security_authorized_idp_login.py new file mode 100644 index 000000000..3960dadf7 --- /dev/null +++ b/rules/push_security_rules/push_security_authorized_idp_login.py @@ -0,0 +1,27 @@ +# Configure allowed identity provider logins to SaaS apps +allowed_idps = { + # "GOOGLE_WORKSPACE": {"OIDC_LOGIN", "SAML_LOGIN"}, + "OKTA": {"PASSWORD_LOGIN"}, +} + + +def rule(event): + if event.get("object") != "LOGIN": + return False + + identity_provider = event.deep_get("new", "identityProvider") + login_type = event.deep_get("new", "loginType") + + if identity_provider in allowed_idps and login_type in allowed_idps[identity_provider]: + return True + return False + + +def title(event): + identity_provider = event.deep_get("new", "identityProvider", default="Null identityProvider") + login_type = event.deep_get("new", "loginType", default="Null loginType") + app_type = event.deep_get("new", "appType", default="Null appType") + new_email = event.deep_get("new", "email") + + return f"Authorized identity provider in use. User: {new_email} \ + used {identity_provider} {login_type} on {app_type}" diff --git a/rules/push_security_rules/push_security_authorized_idp_login.yml b/rules/push_security_rules/push_security_authorized_idp_login.yml new file mode 100644 index 000000000..a49dcf805 --- /dev/null +++ b/rules/push_security_rules/push_security_authorized_idp_login.yml @@ -0,0 +1,118 @@ +AnalysisType: rule +Filename: push_security_authorized_idp_login.py +RuleID: "Push.Security.Authorized.IdP.Login" +DisplayName: "Push Security Authorized IdP Login" +Enabled: false +CreateAlert: false +LogTypes: + - PushSecurity.Activity +Tags: + - Configuration Required +Severity: Info +Description: Login to application with unauthorized identity provider which could indicate a SAMLjacking attack. +DedupPeriodMinutes: 60 +Threshold: 1 +Reference: https://github.com/pushsecurity/saas-attacks/blob/main/techniques/samljacking/description.md +InlineFilters: + - All: [] +Tests: + - Name: Google Workspace Password Login + ExpectedResult: false + Log: + id: d240e3f2-3cd6-425f-a835-dad0ff237d09 + new: + accountId: a93b45a7-fdce-489e-b76d-2bd6862a62ba + appId: 8348ca36-d254-4e1b-8f31-6837d82fc5cb + appType: GOOGLE_WORKSPACE + browser: EDGE + email: jet.black@issp.com + employeeId: ca6cf7ce-90e6-4eb5-a262-7899bc48c39c + identityProvider: GOOGLE_WORKSPACE + leakedPassword: false + loginTimestamp: 1.707773386e+09 + loginType: PASSWORD_LOGIN + os: WINDOWS + passwordId: 6ae9f0b2-9300-43f0-b210-c0d3c16640f8 + passwordManuallyTyped: false + sourceIpAddress: 35.90.103.134 + userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.81 + weakPassword: false + weakPasswordReasons: null + object: LOGIN + timestamp: 1.707774319e+09 + version: "1" + - Name: Microsoft 365 OIDC Login + ExpectedResult: false + Log: + id: d240e3f2-3cd6-425f-a835-dad0ff237d09 + new: + accountId: a93b45a7-fdce-489e-b76d-2bd6862a62ba + appId: 8348ca36-d254-4e1b-8f31-6837d82fc5cb + appType: DROPBOX + browser: EDGE + email: jet.black@issp.com + employeeId: ca6cf7ce-90e6-4eb5-a262-7899bc48c39c + identityProvider: MICROSOFT_365 + leakedPassword: false + loginTimestamp: 1.707773386e+09 + loginType: OIDC_LOGIN + os: WINDOWS + passwordId: 6ae9f0b2-9300-43f0-b210-c0d3c16640f8 + passwordManuallyTyped: false + sourceIpAddress: 35.90.103.134 + userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.81 + weakPassword: false + weakPasswordReasons: null + object: LOGIN + timestamp: 1.707774319e+09 + version: "1" + - Name: Okta Login + ExpectedResult: true + Log: + id: d240e3f2-3cd6-425f-a835-dad0ff237d09 + new: + accountId: a93b45a7-fdce-489e-b76d-2bd6862a62ba + appId: 8348ca36-d254-4e1b-8f31-6837d82fc5cb + appType: Dropbox + browser: EDGE + email: jet.black@issp.com + employeeId: ca6cf7ce-90e6-4eb5-a262-7899bc48c39c + identityProvider: OKTA + leakedPassword: false + loginTimestamp: 1.707773386e+09 + loginType: PASSWORD_LOGIN + os: WINDOWS + passwordId: 6ae9f0b2-9300-43f0-b210-c0d3c16640f8 + passwordManuallyTyped: false + sourceIpAddress: 35.90.103.134 + userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.81 + weakPassword: false + weakPasswordReasons: null + object: LOGIN + timestamp: 1.707774319e+09 + version: "1" + - Name: Password Login + ExpectedResult: false + Log: + id: d240e3f2-3cd6-425f-a835-dad0ff237d09 + new: + accountId: a93b45a7-fdce-489e-b76d-2bd6862a62ba + appId: 8348ca36-d254-4e1b-8f31-6837d82fc5cb + appType: DROPBOX + browser: EDGE + email: jet.black@issp.com + employeeId: ca6cf7ce-90e6-4eb5-a262-7899bc48c39c + identityProvider: null + leakedPassword: false + loginTimestamp: 1.707773386e+09 + loginType: PASSWORD_LOGIN + os: WINDOWS + passwordId: 6ae9f0b2-9300-43f0-b210-c0d3c16640f8 + passwordManuallyTyped: false + sourceIpAddress: 35.90.103.134 + userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.81 + weakPassword: false + weakPasswordReasons: null + object: LOGIN + timestamp: 1.707774319e+09 + version: "1"