-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into data-model-unit-tests
- Loading branch information
Showing
12 changed files
with
1,388 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
rules/aws_cloudtrail_rules/aws_cloudtrail_event_selectors_disabled.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context, lookup_aws_account_name | ||
|
||
# API calls that are indicative of CloudTrail changes | ||
CLOUDTRAIL_EDIT_SELECTORS = {"PutEventSelectors"} | ||
|
||
|
||
def rule(event): | ||
if not (aws_cloudtrail_success(event) and event.get("eventName") in CLOUDTRAIL_EDIT_SELECTORS): | ||
return False | ||
|
||
# Check if management events are included for each selector. | ||
# deep_walk only returns a list if there's more than 1 entry in the nested array, so we must | ||
# enforce it to be a list. | ||
includes = event.deep_walk("requestParameters", "eventSelectors", "includeManagementEvents") | ||
if not isinstance(includes, list): | ||
includes = [includes] | ||
|
||
# Return False all the management events are included, else return True and raise alert | ||
return not all(includes) | ||
|
||
|
||
def dedup(event): | ||
# Merge on the CloudTrail ARN | ||
return event.deep_get("requestParameters", "trailName", default="<UNKNOWN_NAME>") | ||
|
||
|
||
def title(event): | ||
return ( | ||
f"Management events have been exluded from CloudTrail [{dedup(event)}] in account " | ||
f"[{lookup_aws_account_name(event.get('recipientAccountId'))}]" | ||
) | ||
|
||
|
||
def alert_context(event): | ||
return aws_rule_context(event) |
231 changes: 231 additions & 0 deletions
231
rules/aws_cloudtrail_rules/aws_cloudtrail_event_selectors_disabled.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
AnalysisType: rule | ||
Filename: aws_cloudtrail_event_selectors_disabled.py | ||
RuleID: "AWS.CloudTrail.EventSelectorsDisabled" | ||
DisplayName: "CloudTrail Event Delectors Disabled" | ||
Enabled: true | ||
LogTypes: | ||
- AWS.CloudTrail | ||
Tags: | ||
- AWS | ||
- Security Control | ||
- Defense Evasion:Impair Defenses | ||
Reports: | ||
CIS: | ||
- 3.5 | ||
MITRE ATT&CK: | ||
- TA0005:T1562 | ||
Severity: Medium | ||
Description: > | ||
A CloudTrail Trail was modified to exclude management events for 1 or more resource types. | ||
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified | ||
Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-update-a-trail-console.html | ||
SummaryAttributes: | ||
- eventName | ||
- userAgent | ||
- sourceIpAddress | ||
- recipientAccountId | ||
- p_any_aws_arns | ||
Tests: | ||
- Name: Event Selector Disabled | ||
ExpectedResult: true | ||
Log: { | ||
"p_event_time": "2024-11-25 17:51:21.000000000", | ||
"p_log_type": "AWS.CloudTrail", | ||
"p_parse_time": "2024-11-25 17:55:54.253083422", | ||
"awsRegion": "us-west-2", | ||
"eventCategory": "Management", | ||
"eventID": "4ca1cb25-7633-496b-8f92-6de876228c3f", | ||
"eventName": "PutEventSelectors", | ||
"eventSource": "cloudtrail.amazonaws.com", | ||
"eventTime": "2024-11-25 17:51:21.000000000", | ||
"eventType": "AwsApiCall", | ||
"eventVersion": "1.11", | ||
"managementEvent": true, | ||
"readOnly": false, | ||
"recipientAccountId": "111122223333", | ||
"requestID": "a8c6184a-89b1-4fc1-a6fa-324748d48b64", | ||
"requestParameters": { | ||
"eventSelectors": [ | ||
{ | ||
"dataResources": [ | ||
{ | ||
"type": "AWS::S3::Object", | ||
"values": [] | ||
}, | ||
{ | ||
"type": "AWS::Lambda::Function", | ||
"values": [] | ||
} | ||
], | ||
"excludeManagementEventSources": [], | ||
"includeManagementEvents": false, | ||
"readWriteType": "ReadOnly" | ||
} | ||
], | ||
"trailName": "sample-cloudtrail-name" | ||
}, | ||
"responseElements": { | ||
"eventSelectors": [ | ||
{ | ||
"dataResources": [ | ||
{ | ||
"type": "AWS::S3::Object", | ||
"values": [] | ||
}, | ||
{ | ||
"type": "AWS::Lambda::Function", | ||
"values": [] | ||
} | ||
], | ||
"excludeManagementEventSources": [], | ||
"includeManagementEvents": false, | ||
"readWriteType": "ReadOnly" | ||
} | ||
], | ||
"trailARN": "arn:aws:cloudtrail:us-west-2:111122223333:trail/sample-cloudtrail-name" | ||
}, | ||
"sourceIPAddress": "1.2.3.4", | ||
"tlsDetails": { | ||
"cipherSuite": "TLS_AES_128_GCM_SHA256", | ||
"clientProvidedHostHeader": "cloudtrail.us-west-2.amazonaws.com", | ||
"tlsVersion": "TLSv1.3" | ||
}, | ||
"userAgent": "sample-user-agent", | ||
"userIdentity": { | ||
"accessKeyId": "SAMPLE_ACCESS_KEY_ID", | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins", | ||
"principalId": "EXAMPLEPRINCIPLEID:leroy.jenkins", | ||
"sessionContext": { | ||
"attributes": { | ||
"creationDate": "2024-11-25T16:53:42Z", | ||
"mfaAuthenticated": "false" | ||
}, | ||
"sessionIssuer": { | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole", | ||
"principalId": "EXAMPLEPRINCIPLEID", | ||
"type": "Role", | ||
"userName": "SampleRole" | ||
} | ||
}, | ||
"type": "AssumedRole" | ||
} | ||
} | ||
- Name: Event Selector Enabled | ||
ExpectedResult: false | ||
Log: { | ||
"p_event_time": "2024-11-25 17:51:21.000000000", | ||
"p_log_type": "AWS.CloudTrail", | ||
"p_parse_time": "2024-11-25 17:55:54.253083422", | ||
"awsRegion": "us-west-2", | ||
"eventCategory": "Management", | ||
"eventID": "4ca1cb25-7633-496b-8f92-6de876228c3f", | ||
"eventName": "PutEventSelectors", | ||
"eventSource": "cloudtrail.amazonaws.com", | ||
"eventTime": "2024-11-25 17:51:21.000000000", | ||
"eventType": "AwsApiCall", | ||
"eventVersion": "1.11", | ||
"managementEvent": true, | ||
"readOnly": false, | ||
"recipientAccountId": "111122223333", | ||
"requestID": "a8c6184a-89b1-4fc1-a6fa-324748d48b64", | ||
"requestParameters": { | ||
"eventSelectors": [ | ||
{ | ||
"dataResources": [], | ||
"excludeManagementEventSources": [], | ||
"includeManagementEvents": true, | ||
"readWriteType": "All" | ||
} | ||
], | ||
"trailName": "sample-cloudtrail-name" | ||
}, | ||
"responseElements": { | ||
"eventSelectors": [ | ||
{ | ||
"dataResources": [], | ||
"excludeManagementEventSources": [], | ||
"includeManagementEvents": true, | ||
"readWriteType": "All" | ||
} | ||
], | ||
"trailARN": "arn:aws:cloudtrail:us-west-2:111122223333:trail/sample-cloudtrail-name" | ||
}, | ||
"sourceIPAddress": "1.2.3.4", | ||
"tlsDetails": { | ||
"cipherSuite": "TLS_AES_128_GCM_SHA256", | ||
"clientProvidedHostHeader": "cloudtrail.us-west-2.amazonaws.com", | ||
"tlsVersion": "TLSv1.3" | ||
}, | ||
"userAgent": "sample-user-agent", | ||
"userIdentity": { | ||
"accessKeyId": "SAMPLE_ACCESS_KEY_ID", | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins", | ||
"principalId": "EXAMPLEPRINCIPLEID:leroy.jenkins", | ||
"sessionContext": { | ||
"attributes": { | ||
"creationDate": "2024-11-25T16:53:42Z", | ||
"mfaAuthenticated": "false" | ||
}, | ||
"sessionIssuer": { | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole", | ||
"principalId": "EXAMPLEPRINCIPLEID", | ||
"type": "Role", | ||
"userName": "SampleRole" | ||
} | ||
}, | ||
"type": "AssumedRole" | ||
} | ||
} | ||
- Name: Uninteresting Event Type | ||
ExpectedResult: false | ||
Log: { | ||
"p_event_time": "2024-11-25 17:50:24.000000000", | ||
"p_log_type": "AWS.CloudTrail", | ||
"p_parse_time": "2024-11-25 17:55:54.172592534", | ||
"awsRegion": "us-west-2", | ||
"eventCategory": "Management", | ||
"eventID": "63fb143a-c494-4510-8e9e-34172e4872c3", | ||
"eventName": "GetEventSelectors", | ||
"eventSource": "cloudtrail.amazonaws.com", | ||
"eventTime": "2024-11-25 17:50:24.000000000", | ||
"eventType": "AwsApiCall", | ||
"eventVersion": "1.11", | ||
"managementEvent": true, | ||
"readOnly": true, | ||
"recipientAccountId": "111122223333", | ||
"requestID": "cad6aff4-1558-49c5-ae4a-c512058751f1", | ||
"requestParameters": { | ||
"trailName": "sample-cloudtrail-name" | ||
}, | ||
"sourceIPAddress": "1.2.3.4", | ||
"tlsDetails": { | ||
"cipherSuite": "TLS_AES_128_GCM_SHA256", | ||
"clientProvidedHostHeader": "cloudtrail.us-west-2.amazonaws.com", | ||
"tlsVersion": "TLSv1.3" | ||
}, | ||
"userAgent": "APN/1.0 HashiCorp/1.0 Terraform/1.1.2 (+https://www.terraform.io) terraform-provider-aws/3.76.1 (+https://registry.terraform.io/providers/hashicorp/aws) aws-sdk-go/1.44.157 (go1.19.3; darwin; arm64) stratus-red-team_83c9a458-ffab-4d43-8b02-9691311e8c0a HashiCorp-terraform-exec/0.17.3", | ||
"userIdentity": { | ||
"accessKeyId": "SAMPLE_ACCESS_KEY_ID", | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins", | ||
"principalId": "EXAMPLEPRINCIPLEID:leroy.jenkins", | ||
"sessionContext": { | ||
"attributes": { | ||
"creationDate": "2024-11-25T16:53:42Z", | ||
"mfaAuthenticated": "false" | ||
}, | ||
"sessionIssuer": { | ||
"accountId": "111122223333", | ||
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole", | ||
"principalId": "EXAMPLEPRINCIPLEID", | ||
"type": "Role", | ||
"userName": "SampleRole" | ||
} | ||
}, | ||
"type": "AssumedRole" | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
rules/aws_cloudtrail_rules/aws_cloudtrail_short_lifecycle.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context, lookup_aws_account_name | ||
from panther_base_helpers import deep_get | ||
|
||
# Use this to record the names of your S3 buckets that have cloudtrail logs | ||
# If a bucket name isn't mentioned here, we still make a best guess as to whether or not it | ||
# contains CloudTrail data, but the confidence rating will be lower, and so will the severity | ||
CLOUDTRAIL_BUCKETS = ("example_cloudtrail_bucket_name",) | ||
|
||
# This is the minimum length fo time CloudTrail logs should remain in an S3 bucket. | ||
# We set this to 7 initially, since this is the recommended amount of time logs ingested by | ||
# Panther should remain available. You can modify this if you wish. | ||
CLOUDTRAIL_MINIMUM_STORAGE_PERIOD_DAYS = 7 | ||
|
||
|
||
def rule(event): | ||
# Only alert for successful PutBucketLifecycle events | ||
if not (aws_cloudtrail_success(event) and event.get("eventName") == "PutBucketLifecycle"): | ||
return False | ||
|
||
# Exit out if the bucket doesn't have cloudtrail logs | ||
# We check this be either comparing the bucket name to a list of buckets the user knows has | ||
# CT logs, or by heuristically looking at the name and guessing whether it likely has CT logs | ||
bucket_name = event.deep_get("requestParameters", "bucketName") | ||
if not bucket_name or ( | ||
not is_cloudtrail_bucket(bucket_name) and not guess_is_cloudtrail_bucket(bucket_name) | ||
): | ||
return False | ||
|
||
# Don't alert if the Rule status is disabled | ||
lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule") | ||
if lifecycle.get("Status") != "Enabled": | ||
return False | ||
|
||
# Alert if the lifecycle period is short | ||
duration = deep_get(lifecycle, "Expiration", "Days", default=0) | ||
return duration < CLOUDTRAIL_MINIMUM_STORAGE_PERIOD_DAYS | ||
|
||
|
||
def title(event): | ||
bucket_name = event.deep_get("requestParameters", "bucketName", default="<UNKNOWN S3 BUCKET>") | ||
lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule") | ||
duration = deep_get(lifecycle, "Expiration", "Days", default=0) | ||
rule_id = lifecycle.get("ID", "<UNKNOWN RULE ID>") | ||
account = event.deep_get("userIdentity", "accountId", default="<UNKNOWN_AWS_ACCOUNT>") | ||
return ( | ||
f"S3 Bucket {bucket_name} in account {lookup_aws_account_name(account)} " | ||
f"has new rule {rule_id} set to delete CloudTrail logs after " | ||
f"{duration} day{'s' if duration != 1 else ''}" | ||
) | ||
|
||
|
||
def severity(event): | ||
# Return lower severity if we aren't positive this bucket has cloudtrail logs. | ||
bucket_name = event.deep_get("requestParameters", "bucketName") | ||
if not is_cloudtrail_bucket(bucket_name): | ||
return "LOW" | ||
return "DEFAULT" | ||
|
||
|
||
def alert_context(event): | ||
context = aws_rule_context(event) | ||
|
||
# Add name of S3 bucket, Rule ID, and expiration duration to context | ||
bucket_name = event.deep_get("requestParameters", "bucketName", default="<UNKNOWN S3 BUCKET>") | ||
lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule") | ||
duration = deep_get(lifecycle, "Expiration", "Days", default=0) | ||
rule_id = lifecycle.get("ID", "<UNKNOWN RULE ID>") | ||
context.update( | ||
{ | ||
"bucketName": bucket_name, | ||
"lifecycleRuleID": rule_id, | ||
"lifecycleRuleDurationDays": duration, | ||
} | ||
) | ||
|
||
return context | ||
|
||
|
||
def is_cloudtrail_bucket(bucket_name: str) -> bool: | ||
"""Returns True if the bucket is known to contain CloudTrail logs.""" | ||
return bucket_name in CLOUDTRAIL_BUCKETS | ||
|
||
|
||
def guess_is_cloudtrail_bucket(bucket_name: str) -> bool: | ||
"""Takes a best guess at whether a bucket contains CloudTrail logs or not.""" | ||
# Maybe one day, this check will get more complex | ||
return "trail" in bucket_name.lower() |
Oops, something went wrong.