Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions filters/aws/aws.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AWS filter v1.1.0: CloudTrail JSON and identified GuardDuty EventBridge findings.
# AWS filter v1.1.1: CloudTrail JSON and identified GuardDuty EventBridge findings.
# Original vendor objects and legacy aliases are retained. API success is request success.
# Correlation candidates are generated here, never trusted from input.
pipeline:
Expand Down Expand Up @@ -282,13 +282,14 @@ pipeline:
params:
key: actionResult
value: success
# CheckMfa only checks whether MFA is required; it does not finish a sign-in.
# Preserve its vendor response without promoting the intermediate success.
where: equals("log.awsRecordType","cloudtrail") && !((exists("log.errorCode") && !equals("log.errorCode","")) || (exists("log.errorMessage")
&& !equals("log.errorMessage","")) || (exists("log.responseElements.errorCode") && !equals("log.responseElements.errorCode",""))
|| (exists("log.responseElements.errorMessage") && !equals("log.responseElements.errorMessage","")) || equalsIgnoreCase("log.responseElements.ConsoleLogin","Failure")
|| equalsIgnoreCase("log.responseElements.GetSigninToken","Failure") || equalsIgnoreCase("log.responseElements.CheckMfa","Failure"))
&& (equals("log.eventType","AwsApiCall") || (equals("log.eventName","ConsoleLogin") && equals("log.responseElements.ConsoleLogin","Success"))
|| (equals("log.eventName","GetSigninToken") && equals("log.responseElements.GetSigninToken","Success")) || (equals("log.eventName","CheckMfa")
&& equals("log.responseElements.CheckMfa","Success")))
|| (equals("log.eventName","GetSigninToken") && equals("log.responseElements.GetSigninToken","Success")))
- grok:
source: log.recipientAccountId
patterns:
Expand Down Expand Up @@ -387,7 +388,7 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.aws_ecs_credential_theft
key: log.correlationCandidate.awsEcsCredentialTheft
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "ecs.amazonaws.com") &&
Expand All @@ -398,14 +399,14 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.saml_provider_change
key: log.correlationCandidate.samlProviderChange
value: match
where: equals("log.awsRecordType","cloudtrail") && equals("log.eventSource","iam.amazonaws.com") && oneOf("log.eventName",["CreateSAMLProvider",
"UpdateSAMLProvider"]) && equals("actionResult","success")
- add:
function: string
params:
key: log.correlationCandidate.aws_securityhub_finding_evasion
key: log.correlationCandidate.awsSecurityhubFindingEvasion
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "securityhub.amazonaws.com") &&
Expand All @@ -415,7 +416,7 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.aws_ssm_sendcommand_abuse
key: log.correlationCandidate.awsSsmSendcommandAbuse
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "ssm.amazonaws.com") &&
Expand All @@ -424,43 +425,43 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.aws_sso_suspicious_activities
key: log.correlationCandidate.awsSsoSuspiciousActivities
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","sso.amazonaws.com") && oneOf("log.eventName",["CreatePermissionSet",
"AttachManagedPolicyToPermissionSet", "DeletePermissionSet", "PutInlinePolicyToPermissionSet"]) && equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.cloudformation_stack_deletion
key: log.correlationCandidate.cloudformationStackDeletion
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "cloudformation.amazonaws.com") && equals("log.eventName",
"DeleteStack") && equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.console_login_impossible_travel
key: log.correlationCandidate.consoleLoginImpossibleTravel
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","signin.amazonaws.com") && oneOf("log.eventName",["ConsoleLogin"])
&& equals("actionResult","success") && regexMatch("origin.geolocation.countryCode","^[A-Z]{2}$"))
- add:
function: string
params:
key: log.correlationCandidate.cross_account_access_anomalies
key: log.correlationCandidate.crossAccountAccessAnomalies
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","sts.amazonaws.com") && oneOf("log.eventName",["AssumeRole"])
&& equals("actionResult","success") && regexMatch("log.userIdentity.accountId","^[0-9]{12}$") && regexMatch("log.responseElements.assumedRoleUser.arn","^arn:[^:]+:sts::[0-9]{12}:assumed-role/.+$")
&& !regexMatch("log.responseElements.assumedRoleUser.arn","^arn:[^:]+:sts::" + safe("log.userIdentity.accountId", "") + ":assumed-role/"))
- add:
function: string
params:
key: log.correlationCandidate.iam_backdoor_creation_attempts
key: log.correlationCandidate.iamBackdoorCreationAttempts
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "iam.amazonaws.com") && oneOf("log.eventName", ["CreateUser",
"CreateAccessKey", "AttachUserPolicy", "PutUserPolicy", "CreateLoginProfile"]) && equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.iam_privilege_escalation_paths
key: log.correlationCandidate.iamPrivilegeEscalationPaths
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "iam.amazonaws.com") &&
Expand All @@ -469,28 +470,28 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.lambda_privilege_escalation
key: log.correlationCandidate.lambdaPrivilegeEscalation
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","iam.amazonaws.com") && oneOf("log.eventName",["AttachRolePolicy",
"AttachUserPolicy"]) && equals("actionResult","success") && regexMatch("log.requestParameters.policyArn","^arn:(aws|aws-cn|aws-us-gov):iam::aws:policy/(AdministratorAccess|IAMFullAccess)$"))
- add:
function: string
params:
key: log.correlationCandidate.mass_resource_deletion
key: log.correlationCandidate.massResourceDeletion
value: match
where: equals("log.awsRecordType","cloudtrail") && (regexMatch("log.eventName","^(Delete|Terminate|Remove)[A-Z]") && equals("actionResult","success")
&& !equals("log.eventSource","s3.amazonaws.com"))
- add:
function: string
params:
key: log.correlationCandidate.route53_dns_hijacking
key: log.correlationCandidate.route53DnsHijacking
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "route53.amazonaws.com") && equals("log.eventName",
"ChangeResourceRecordSets") && equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.s3_bulk_data_exfiltration
key: log.correlationCandidate.s3BulkDataExfiltration
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "s3.amazonaws.com") &&
Expand All @@ -499,14 +500,14 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.secrets_manager_suspicious_access
key: log.correlationCandidate.secretsManagerSuspiciousAccess
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "secretsmanager.amazonaws.com") && (equals("log.eventName",
"GetSecretValue") || equals("log.eventName", "BatchGetSecretValue")) && equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.security_group_modifications
key: log.correlationCandidate.securityGroupModifications
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","ec2.amazonaws.com") && oneOf("log.eventName",["AuthorizeSecurityGroupIngress",
"AuthorizeSecurityGroupEgress", "ModifySecurityGroupRules"]) && equals("actionResult","success") && (exists('log.requestParameters.ipPermissions.items.#(ipRanges.items.#(cidrIp=="0.0.0.0/0"))')
Expand All @@ -515,7 +516,7 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.ssm_session_abuse
key: log.correlationCandidate.ssmSessionAbuse
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "ssm.amazonaws.com") &&
Expand All @@ -524,7 +525,7 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.sts_token_abuse
key: log.correlationCandidate.stsTokenAbuse
value: match
where: |-
equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource", "sts.amazonaws.com") &&
Expand All @@ -537,7 +538,7 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.unusual_api_call_patterns
key: log.correlationCandidate.unusualApiCallPatterns
value: match
where: "equals(\"log.awsRecordType\",\"cloudtrail\") && (exists(\"log.eventSource\") && \nexists(\"log.sourceIPAddress\") &&\n\
exists(\"log.eventName\") &&\n(\n oneOf(\"log.eventName\", [\"DescribeSecurityGroups\", \"DescribeNetworkAcls\", \"DescribeVpcs\"\
Expand All @@ -548,21 +549,21 @@ pipeline:
- add:
function: string
params:
key: log.correlationCandidate.vpc_flow_log_anomalies
key: log.correlationCandidate.vpcFlowLogAnomalies
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","ec2.amazonaws.com") && oneOf("log.eventName",["DeleteFlowLogs"])
&& equals("actionResult","success"))
- add:
function: string
params:
key: log.correlationCandidate.credential_access_aws_iam_assume_role_brute_force
key: log.correlationCandidate.credentialAccessAwsIamAssumeRoleBruteForce
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","iam.amazonaws.com") && oneOf("log.eventName",["UpdateAssumeRolePolicy"])
&& oneOf("log.errorCode",["MalformedPolicyDocument", "MalformedPolicyDocumentException"]) && equals("actionResult","failure"))
- add:
function: string
params:
key: log.correlationCandidate.credential_access_root_console_failure_brute_force
key: log.correlationCandidate.credentialAccessRootConsoleFailureBruteForce
value: match
where: equals("log.awsRecordType","cloudtrail") && (equals("log.eventSource","signin.amazonaws.com") && oneOf("log.eventName",["ConsoleLogin"])
&& equals("log.userIdentity.type","Root") && oneOf("actionResult",["failure", "denied"]))
40 changes: 40 additions & 0 deletions plugins/alerts/aws_action_result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"testing"

