Skip to content

Commit 308e7c5

Browse files
authored
Merge pull request #24 from rubysoho07/amazon-q-rule
feat: Adopt Amazon Q Developer
2 parents 8d907e5 + d4eff6d commit 308e7c5

10 files changed

Lines changed: 303 additions & 5 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Lambda Layer in Python
2+
3+
## Purpose
4+
5+
This rule manages features to process CloudTrail log by event name.
6+
7+
## Instructions
8+
9+
* Please refer the rule of a CloudTrail event: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-events.html
10+
* Get AWS service name from `eventSource` in the event. If `eventSource` is "cloudtrail.amazonaws.com", use "cloudtrail" before "amazonaws.com". (ID: SERVICE_NAME)
11+
* Create a file if `layer/python/cloudtrail_watcher/services/SERVICE_NAME.py` doesn't exist. File structure is below. (ID: SERVICE_FILE)
12+
13+
```python
14+
import boto3
15+
16+
from .common import *
17+
18+
SERVICE_NAME = boto3.client('SERVICE_NAME')
19+
20+
def process_event(event: dict, set_tag: bool = False) -> dict:
21+
""" Process CloudTrail event for SERVICE_NAME """
22+
23+
result = dict()
24+
25+
return result
26+
```
27+
* If you created a new file, add file name to `__all__` in `lambda/python/cloudtrail_watcher/services/__init__.py` as a new module.
28+
29+
* Get the name of the event from `eventName` in the CloudTrail event. (ID: EVENT_NAME)
30+
* Create a function in SERVICE_FILE. (ID: PROCESS_EVENT_FUNCTION)
31+
* Function name: Start with `_process_`, add EVENT_NAME with snake case.
32+
* Arguments: event in dict, set_tag in bool. The default value of set_tag is False.
33+
* Return value: resource name in list
34+
* If set_tag is True, check whether 'User' tag exists.
35+
* If 'User' tag doesn't exists, set 'User' tag by using 'get_user_identity' function in 'common.py'.
36+
* Add a rule below in process_event function.
37+
38+
```python
39+
if event['eventName'] == 'EVENT_NAME':
40+
result['resource_id'] = "Call the function created from PROCESS_FUNCTION"
41+
else:
42+
message = f"Cannot process event: {event['eventName']}, eventID: f{event['eventID']}"
43+
result['error'] = message
44+
```
45+
46+
* Add permission for used in PROCESS_EVENT_FUNCTION.
47+
* File name: "template.sar.yaml" in the root of the project
48+
* Add permission in IAM Policy document of WatcherFunctionPolicy resource.
49+
50+
## Priority
51+
52+
High
53+
54+
## Error Handling
55+
56+
N/A
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Test Code Modification
2+
3+
## Purpose
4+
5+
This rule modifies test code.
6+
7+
## Instructions
8+
9+
* If added new file in test/services/samples, mask sensitive data.
10+
* If accountId is not "000000000000", change it to "000000000000".
11+
* If values is not "test_user" after "user/", change it to "test_user".
12+
* Change same values with sourceIPAddress to "127.0.0.1".
13+
* Change principalId to "YOUR_PRINCIPAL_ID".
14+
* Change accessKeyId to "YOUR_ACCESS_KEY_ID".
15+
* If you discover same values in other places, change them in the same way.
16+
* Create test case for the action in test/services/test_services.py file.
17+
* Create a class and a test case for the service if the class doesn't exist.
18+
* If the class exists, just create a test case in the class.
19+
20+
## Priority
21+
22+
Low
23+
24+
## Error Handling
25+
26+
N/A

