Skip to content

Commit

Permalink
feat(codebuild): add new check `codebuild_report_group_export_encrypt…
Browse files Browse the repository at this point in the history
…ed` (#5384)

Co-authored-by: Sergio Garcia <[email protected]>
Co-authored-by: Sergio <[email protected]>
  • Loading branch information
3 people authored Oct 15, 2024
1 parent aac6038 commit 78d2fb9
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 4 deletions.
1 change: 1 addition & 0 deletions permissions/create_role_to_assume_cfn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Resources:
- 'cloudtrail:GetInsightSelectors'
- 'codeartifact:List*'
- 'codebuild:BatchGet*'
- 'codebuild:ListReportGroups'
- 'cognito-idp:GetUserPoolMfaConfig'
- 'dlm:Get*'
- 'drs:Describe*'
Expand Down
1 change: 1 addition & 0 deletions permissions/prowler-additions-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"cloudtrail:GetInsightSelectors",
"codeartifact:List*",
"codebuild:BatchGet*",
"codebuild:ListReportGroups",
"cognito-idp:GetUserPoolMfaConfig",
"dlm:Get*",
"drs:Describe*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "codebuild_report_group_export_encrypted",
"CheckTitle": "CodeBuild report group exports are encrypted at rest",
"CheckType": [],
"ServiceName": "codebuild",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Ensure that CodeBuild report group exports are encrypted at rest.",
"Risk": "If CodeBuild report group exports are not encrypted, sensitive data could be exposed to unauthorized access.",
"RelatedUrl": "https://docs.aws.amazon.com/codebuild/latest/userguide/report-group-export-settings.html",
"Remediation": {
"Code": {
"CLI": "aws codebuild update-report-group --arn <report-group-arn> --export-config \"exportConfigType=S3, s3Destination={bucket=, encryptionDisabled=true}\"",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/codebuild-controls.html#codebuild-7",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure CodeBuild report group exports to use encryption at rest. This can be done by specifying a KMS key ID when creating or updating the report group.",
"Url": "https://docs.aws.amazon.com/codebuild/latest/userguide/report-group-export-settings.html"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.codebuild.codebuild_client import codebuild_client


class codebuild_report_group_export_encrypted(Check):
def execute(self):
findings = []
for report_group in codebuild_client.report_groups.values():
if report_group.export_config and report_group.export_config.type == "S3":
report = Check_Report_AWS(self.metadata())
report.resource_id = report_group.name
report.resource_arn = report_group.arn
report.region = report_group.region
report.resource_tags = report_group.tags
report.status = "PASS"
report.status_extended = f"CodeBuild report group {report_group.name} exports are encrypted at {report_group.export_config.bucket_location} with KMS key {report_group.export_config.encryption_key}."

if not report_group.export_config.encrypted:
report.status = "FAIL"
report.status_extended = f"CodeBuild report group {report_group.name} exports are not encrypted at {report_group.export_config.bucket_location}."

findings.append(report)

return findings
75 changes: 75 additions & 0 deletions prowler/providers/aws/services/codebuild/codebuild_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def __init__(self, provider):
self.__threading_call__(self._list_builds_for_project, self.projects.values())
self.__threading_call__(self._batch_get_builds, self.projects.values())
self.__threading_call__(self._batch_get_projects, self.projects.values())
self.report_groups = {}
self.__threading_call__(self._list_report_groups)
self.__threading_call__(
self._batch_get_report_groups, self.report_groups.values()
)

def _list_projects(self, regional_client):
logger.info("Codebuild - Listing projects...")
Expand Down Expand Up @@ -119,6 +124,60 @@ def _batch_get_projects(self, project):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)

def _list_report_groups(self, regional_client):
logger.info("Codebuild - Listing report groups...")
try:
list_report_groups_paginator = regional_client.get_paginator(
"list_report_groups"
)
for page in list_report_groups_paginator.paginate():
for report_group_arn in page["reportGroups"]:
if not self.audit_resources or (
is_resource_filtered(report_group_arn, self.audit_resources)
):
self.report_groups[report_group_arn] = ReportGroup(
arn=report_group_arn,
name=report_group_arn.split(":")[-1].split("/")[-1],
region=regional_client.region,
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)

def _batch_get_report_groups(self, report_group):
logger.info("Codebuild - Getting report groups...")
try:
report_group_info = self.regional_clients[
report_group.region
].batch_get_report_groups(reportGroupArns=[report_group.arn])[
"reportGroups"
][
0
]

report_group.status = report_group_info.get("status", "DELETING")

export_config = report_group_info.get("exportConfig", {})
if export_config:
s3_destination = export_config.get("s3Destination", {})
report_group.export_config = ExportConfig(
type=export_config.get("exportConfigType", "NO_EXPORT"),
bucket_location=(
f"s3://{s3_destination.get('bucket', '')}/{s3_destination.get('path', '')}"
if s3_destination.get("bucket", "")
else ""
),
encryption_key=s3_destination.get("encryptionKey", ""),
encrypted=(not s3_destination.get("encryptionDisabled", True)),
)

report_group.tags = report_group_info.get("tags", [])
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)


class Build(BaseModel):
id: str
Expand Down Expand Up @@ -160,3 +219,19 @@ class Project(BaseModel):
s3_logs: Optional[s3Logs]
cloudwatch_logs: Optional[CloudWatchLogs]
tags: Optional[list]


class ExportConfig(BaseModel):
type: str
bucket_location: str
encryption_key: str
encrypted: bool


class ReportGroup(BaseModel):
arn: str
name: str
region: str
status: Optional[str]
export_config: Optional[ExportConfig]
tags: Optional[list]
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from unittest.mock import patch

import botocore
from moto import mock_aws

from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
set_mocked_aws_provider,
)

