From 965111245a58c87715bbfad81b54470be830360c Mon Sep 17 00:00:00 2001 From: Neil Millard Date: Wed, 2 Jul 2025 10:20:15 +0100 Subject: [PATCH] feat(aws): add new check for Codebuild projects visibility (#8127) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...ject_not_publicly_accessible.metadata.json | 30 +++ ...debuild_project_not_publicly_accessible.py | 26 +++ .../services/codebuild/codebuild_service.py | 2 + .../__init__.py | 0 ...ld_project_not_publicly_accessible_test.py | 176 ++++++++++++++++++ .../codebuild/codebuild_service_test.py | 3 + 8 files changed, 238 insertions(+) create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py create mode 100644 tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py create mode 100644 tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 1fc1375568..0fccd64ef4 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -44,6 +44,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `monitor_alert_service_health_exists` check for Azure provider [(#8067)](https://github.com/prowler-cloud/prowler/pull/8067) - Replace `Domain.Read.All` with `Directory.Read.All` in Azure and M365 docs [(#8075)](https://github.com/prowler-cloud/prowler/pull/8075) - Refactor IaC provider to use Checkov as Python library [(#8093)](https://github.com/prowler-cloud/prowler/pull/8093) +- New check `codebuild_project_not_publicly_accessible` for AWS provider [(#8127)](https://github.com/prowler-cloud/prowler/pull/8127) ### Fixed - Consolidate Azure Storage file service properties to the account level, improving the accuracy of the `storage_ensure_file_shares_soft_delete_is_enabled` check [(#8087)](https://github.com/prowler-cloud/prowler/pull/8087) diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json new file mode 100644 index 0000000000..5d7427df8e --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "codebuild_project_not_publicly_accessible", + "CheckTitle": "Ensure AWS CodeBuild projects are not public", + "CheckType": [], + "ServiceName": "codebuild", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:codebuild:region:account-id:project:project-name", + "Severity": "high", + "ResourceType": "AwsCodeBuildProject", + "Description": "Check for CodeBuild projects ensuring that the project visibility is appropriate", + "Risk": "Public CodeBuild Project ensures all build logs and artifacts are available to the public. Environment variables, source code, and other sensitive information may have been output to the build logs and artifacts. You must be careful about what information is output to the build logs.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "NativeIaC": "", + "Terraform": "", + "CLI": "aws codebuild update-project --name --project-visibility PRIVATE", + "Other": "" + }, + "Recommendation": { + "Text": "Ensure that all CodeBuild projects are private to avoid fact gathering about builds from an Attacker.", + "Url": "https://docs.aws.amazon.com/codebuild/latest/userguide/public-builds.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py new file mode 100644 index 0000000000..6fd42e971c --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible.py @@ -0,0 +1,26 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.codebuild.codebuild_client import codebuild_client + + +class codebuild_project_not_publicly_accessible(Check): + def execute(self) -> List[Check_Report_AWS]: + findings = [] + + projects = codebuild_client.projects + for arn, project in projects.items(): + report = Check_Report_AWS(self.metadata(), resource=project) + report.resource_id = project.name + report.resource_arn = arn + report.region = project.region + report.status = "FAIL" + report.status_extended = f"CodeBuild project {project.name} is public." + + if project.project_visibility == "PRIVATE": + report.status = "PASS" + report.status_extended = f"CodeBuild project {project.name} is private." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/codebuild/codebuild_service.py b/prowler/providers/aws/services/codebuild/codebuild_service.py index c7210a67dd..475f578e94 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -121,6 +121,7 @@ class Codebuild(AWSService): ) project.tags = project_info.get("tags", []) project.service_role_arn = project_info.get("serviceRole", "") + project.project_visibility = project_info.get("projectVisibility", "") except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -222,6 +223,7 @@ class Project(BaseModel): s3_logs: Optional[s3Logs] cloudwatch_logs: Optional[CloudWatchLogs] tags: Optional[list] + project_visibility: Optional[str] = None class ExportConfig(BaseModel): diff --git a/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py new file mode 100644 index 0000000000..f94146d4fc --- /dev/null +++ b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/codebuild_project_not_publicly_accessible_test.py @@ -0,0 +1,176 @@ +from unittest import mock + +from prowler.providers.aws.services.codebuild.codebuild_service import Project + +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" + + +class Test_codebuild_project_not_publicly_accessible: + def test_project_public(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="PUBLIC", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_private(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="PRIVATE", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is private." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_no_visibility_set(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility=None, + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION + + def test_project_empty_visibility(self): + codebuild_client = mock.MagicMock + project_name = "test-project" + project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" + codebuild_client.projects = { + project_arn: Project( + name=project_name, + arn=project_arn, + region="eu-west-1", + project_visibility="", + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_not_publicly_accessible.codebuild_project_not_publicly_accessible import ( + codebuild_project_not_publicly_accessible, + ) + + check = codebuild_project_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} is public." + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION diff --git a/tests/providers/aws/services/codebuild/codebuild_service_test.py b/tests/providers/aws/services/codebuild/codebuild_service_test.py index f35c55ed58..9884b3a09f 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -28,6 +28,7 @@ build_id = "test:93f838a7-cd20-48ae-90e5-c10fbbc78ca6" last_invoked_time = datetime.now() - timedelta(days=2) bitbucket_url = "https://bitbucket.org/example/repo.git" secondary_bitbucket_url = "https://bitbucket.org/example/secondary-repo.git" +project_visibility = "PRIVATE" report_group_arn = f"arn:{AWS_COMMERCIAL_PARTITION}:codebuild:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:report-group/{project_name}" @@ -71,6 +72,7 @@ def mock_make_api_call(self, operation_name, kwarg): }, }, "tags": [{"key": "Name", "value": project_name}], + "projectVisibility": project_visibility, } ] } @@ -152,6 +154,7 @@ class Test_Codebuild_Service: ) assert codebuild.projects[project_arn].tags[0]["key"] == "Name" assert codebuild.projects[project_arn].tags[0]["value"] == project_name + assert codebuild.projects[project_arn].project_visibility == project_visibility # Asserttions related with report groups assert len(codebuild.report_groups) == 1 assert isinstance(codebuild.report_groups, dict)