"github.com/threatwinds/go-sdk/plugins"
"github.com/tidwall/gjson"
)

// These are fabricated raw CloudTrail records exercised by the existing offline
// extraction model and the pinned SDK CEL evaluator. They do not run the closed
// EventProcessor or feeds plugin and do not establish that a TI alert was created.
func TestAWSAuthenticationOutcome(t *testing.T) {
cfg, cache := awsConfig(t), plugins.NewCELCache("aws-authentication-outcome")
for _, tc := range []struct {
name, raw, result string
}{
{"MFA requirement check", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"CheckMfa","sourceIPAddress":"192.0.2.10","responseElements":{"CheckMfa":"Success"}}`, ""},
{"failed check", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"CheckMfa","sourceIPAddress":"192.0.2.10","responseElements":{"CheckMfa":"Failure"}}`, "failure"},
{"completed sign-in", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"ConsoleLogin","sourceIPAddress":"192.0.2.10","responseElements":{"ConsoleLogin":"Success"}}`, "success"},
{"failed sign-in", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"ConsoleLogin","sourceIPAddress":"192.0.2.10","responseElements":{"ConsoleLogin":"Failure"}}`, "failure"},
{"missing sign-in outcome", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"ConsoleLogin","sourceIPAddress":"192.0.2.10","responseElements":null}`, ""},
{"denial overrides success", `{"eventVersion":"1.09","eventSource":"signin.amazonaws.com","eventType":"AwsConsoleSignIn","eventName":"ConsoleLogin","sourceIPAddress":"192.0.2.10","errorCode":"AccessDenied","responseElements":{"ConsoleLogin":"Success"}}`, "denied"},
} {
t.Run(tc.name, func(t *testing.T) {
out := awsParse(t, cfg, tc.raw, "synthetic-collector", cache)
result := gjson.Get(out, "actionResult")
if result.String() != tc.result || (tc.result == "" && result.Exists()) {
t.Fatalf("actionResult = %s, want %q", result.Raw, tc.result)
}
eligible, err := cache.Eval(`equals("actionResult","success") && inCIDR("origin.ip","0.0.0.0/0")`, out)
if err != nil || eligible != (tc.result == "success") {
t.Fatalf("SDK success-and-IP predicate = %v, %v", eligible, err)
}
if tc.name == "MFA requirement check" && gjson.Get(out, "log.responseElements.CheckMfa").String() != "Success" {
t.Fatal("vendor check result was lost")
}
})
}
}
14 changes: 11 additions & 3 deletions plugins/alerts/aws_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ func awsRegex(t *testing.T, g *plugins.Grok, cfg *plugins.Config) *regexp.Regexp
}
return r
}

// awsStoredName is the name the parser plugins store for a grok, rename or add target:
// utils.SanitizeField keeps only letters, digits and dots.
func awsStoredName(name string) string {
utils.SanitizeField(&name)
return name
}

func awsParse(t *testing.T, cfg *plugins.Config, raw string, dataSource string, cache *plugins.CELCache, enrichment ...map[string]any) string {
return awsParseMode(t, cfg, raw, dataSource, cache, false, enrichment...)
}
Expand Down Expand Up @@ -169,13 +177,13 @@ func awsParseMode(t *testing.T, cfg *plugins.Config, raw string, dataSource stri
}
for i, p := range g.Patterns {
if p.FieldName != "" {
awsPut(draft, p.FieldName, m[r.SubexpIndex(fmt.Sprintf("f%d", i))], false)
awsPut(draft, awsStoredName(p.FieldName), m[r.SubexpIndex(fmt.Sprintf("f%d", i))], false)
}
}
case "rename":
for _, p := range s.Rename.From {
if v, ok := awsGet(draft, p); ok {
awsPut(draft, s.Rename.To, v, false)
awsPut(draft, awsStoredName(s.Rename.To), v, false)
awsPut(draft, p, nil, true)
break
}
Expand All @@ -184,7 +192,7 @@ func awsParseMode(t *testing.T, cfg *plugins.Config, raw string, dataSource stri
if s.Add.Function != "string" {
t.Fatalf("unsupported add function %s", s.Add.Function)
}
awsPut(draft, s.Add.Params["key"].GetStringValue(), s.Add.Params["value"].AsInterface(), false)
awsPut(draft, awsStoredName(s.Add.Params["key"].GetStringValue()), s.Add.Params["value"].AsInterface(), false)
case "delete":
for _, p := range s.Delete.Fields {
awsPut(draft, p, nil, true)
Expand Down
13 changes: 7 additions & 6 deletions plugins/alerts/aws_history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ func TestAWSSDKHistory(t *testing.T) {
queries := 0
mapping := map[string]any{"properties": map[string]any{}}
props := mapping["properties"].(map[string]any)
paths := []string{"dataSource", "log.awsAccountKeyType", "log.awsAccountKey", "log.awsActorKeyType", "log.awsActorKey", "origin.ip", "origin.geolocation.countryCode", "log.eventName", "log.correlationCandidate.saml_provider_change"}
paths := []string{"dataSource", "log.awsAccountKeyType", "log.awsAccountKey", "log.awsActorKeyType", "log.awsActorKey", "origin.ip", "origin.geolocation.countryCode", "log.eventName"}
for name, r := range rules {
if len(r.Correlation) > 0 {
paths = append(paths, "log.correlationCandidate."+name)
if awsHistoryMarkers[name] == "" {
t.Fatalf("history rule %s has no correlation marker", name)
}
paths = append(paths, "log.correlationCandidate."+awsHistoryMarkers[name])
}
}
for _, path := range paths {
Expand Down Expand Up @@ -225,10 +228,8 @@ func TestAWSSDKHistory(t *testing.T) {
if yes, e := cache.Eval(r.Where, out); e != nil || !yes {
t.Fatalf("raw trigger failed: %v %v", yes, e)
}
marker := "log.correlationCandidate." + tc.rule
if tc.rule == "aws_golden_saml_attack" {
marker = "log.correlationCandidate.saml_provider_change"
}
// The filter stores this marker and the rule counts it under the same name.
marker := "log.correlationCandidate." + awsHistoryMarkers[tc.rule]
terms = map[string]string{"dataSource": "collector-test", "log.awsAccountKeyType": "recipient", "log.awsAccountKey": "123456789012", marker: "match"}
notTerms = map[string]string{}
if tc.mode == "ip" {
Expand Down
Loading
Loading