layer/python/cloudtrail_watcher/services/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@
1515
'airflow',
1616
'dynamodb',
1717
'elasticloadbalancing',
18-
'cloudfront'
18+
'cloudfront',
19+
'sqs',
20+
'sns'
1921
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import boto3
2+
3+
from .common import *
4+
5+
sns = boto3.client('sns')
6+
7+
def process_event(event: dict, set_tag: bool = False) -> dict:
8+
""" Process CloudTrail event for sns """
9+
10+
result = dict()
11+
12+
if event['eventName'] == 'CreateTopic':
13+
result['resource_id'] = _process_create_topic(event, set_tag)
14+
else:
15+
message = f"Cannot process event: {event['eventName']}, eventID: {event['eventID']}"
16+
result['error'] = message
17+
18+
return result
19+
20+
21+
def _process_create_topic(event: dict, set_tag: bool = False) -> list:
22+
""" Process CreateTopic event """
23+
24+
topic_arn = event['responseElements']['topicArn']
25+
topic_name = event['requestParameters']['name']
26+
27+
if set_tag:
28+
if not check_contain_mandatory_tag_list(event['requestParameters']['tags']):
29+
sns.tag_resource(
30+
ResourceArn=topic_arn,
31+
Tags=[{'Key': 'User', 'Value': get_user_identity(event)}]
32+
)
33+
34+
return [topic_name]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import boto3
2+
3+
from .common import *
4+
5+
sqs = boto3.client('sqs')
6+
7+
8+
def process_event(event: dict, set_tag: bool = False) -> dict:
9+
""" Process CloudTrail event for SQS """
10+
11+
result = dict()
12+
13+
if event['eventName'] == 'CreateQueue':
14+
result['resource_id'] = _process_create_queue(event, set_tag)
15+
else:
16+
message = f"Cannot process event: {event['eventName']}, eventID: {event['eventID']}"
17+
result['error'] = message
18+
19+
return result
20+
21+
22+
def _process_create_queue(event: dict, set_tag: bool = False) -> list:
23+
""" Process CreateQueue event """
24+
25+
if set_tag is True:
26+
if 'tags' not in event['requestParameters'] or \
27+
check_contain_mandatory_tag_dict(event['requestParameters']['tags']) is False:
28+
sqs.tag_queue(
29+
QueueUrl=event['responseElements']['queueUrl'],
30+
Tags={
31+
'User': get_user_identity(event)
32+
}
33+
)
34+
35+
return [event['requestParameters']['queueName']]

template.sar.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ Resources:
6868
"cloudfront:ListTagsForResource",
6969
"cloudfront:TagResource",
7070
"ecr:ListTagsForResource",
71-
"ecr:TagResource"
71+
"ecr:TagResource",
72+
"sqs:TagQueue",
73+
"sns:TagResource"
7274
]
7375
Resource: "*"
7476

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"eventVersion": "1.11",
3+
"userIdentity": {
4+
"type": "IAMUser",
5+
"principalId": "YOUR_PRINCIPAL_ID",
6+
"arn": "arn:aws:iam::000000000000:user/test_user",
7+
"accountId": "000000000000",
8+
"accessKeyId": "YOUR_ACCESS_KEY_ID",
9+
"userName": "test_user",
10+
"sessionContext": {
11+
"attributes": {
12+
"creationDate": "2025-09-05T02:33:23Z",
13+
"mfaAuthenticated": "true"
14+
}
15+
}
16+
},
17+
"eventTime": "2025-09-05T13:15:27Z",
18+
"eventSource": "sns.amazonaws.com",
19+
"eventName": "CreateTopic",
20+
"awsRegion": "ap-northeast-2",
21+
"sourceIPAddress": "127.0.0.1",
22+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
23+
"requestParameters": {
24+
"name": "sns-test.fifo",
25+
"attributes": {
26+
"Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:Publish\",\"SNS:RemovePermission\",\"SNS:SetTopicAttributes\",\"SNS:DeleteTopic\",\"SNS:ListSubscriptionsByTopic\",\"SNS:GetTopicAttributes\",\"SNS:AddPermission\",\"SNS:Subscribe\"],\"Resource\":\"arn:aws:sns:ap-northeast-2:000000000000:sns-test.fifo\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"000000000000\"}}}]}",
27+
"TracingConfig": "PassThrough",
28+
"FifoTopic": "true",
29+
"FifoThroughputScope": "MessageGroup"
30+
},
31+
"tags": []
32+
},
33+
"responseElements": {
34+
"topicArn": "arn:aws:sns:ap-northeast-2:000000000000:sns-test.fifo"
35+
},
36+
"requestID": "e9345ab2-abc8-5d6e-a882-3b864f67588a",
37+
"eventID": "ae241752-2a62-4166-9991-c1854aee9d36",
38+
"readOnly": false,
39+
"eventType": "AwsApiCall",
40+
"managementEvent": true,
41+
"recipientAccountId": "000000000000",
42+
"eventCategory": "Management",
43+
"tlsDetails": {
44+
"tlsVersion": "TLSv1.3",
45+
"cipherSuite": "TLS_AES_128_GCM_SHA256",
46+
"clientProvidedHostHeader": "sns.ap-northeast-2.amazonaws.com"
47+
},
48+
"sessionCredentialFromConsole": "true"
49+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"eventVersion": "1.11",
3+
"userIdentity": {
4+
"type": "IAMUser",
5+
"principalId": "YOUR_PRINCIPAL_ID",
6+
"arn": "arn:aws:iam::000000000000:user/test_user",
7+
"accountId": "000000000000",
8+
"accessKeyId": "YOUR_ACCESS_KEY_ID",
9+
"userName": "test_user",
10+
"sessionContext": {
11+
"attributes": {
12+
"creationDate": "2025-09-05T02:33:23Z",
13+
"mfaAuthenticated": "true"
14+
}
15+
}
16+
},
17+
"eventTime": "2025-09-05T10:43:21Z",
18+
"eventSource": "sqs.amazonaws.com",
19+
"eventName": "CreateQueue",
20+
"awsRegion": "ap-northeast-2",
21+
"sourceIPAddress": "127.0.0.1",
22+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
23+
"requestParameters": {
24+
"queueName": "test-queue",
25+
"attributes": {
26+
"Policy": "{\"Version\":\"2012-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__owner_statement\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"000000000000\"},\"Action\":[\"SQS:*\"],\"Resource\":\"arn:aws:sqs:ap-northeast-2:000000000000:test-queue\"}]}",
27+
"ReceiveMessageWaitTimeSeconds": "0",
28+
"SqsManagedSseEnabled": "true",
29+
"DelaySeconds": "0",
30+
"KmsMasterKeyId": "",
31+
"RedrivePolicy": "",
32+
"MessageRetentionPeriod": "345600",
33+
"MaximumMessageSize": "1048576",
34+
"VisibilityTimeout": "30",
35+
"RedriveAllowPolicy": ""
36+
},
37+
"tags": {
38+
"TestKey": "TestValue"
39+
}
40+
},
41+
"responseElements": {
42+
"queueUrl": "https://sqs.ap-northeast-2.amazonaws.com/000000000000/test-queue"
43+
},
44+
"requestID": "ded62e37-7691-5699-bb20-4f03b7984293",
45+
"eventID": "b5a8ec24-29c1-4ebd-8dbb-3e328e66818f",
46+
"readOnly": false,
47+
"resources": [
48+
{
49+
"accountId": "000000000000",
50+
"type": "AWS::SQS::Queue",
51+
"ARN": "arn:aws:sqs:ap-northeast-2:000000000000:test-queue"
52+
}
53+
],
54+
"eventType": "AwsApiCall",
55+
"managementEvent": true,
56+
"recipientAccountId": "000000000000",
57+
"eventCategory": "Management",
58+
"tlsDetails": {
59+
"tlsVersion": "TLSv1.3",
60+
"cipherSuite": "TLS_AES_128_GCM_SHA256",
61+
"clientProvidedHostHeader": "sqs.ap-northeast-2.amazonaws.com"
62+
},
63+
"sessionCredentialFromConsole": "true"
64+
}