orig = botocore.client.BaseClient._make_api_call


def mock_make_api_call_encrypted(self, operation_name, kwarg):
if operation_name == "ListReportGroups":
return {
"reportGroups": [
f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-encrypted"
]
}
elif operation_name == "BatchGetReportGroups":
return {
"reportGroups": [
{
"name": "test-report-group-encrypted",
"arn": f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-encrypted",
"exportConfig": {
"exportConfigType": "S3",
"s3Destination": {
"bucket": "test-bucket",
"path": "test-path",
"encryptionKey": f"arn:aws:kms:eu-west-1:{AWS_ACCOUNT_NUMBER}:key/12345678-1234-1234-1234-{AWS_ACCOUNT_NUMBER}",
"encryptionDisabled": False,
},
},
"tags": [{"key": "Name", "value": "test-report-group-encrypted"}],
"status": "ACTIVE",
}
]
}
# If we don't want to patch the API call
return orig(self, operation_name, kwarg)


def mock_make_api_call_not_encrypted(self, operation_name, kwarg):
if operation_name == "ListReportGroups":
return {
"reportGroups": [
f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-not-encrypted"
]
}
elif operation_name == "BatchGetReportGroups":
return {
"reportGroups": [
{
"name": "test-report-group-not-encrypted",
"arn": f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-not-encrypted",
"exportConfig": {
"exportConfigType": "S3",
"s3Destination": {
"bucket": "test-bucket",
"path": "test-path",
"encryptionKey": "",
"encryptionDisabled": True,
},
},
"tags": [
{"key": "Name", "value": "test-report-group-not-encrypted"}
],
"status": "ACTIVE",
}
]
}
# If we don't want to patch the API call
return orig(self, operation_name, kwarg)


class Test_codebuild_report_group_export_encrypted:
@mock_aws
def test_no_report_groups(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])

from prowler.providers.aws.services.codebuild.codebuild_service import Codebuild

with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), patch(
"prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted.codebuild_client",
new=Codebuild(aws_provider),
):
from prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted import (
codebuild_report_group_export_encrypted,
)

check = codebuild_report_group_export_encrypted()
result = check.execute()

assert len(result) == 0

@patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_encrypted
)
@mock_aws
def test_report_group_export_encrypted(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])

from prowler.providers.aws.services.codebuild.codebuild_service import Codebuild

with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), patch(
"prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted.codebuild_client",
new=Codebuild(aws_provider),
):
from prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted import (
codebuild_report_group_export_encrypted,
)

check = codebuild_report_group_export_encrypted()
result = check.execute()

assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"CodeBuild report group test-report-group-encrypted exports are encrypted at s3://test-bucket/test-path with KMS key arn:aws:kms:eu-west-1:{AWS_ACCOUNT_NUMBER}:key/12345678-1234-1234-1234-{AWS_ACCOUNT_NUMBER}."
)
assert result[0].resource_id == "test-report-group-encrypted"
assert (
result[0].resource_arn
== f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-encrypted"
)
assert result[0].resource_tags == [
{"key": "Name", "value": "test-report-group-encrypted"}
]
assert result[0].region == AWS_REGION_EU_WEST_1

@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_not_encrypted,
)
@mock_aws
def test_report_group_export_not_encrypted(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])

from prowler.providers.aws.services.codebuild.codebuild_service import Codebuild

with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), patch(
"prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted.codebuild_client",
new=Codebuild(aws_provider),
):
from prowler.providers.aws.services.codebuild.codebuild_report_group_export_encrypted.codebuild_report_group_export_encrypted import (
codebuild_report_group_export_encrypted,
)

check = codebuild_report_group_export_encrypted()
result = check.execute()

assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "CodeBuild report group test-report-group-not-encrypted exports are not encrypted at s3://test-bucket/test-path."
)
assert result[0].resource_id == "test-report-group-not-encrypted"
assert (
result[0].resource_arn
== f"arn:aws:codebuild:eu-west-1:{AWS_ACCOUNT_NUMBER}:report-group/test-report-group-not-encrypted"
)
assert result[0].resource_tags == [
{"key": "Name", "value": "test-report-group-not-encrypted"}
]
assert result[0].region == AWS_REGION_EU_WEST_1
Loading

0 comments on commit 78d2fb9

Please sign in to comment.