Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add object lock configuration to augment table #9532

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
59 changes: 59 additions & 0 deletions c7n/resources/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,65 @@
}


@S3.filter_registry.register('lock-configuration')
class S3LockConfigurationFilter(ValueFilter):
"""
Filter S3 buckets based on their object lock configurations

:example:

Get all buckets where lock configuration mode is COMPLIANCE

.. code-block:: yaml

policies:
- name: lock-configuration-compliance
resource: aws.s3
filters:
- type: lock-configuration
key: Rule.DefaultRetention.Mode
value: COMPLIANCE

"""
schema = type_schema('lock-configuration', rinherit=ValueFilter.schema)
permissions = ('s3:GetBucketObjectLockConfiguration',)
annotate = True
annotation_key = 'ObjectLockConfiguration'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally want a c7n: prefix on annotation keys to distinguish from native attributes.


def _process_resource(self, client, resource):
try:
config = client.get_object_lock_configuration(
Bucket=resource['Name']
)['ObjectLockConfiguration']
except ClientError as e:
if e.response['Error']['Code'] == 'ObjectLockConfigurationNotFoundError':
config = None
else:
raise
resource[self.annotation_key] = config

def process(self, resources, event=None):
client = local_session(self.manager.session_factory).client('s3')
with self.executor_factory(max_workers=3) as w:
futures = []
for res in resources:
if self.annotation_key in res:
continue

Check warning on line 885 in c7n/resources/s3.py

View check run for this annotation

Codecov / codecov/patch

c7n/resources/s3.py#L885

Added line #L885 was not covered by tests
futures.append(w.submit(self._process_resource, client, res))
for f in as_completed(futures):
exc = f.exception()
if exc:
self.log.error(
"Exception getting bucket lock configuration \n %s" % (
exc))
return super().process(resources, event)

def __call__(self, r):
if self.annotate:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaics given the class value for annotate = true, there's no need for the conditional here.

return super().__call__(r.setdefault(self.annotation_key, None))
return super().__call__(r.pop(self.annotation_key, None))

Check warning on line 898 in c7n/resources/s3.py

View check run for this annotation

Codecov / codecov/patch

c7n/resources/s3.py#L898

Added line #L898 was not covered by tests


ENCRYPTION_STATEMENT_GLOB = {
'Effect': 'Deny',
'Principal': '*',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"status_code": 200,
"data": {
"ResponseMetadata": {},
"ObjectLockConfiguration": {
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "GOVERNANCE",
"Days": 7,
"Years": 0
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"status_code": 404,
"data": {
"Error": {
"Code": "ObjectLockConfigurationNotFoundError",
"Message": "Object Lock configuration does not exist for this bucket",
"BucketName": "c7n-test-s3-bucket-bucket-key-disabled"
},
"ResponseMetadata": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"status_code": 200,
"data": {
"ResponseMetadata": {},
"Buckets": [
{
"Name": "c7n-test-s3-bucket",
"CreationDate": {
"__class__": "datetime",
"year": 2023,
"month": 8,
"day": 22,
"hour": 13,
"minute": 27,
"second": 29,
"microsecond": 0
}
},
{
"Name": "c7n-test-s3-bucket-bucket-key-disabled",
"CreationDate": {
"__class__": "datetime",
"year": 2023,
"month": 8,
"day": 22,
"hour": 13,
"minute": 29,
"second": 11,
"microsecond": 0
}
}
],
"Owner": {
"DisplayName": "mandeep.bal",
"ID": "e7c8bb65a5fc49cf906715eae09de9e4bb7861a96361ba79b833aa45f6833b15"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"status_code": 200,
"data": {
"ResponseMetadata": {},
"ObjectLockConfiguration": {
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "GOVERNANCE",
"Days": 7,
"Years": 0
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"status_code": 404,
"data": {
"Error": {
"Code": "NoSuchBucket",
"Message": "The specified bucket does not exist",
"BucketName": "c7n-test-s3-bucket-bucket-key-disabled"
},
"ResponseMetadata": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"status_code": 200,
"data": {
"ResponseMetadata": {},
"Buckets": [
{
"Name": "c7n-test-s3-bucket",
"CreationDate": {
"__class__": "datetime",
"year": 2023,
"month": 8,
"day": 22,
"hour": 13,
"minute": 27,
"second": 29,
"microsecond": 0
}
},
{
"Name": "c7n-test-s3-bucket-bucket-key-disabled",
"CreationDate": {
"__class__": "datetime",
"year": 2023,
"month": 8,
"day": 22,
"hour": 13,
"minute": 29,
"second": 11,
"microsecond": 0
}
}
],
"Owner": {
"DisplayName": "mandeep.bal",
"ID": "e7c8bb65a5fc49cf906715eae09de9e4bb7861a96361ba79b833aa45f6833b15"
}
}
}
56 changes: 56 additions & 0 deletions tests/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -4540,3 +4540,59 @@ def test_bucket_encryption_invalid(self):
]
},
)


class S3ObjectLockFilterTest(BaseTest):
def test_query(self):
self.patch(s3.S3, "executor_factory", MainThreadExecutor)
self.patch(s3.S3LockConfigurationFilter, "executor_factory", MainThreadExecutor)
self.patch(s3, "S3_AUGMENT_TABLE", [])
factory = self.replay_flight_data('test_s3_bucket_object_lock_configuration')

p = self.load_policy(
{
'name': 'test-s3-bucket-key-disabled',
'resource': 'aws.s3',
'filters': [
{
'type': 'lock-configuration',
'key': 'Rule.DefaultRetention.Mode',
'value': 'GOVERNANCE',
}
]
},
session_factory=factory
)
resources = p.run()
self.assertEqual(len(resources), 1)
self.assertEqual(resources[0]['Name'], 'c7n-test-s3-bucket')
self.assertEqual(
resources[0]['ObjectLockConfiguration']['Rule']['DefaultRetention']['Mode'],
'GOVERNANCE'
)

def test_query_exception(self):
self.patch(s3.S3, "executor_factory", MainThreadExecutor)
self.patch(s3.S3LockConfigurationFilter, "executor_factory", MainThreadExecutor)
self.patch(s3, "S3_AUGMENT_TABLE", [])
log_mock = mock.MagicMock()
self.patch(s3.S3LockConfigurationFilter, "log", log_mock)

factory = self.replay_flight_data('test_s3_bucket_object_lock_configuration_exception')
p = self.load_policy(
{
'name': 'test-s3-bucket-key-disabled',
'resource': 'aws.s3',
'filters': [
{
'type': 'lock-configuration',
'key': 'Rule.DefaultRetention.Mode',
'value': 'GOVERNANCE',
}
]
},
session_factory=factory
)
resources = p.run()
self.assertEqual(len(resources), 1)
log_mock.error.assert_called()