test/services/test_process_event_by_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import unittest
33

4-
from layer.python.cloudtrail_watcher.utils import build_result, notify_slack
4+
from cloudtrail_watcher.utils import build_result, notify_slack
55

66

77
class ProcessEventTest(unittest.TestCase):

test/services/test_services.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import unittest
33

4-
from layer.python.cloudtrail_watcher.utils import build_result
4+
from cloudtrail_watcher.utils import build_result
55

66

77
class EC2Test(unittest.TestCase):
@@ -413,4 +413,34 @@ def test_create_repository(self):
413413
self.assertEqual(result['region'], 'ap-northeast-2')
414414
self.assertEqual(result['event_name'], 'CreateRepository')
415415
self.assertEqual(result['source_ip_address'], '127.0.0.1')
416-
self.assertEqual(result['event_source'], 'ecr')
416+
self.assertEqual(result['event_source'], 'ecr')
417+
418+
419+
class SQSTest(unittest.TestCase):
420+
def test_create_queue(self):
421+
with open('./samples/sqs_CreateQueue.json') as f:
422+
data = json.loads(f.read())
423+
424+
result = build_result(data)
425+
426+
self.assertEqual(result['resource_id'], ['test-queue'])
427+
self.assertEqual(result['identity'], 'user/test_user')
428+
self.assertEqual(result['region'], 'ap-northeast-2')
429+
self.assertEqual(result['event_name'], 'CreateQueue')
430+
self.assertEqual(result['source_ip_address'], '127.0.0.1')
431+
self.assertEqual(result['event_source'], 'sqs')
432+
433+
434+
class SNSTest(unittest.TestCase):
435+
def test_create_topic(self):
436+
with open('./samples/sns_CreateTopic.json') as f:
437+
data = json.loads(f.read())
438+
439+
result = build_result(data)
440+
441+
self.assertEqual(result['resource_id'], ['sns-test.fifo'])
442+
self.assertEqual(result['identity'], 'user/test_user')
443+
self.assertEqual(result['region'], 'ap-northeast-2')
444+
self.assertEqual(result['event_name'], 'CreateTopic')
445+
self.assertEqual(result['source_ip_address'], '127.0.0.1')
446+
self.assertEqual(result['event_source'], 'sns')

0 commit comments

Comments
 (0)